ScanLogic.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. namespace app\common\logic;
  3. use app\common\library\Log;
  4. use app\common\model\LedgerWalletModel;
  5. use app\common\model\OfflineRechargeRecordModel;
  6. use app\common\model\OfflineRechargeVerifyModel;
  7. use app\common\model\ParametersModel;
  8. use app\common\model\UserModel;
  9. use app\common\model\ServersModel;
  10. use Exception;
  11. use fast\Action;
  12. use fast\Asset;
  13. use fast\Http;
  14. use fast\RechargeOrderType;
  15. use fast\RechargeStatus;
  16. use fast\RechargeType;
  17. use think\Db;
  18. use think\Env;
  19. use think\Error;
  20. use think\Model;
  21. class ScanLogic
  22. {
  23. /**
  24. * 币安API地址
  25. * @var string
  26. */
  27. private string $bscApiUrl = '';
  28. /**
  29. * 币安链USDT地址
  30. * @var string
  31. */
  32. private string $bscUSDAddress = '0x55d398326f99059ff775485246999027b3197955';
  33. /**
  34. * 平台收款钱包地址
  35. * @var string
  36. */
  37. private string $collectionAddress = '';
  38. /**
  39. * 最少确认节点数量
  40. * @var int
  41. */
  42. private int $minConfirmations = 12;
  43. /**
  44. * value的放大比例 固定为: 10的18次方
  45. * @var int
  46. */
  47. private string $valueScale = '1000000000000000000';
  48. public function __construct()
  49. {
  50. $apikey = (new ParametersModel)->getValue('bscApiKey');
  51. if (empty($apikey)) {
  52. dump("读取币安apikey错误");
  53. exit(1);
  54. }
  55. $collectionAddress = Env::get('rental.pay_address');
  56. if (empty($collectionAddress)) {
  57. dump("读取平台收款钱包地址错误");
  58. exit(1);
  59. }
  60. $this->collectionAddress = strtolower($collectionAddress);
  61. $this->bscApiUrl = "https://api.bscscan.com/api?module=account&action=tokentx&contractaddress=$this->bscUSDAddress&page=1&offset=10000&endblock=99999999&apikey=$apikey&address=";
  62. $this->hashStatusUrl = "https://api.bscscan.com/api?module=transaction&action=gettxreceiptstatus&apikey=$apikey&txhash=";
  63. }
  64. public function scanRechargeOrders()
  65. {
  66. $recharges = (new OfflineRechargeRecordModel())->where([
  67. 'type' => RechargeType::CashFromChain,
  68. 'status' => RechargeStatus::StatusUnAuth,
  69. ])->order('id DESC')->limit(20)->select();
  70. if (!is_null($recharges)) {
  71. foreach ($recharges as $v) {
  72. $vArr = $v->toArray();
  73. // 请求api
  74. $result = $this->verifyTxHash($vArr);
  75. // 记录执行记录
  76. (new OfflineRechargeVerifyModel())->insert([
  77. 'order_id' => $vArr['id'],
  78. 'user_id' => $vArr['user_id'],
  79. 'result' => $result == '',
  80. 'fail_reason' => $result,
  81. 'create_time' => time(),
  82. ]);
  83. }
  84. }
  85. }
  86. /*
  87. * 扫描收款地址
  88. */
  89. public function scanCollectionAddress()
  90. {
  91. $max_block_number = (new OfflineRechargeRecordModel())
  92. ->max('block_number');
  93. $max_block_number ++;
  94. $url = $this->bscApiUrl . $this->collectionAddress . "&startblock=" . $max_block_number;
  95. //$user['address'] = strtolower($user['address']); // 转小写
  96. // 发起请求
  97. $body = Http::get($url);
  98. if (empty($body)) {
  99. return "api返回内容为空";
  100. }
  101. // 转成数组
  102. $rsArr = json_decode($body, true);
  103. if (empty($rsArr) || !is_array($rsArr)) {
  104. return "api返回数据异常";
  105. }
  106. if ($rsArr['status'] != '1') {
  107. return 'api返回status不为1,错误信息:' . $rsArr['message'];
  108. }
  109. $errMsg = '';
  110. $new_block_number = 0;
  111. $all_user_address = [];//钱包地址为键值的所有会员的数组
  112. $orderInfo = [];
  113. // 查询匹配交易列表
  114. (new Log())->info('开始匹配交易');
  115. (new Log())->info($rsArr['result']);
  116. foreach ($rsArr['result'] as $v) {
  117. $errMsg = '';
  118. if ($v['to'] != $this->collectionAddress) {
  119. $new_block_number = $v['blockNumber'];
  120. (new Log())->info('收款地址不是平台地址:' . $v['hash']);
  121. continue;//转载地址不是收款地址的数据直接跳过不处理
  122. }
  123. if ($v['contractAddress'] != $this->bscUSDAddress) {
  124. // $errMsg = '币种错误: ' . $v['contractAddress'];
  125. $new_block_number = $v['blockNumber'];
  126. continue;//交易币种的合约地址不是指定代币,不处理。一般币种为USDT
  127. }
  128. $orderInfo['amount'] = bcdiv($v['value'], $this->valueScale, 6);//交易金额
  129. if(!($orderInfo['amount'] > 0)){
  130. $new_block_number = $v['blockNumber'];
  131. continue;//交易金额必须大于0
  132. }else{
  133. $minUsdt = (new ParametersModel)->getValue('minUsdt') ?? '0';//最小购买金额
  134. if($minUsdt > $orderInfo['amount']){
  135. $new_block_number = $v['blockNumber'];
  136. $errMsg = '交易金额必须大于系统最小购买金额:' . $v['hash'];
  137. (new Log())->info('交易金额必须大于系统最小购买金额:' . $v['hash']);
  138. continue;//交易金额必须大于系统最小购买金额
  139. }
  140. }
  141. $check_user = (new UserModel())->getByAddress(strtolower($v['from']));
  142. if(empty($check_user)){
  143. $new_block_number = $v['blockNumber'];
  144. (new Log())->info('转账地址不是平台用户账号:' . $v['hash']);
  145. continue;//转账地址不是平台账号的数据不处理
  146. }
  147. $orderInfo['user_id'] = $check_user['id'];
  148. // if(empty($all_user_address)){
  149. // $all_user_address = (new UserModel())->getAllAddress();
  150. // }
  151. // if(!(isset($all_user_address[$v['from']]) || isset($all_user_address[strtolower($v['from'])]))){
  152. // $new_block_number = $v['blockNumber'];
  153. // (new Log())->info('转账地址不是平台用户账号:' . $v['hash']);
  154. // continue;//转账地址不是平台账号的数据不处理
  155. // }else{
  156. // if(isset($all_user_address[$v['from']])){
  157. // $orderInfo['user_id'] = $all_user_address[$v['from']];
  158. // }else{
  159. // $orderInfo['user_id'] = $all_user_address[strtolower($v['from'])];
  160. // }
  161. // }
  162. $rsData = $this->getHashStatus($v['hash']);
  163. if($rsData['status'] == 0){//当前哈希交易状态为失败,跳出循环,等待下次判断,若真是失败交易,过几分钟后,这个数据不会再出现,这个接口拿到的都是有效的交易数据
  164. break;
  165. }
  166. //判断当前hash是否存在于充值记录
  167. $check_recharge_record = (new OfflineRechargeRecordModel())
  168. ->where('tx_hash', $v['hash'])
  169. ->find();
  170. if(empty($check_recharge_record)){//充值记录表中没有当前记录,则补充插入
  171. (new Log())->info('补单:' . $v['hash']);
  172. $orderInfo['order_type'] = RechargeOrderType::RentalPower;//订单类型
  173. //判断是否服务器购买订单
  174. $servers_list = (new ServersModel())
  175. ->field('id,title,price,power')
  176. ->select();
  177. foreach ($servers_list as $item){
  178. if($item['price'] == $orderInfo['amount']){
  179. $orderInfo['order_type'] = RechargeOrderType::RentalServer;//订单类型
  180. }
  181. }
  182. try {
  183. $orderInfo['id'] = (new OfflineRechargeRecordModel())->createRecord(RechargeType::CashFromChain, $v['hash'], $orderInfo['user_id'], $orderInfo['amount'], RechargeStatus::StatusUnAuth, $orderInfo['order_type'], $v['blockNumber'], '自动补单');
  184. } catch (Exception $e) {
  185. (new Log())->error('自动补单失败:' . $e);
  186. break;
  187. }
  188. }else if($check_recharge_record['status'] == 1){
  189. $new_block_number = $v['blockNumber'];
  190. continue;//当前交易已处理
  191. }else{
  192. $orderInfo = $check_recharge_record->toArray();
  193. if (bccomp($v['value'], bcmul($check_recharge_record['amount'], $this->valueScale, 6)) !== 0) {
  194. $errMsg = '金额错误: ' . $v['value'] . ',' . $check_recharge_record['amount'];
  195. (new Log())->info('金额错误: ' . $v['value'] . ',' . $check_recharge_record['amount']);
  196. }
  197. }
  198. $orderInfo['block_number'] = $v['blockNumber'];
  199. // 校验正确,更新订单信息为成功,并触发各项收益的发放
  200. $updateErrMsg = $this->updateOrderStatus($orderInfo, $errMsg == '');
  201. if (!empty($updateErrMsg)) {
  202. $errMsg .= '|' . $updateErrMsg;
  203. }
  204. // 记录执行记录
  205. (new OfflineRechargeVerifyModel())->insert([
  206. 'order_id' => $orderInfo['id'],
  207. 'user_id' => $orderInfo['user_id'],
  208. 'result' => $errMsg == '',
  209. 'fail_reason' => $errMsg,
  210. 'create_time' => time(),
  211. ]);
  212. }
  213. if($new_block_number > $max_block_number){
  214. //更新最新一条充值记录的区块为最新扫描过的区块
  215. $max_info = (new OfflineRechargeRecordModel())
  216. ->order('id desc')
  217. ->find();
  218. $max_info->save([
  219. 'block_number' => $new_block_number
  220. ]);
  221. }
  222. return $errMsg;
  223. }
  224. private function verifyTxHash(array $orderInfo): string
  225. {
  226. $user = (new UserModel())->getById($orderInfo['user_id']);
  227. if (empty($user)) {
  228. return "用户不存在,ID:" . $orderInfo['user_id'];
  229. }
  230. $rsData = $this->getHashStatus($orderInfo['tx_hash']);
  231. if($rsData['status'] == 0){
  232. return $rsData['msg'];
  233. }
  234. $user['address'] = strtolower($user['address']); // 转小写
  235. $url = $this->bscApiUrl . $user['address'];
  236. // 发起请求
  237. $body = Http::get($url);
  238. if (empty($body)) {
  239. return "api返回内容为空";
  240. }
  241. // 转成数组
  242. $rsArr = json_decode($body, true);
  243. if (empty($rsArr) || !is_array($rsArr)) {
  244. return "api返回数据异常";
  245. }
  246. if ($rsArr['status'] != '1') {
  247. return 'api返回status不为1,错误信息:' . $rsArr['message'];
  248. }
  249. $errMsg = '';
  250. // 查询匹配交易列表
  251. foreach ($rsArr['result'] as $v) {
  252. // 跳过hash不匹配或确认区块不足的记录
  253. if ($v['hash'] != $orderInfo['tx_hash'] || $v['confirmations'] < $this->minConfirmations) {
  254. continue;
  255. }
  256. // 开始校验
  257. if ($v['from'] != $user['address']) {
  258. $errMsg = 'from地址不是用户地址,from: ' . $v['from'];
  259. } else if ($v['to'] != $this->collectionAddress) {
  260. $errMsg = 'to地址不是平台收款地址,to: ' . $v['to'];
  261. } else if ($v['contractAddress'] != $this->bscUSDAddress) {
  262. $errMsg = '币种错误: ' . $v['contractAddress'];
  263. } else if (bccomp($v['value'], bcmul($orderInfo['amount'], $this->valueScale, 6)) !== 0) {
  264. $errMsg = '金额错误: ' . $v['value'] . ',' . $orderInfo['amount'];
  265. }
  266. break;
  267. }
  268. // 校验正确,更新订单信息为成功,并触发各项收益的发放
  269. $updateErrMsg = $this->updateOrderStatus($orderInfo, $errMsg == '');
  270. if (!empty($updateErrMsg)) {
  271. $errMsg .= '|' . $updateErrMsg;
  272. }
  273. return $errMsg;
  274. }
  275. private function updateOrderStatus(array $orderInfo, bool $verify): string
  276. {
  277. if (!$verify) { // 验证失败的处理
  278. try {
  279. (new Log())->error('充值交易验证失败: ' . $verify);
  280. (new OfflineRechargeRecordModel())->updateOrderStatus($orderInfo['id'], RechargeStatus::StatusAuthFail, $orderInfo['block_number']);
  281. } catch (Exception $e) {
  282. return $e->getMessage();
  283. }
  284. return '';
  285. }
  286. if($orderInfo['order_type'] == 1){
  287. //算力租赁订单处理
  288. // 查询兑换比例
  289. $usdtToPower = (new ParametersModel)->getValue('usdtToPowerRate');
  290. $usdtToPowerFloat = floatval($usdtToPower);
  291. if (is_null($usdtToPower) || $usdtToPowerFloat <= 0) {
  292. return '获取USDT兑换算力的比例失败';
  293. }
  294. $uid = $orderInfo['user_id'];
  295. $fee = $orderInfo['amount'];
  296. $power = bcmul($fee, $usdtToPowerFloat, 6); // 该用户兑得的算力
  297. // 启动事务
  298. Db::startTrans();
  299. try {
  300. // 更新总算力和账变
  301. (new LedgerWalletModel)->changeWalletAccount($uid, Asset::POWER, $power, Action::PowerRentalPower, $orderInfo['id']);
  302. // 更新服务器算力,不账变
  303. (new LedgerWalletModel)->changeWalletOnly($uid, Asset::RENTAL_POWER, $power);
  304. // 更新自己(有效会员时间)和所有上级的信息(有效直推人数和团队总算力)
  305. (new UserModel())->updateForRental($uid, $power);
  306. // 发放直推USDT收益
  307. (new LedgerWalletModel)->sendUsdtProfit($uid, $fee);
  308. // 发放直推算力收益
  309. (new LedgerWalletModel)->sendDirectProfit($uid, $power);
  310. // 发代数收益
  311. (new LedgerWalletModel)->sendGenerateProfit($uid, $fee);
  312. // 发放见点奖
  313. (new LedgerWalletModel)->sendRegBonus($uid, $fee);
  314. // 更新购买(充值)记录
  315. (new OfflineRechargeRecordModel())->updateOrderStatus($orderInfo['id'], RechargeStatus::StatusAuthSuccess, 0, $power);
  316. // 提交事务
  317. Db::commit();
  318. } catch (Exception $e) {
  319. // 回滚事务
  320. Db::rollback();
  321. return $e->getMessage();
  322. }
  323. }else{
  324. //服务器购买订单处理
  325. $servers_info = (new ServersModel())
  326. ->where('price', $orderInfo['amount'])
  327. ->find();
  328. if(empty($servers_info)){
  329. return '支付价格和服务器价格不匹配';
  330. }
  331. // 启动事务
  332. Db::startTrans();
  333. try {
  334. // 更新服务器算力,不账变
  335. (new LedgerWalletModel)->changeWalletOnly($orderInfo['user_id'], Asset::SERVER_POWER, $servers_info['power']);
  336. // 发放服务器市场推荐相关收益
  337. //(new LedgerWalletModel)->sendMarketBonus($orderInfo['user_id'], $servers_info);
  338. // 更新购买(充值)记录
  339. (new OfflineRechargeRecordModel())->updateOrderStatus($orderInfo['id'], RechargeStatus::StatusAuthSuccess, 0, $servers_info['power']);
  340. // 提交事务
  341. Db::commit();
  342. } catch (Exception $e) {
  343. // 回滚事务
  344. Db::rollback();
  345. return $e->getMessage();
  346. }
  347. }
  348. return '';
  349. }
  350. /**
  351. * 获取哈希地址成功状态
  352. *
  353. * api接口返回数据格式
  354. * {
  355. "status": "1",
  356. "message": "OK",
  357. "result": {
  358. "status": "1"
  359. }
  360. }
  361. * @param $orderInfo
  362. * @return array|string
  363. */
  364. private function getHashStatus($tx_hash):array
  365. {
  366. $rsData['status'] = 0;//0 失败,1 成功
  367. $rsData['msg'] = '';
  368. $hashStatusUrl = $this->hashStatusUrl . $tx_hash;//接口地址
  369. $body = Http::get($hashStatusUrl);
  370. if (empty($body)) {
  371. $rsData['msg'] = '状态api返回内容为空';
  372. return $rsData;
  373. }
  374. // 转成数组
  375. $rsArr = json_decode($body, true);
  376. if (empty($rsArr) || !is_array($rsArr)) {
  377. $rsData['msg'] = '状态api返回数据异常';
  378. return $rsData;
  379. }
  380. if ($rsArr['status'] != '1') {
  381. $rsData['msg'] = '状态api返回status不为1,当前值为:' . $rsArr['status'];
  382. return $rsData;
  383. }
  384. if ($rsArr['result']['status'] != 1) {
  385. $rsData['msg'] = '状态api返回result中的status不为1,当前值为:' . $rsArr['result']['status'];
  386. return $rsData;
  387. }
  388. $rsData['status'] = 1;
  389. return $rsData;
  390. }
  391. }