IosPay.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <?php
  2. /**
  3. * iOS应用内支付(苹果内购支付)
  4. *
  5. */
  6. namespace app\api\controller\v1;
  7. use app\api\controller\Api;
  8. use app\common\model\Members;
  9. use app\common\model\Game;
  10. use app\common\model\ChannelFrozen;
  11. use app\common\model\Cpurl;
  12. use app\common\model\Pay as PayModel;
  13. use app\common\model\PayCpinfo;
  14. use app\common\model\MemberChannelGame;
  15. use app\common\model\Setting;
  16. use app\common\logic\PayCallback;
  17. use app\common\logic\Pay as PayLogic;
  18. use think\Db;
  19. class IosPay extends Api {
  20. protected $payRequestLimit = 15; //重复下单的限制时间
  21. protected $redis; //redis的句柄对象
  22. //苹果后台设置产品对应的ProductID
  23. private $_apple_product_ids = [];
  24. /**
  25. * 不进行父类的登录验证,所以增加构造方法重写了父类的初始化方法
  26. */
  27. public function _initialize()
  28. {
  29. parent::_initialize();
  30. $settingModel = new Setting;
  31. $this->_apple_product_ids = json_decode($settingModel::getSetting('APPLE_PRODUCT_IDS'),true);
  32. //json解码没有错误发生时
  33. if (JSON_ERROR_NONE == json_last_error()) {
  34. }
  35. else{
  36. $this->jsonResult('',0,'苹果ProductID,json解码错误,请到后台确认配置是否正确');
  37. }
  38. $this->redis = \think\Cache::store('default')->handler();
  39. }
  40. /**
  41. * 新增订单
  42. *
  43. * @return array
  44. */
  45. public function index() {
  46. die();
  47. $gameid = $this->input('gameid'); //游戏ID
  48. $member_id = $this->input('userid'); //用户ID
  49. $serverid = $this->input('serverid'); //服务器ID
  50. $servername = $this->input('servername'); //区服名称
  51. $amount = $this->input('amount'); //充值金额
  52. $roleid = $this->input('roleid'); //角色ID
  53. $attach = $this->input('attach'); //上一级的扩展参数,多数是订单编号[麻花网络自己聚合自己时,会加个aoyou###前缀]
  54. $imeil = trim($this->input('imeil')); //手机imeil码
  55. $productname= $this->input('productname'); //消费的商品名称
  56. $rolename = $this->input('rolename',''); //角色名
  57. $rolelevel = $this->input('rolelevel',''); //角色角色等级
  58. $orderid = makeOrderid();
  59. $pay_status = 0; //cy_pay表的支付状态
  60. $cp_payflag = 0; //cy_paycpinfo表的支付状态
  61. $memberModel = new Members;
  62. $frozenModel = new ChannelFrozen;
  63. $cpurlModel = new Cpurl;
  64. $gameModel = new Game;
  65. $payModel = new PayModel;
  66. $payCpinfoModel = new PayCpinfo;
  67. $memberChannelGameModel = new MemberChannelGame;
  68. $checkResult = $this->validate($this->input,
  69. [
  70. ['gameid', 'require|integer', '游戏id不能为空|游戏ID必须为整型'],
  71. ['userid', 'require|integer', '用户ID不能为空|用户ID必须为整型'],
  72. ['serverid', 'require|max:50', '游戏服务器id不能为空|游戏服务器id不能超过50个字符'],
  73. ['servername', 'require|max:255', '游戏区服名称不能为空|游戏区服名称不能超过255个字符'],
  74. ['amount', 'require|integer|gt:0', '充值金额不能为空|充值金额必须为正整型|充值金额必须大于0'],
  75. ['roleid', 'require', '角色id不能为空'],
  76. ['attach', 'require', '游戏合作方的订单参数不能为空'],
  77. ['channel_id', 'require|integer', '渠道ID不能为空|渠道ID必须为整型'],
  78. ['productname', 'require', '消费的商品名称不能为空'],
  79. ]);
  80. if (true !== $checkResult) {
  81. $this->jsonResult('', 0, $checkResult);
  82. }
  83. $memberInfo = $memberModel->field('username')->where(['id'=>$member_id])->find();
  84. if(empty($memberInfo)){
  85. $this->jsonResult('', 0, '用户不存在');
  86. }
  87. //玩家在这款游戏的归属渠道
  88. $channel_id = $memberChannelGameModel->where(['member_id'=>$member_id,'game_id'=>$gameid])->value('channel_id');
  89. if (empty($channel_id)) {
  90. $this->jsonResult('', 0, '无关联的归属渠道信息');
  91. }
  92. //当前游戏的渠道是否禁止消费(非归属渠道)
  93. if (isFrozenOption($this->input('channel_id'),$gameid,'consume')) {
  94. $this->jsonResult('', 0, '您所在渠道已经被禁止在此游戏消费');
  95. }
  96. //充值回调地址
  97. $cpurl = $cpurlModel->where(['appid'=>$this->appInfo['id']])->value('url');
  98. if(empty($cpurl)){
  99. $this->jsonResult('', 0, '没有回调地址,请通知我方配置');
  100. }
  101. //游戏信息
  102. $gameInfo = $gameModel->field('order_recheck')->where(['id' => $gameid])->find();
  103. if(empty($gameInfo)){
  104. $this->jsonResult('', 0, '游戏id参数错误');
  105. }
  106. //指定时间内,禁止重复下单
  107. if(!requestDuplicateCheck('pay_duplicate_'.$member_id.'_'.$gameid,$this->payRequestLimit)){
  108. $this->jsonResult('', 0, '充值请求过多,请于'.$this->payRequestLimit.'s以后,再次进行充值操作');
  109. }
  110. if($payModel->where(['attach'=>$attach,'gameid'=>$gameid])->find()){
  111. $this->redis->del('pay_duplicate_'.$member_id.'_'.$gameid);
  112. $this->jsonResult('', 0, '游戏合作方的订单参数不能重复');
  113. }
  114. //游戏防沉迷限制
  115. $restrictInfo = model('GameRestrict')->where(['game_id'=>$gameid,'restrict_type'=>'preventhook'])->find();
  116. if($restrictInfo){
  117. $isPreventHook = intval($restrictInfo['restrict_status']);
  118. }
  119. else{
  120. $settingModel = new Setting;
  121. $isPreventHook = intval($settingModel::getSetting('CHILD_LIMIT'));
  122. }
  123. if($isPreventHook == 2){
  124. // 已实名认证用户做防沉迷判断
  125. $membersTwoInfo = model('MembersTwo')->where(['userid' => $member_id])->find();
  126. if ($membersTwoInfo && !empty($membersTwoInfo['realname']) && !empty($membersTwoInfo['idcard']) && $membersTwoInfo['realname']<>-1 && $membersTwoInfo['idcard']<>-1) {
  127. $totalMonthRechargeAmt = model('Pay')->where(['userid' => $member_id,'status'=>1,'create_time'=>['egt',strtotime(date('Y-m-01'))]])->sum('amount');
  128. if (isMeetAgeByIDCard($membersTwoInfo['idcard'],0,8)){
  129. $this->jsonResult('', 0, '未满8岁周岁无法充值');
  130. }
  131. else if(isMeetAgeByIDCard($membersTwoInfo['idcard'],8,16)){
  132. if($amount>50){
  133. $this->jsonResult('', 0, '未满16周岁,单次充值不可超过50元');
  134. }
  135. if(($totalMonthRechargeAmt+$amount) > 200){
  136. $this->jsonResult('', 0, '未满16周岁,每月累充不可超过200元');
  137. }
  138. }
  139. else if(isMeetAgeByIDCard($membersTwoInfo['idcard'],16,18)){
  140. if($amount>100){
  141. $this->jsonResult('', 0, '未满18周岁,单次充值不可超过100元');
  142. }
  143. if(($totalMonthRechargeAmt+$amount) > 400){
  144. $this->jsonResult('', 0, '未满18周岁,每月累充不可超过400元');
  145. }
  146. }
  147. }
  148. }
  149. $payData['orderid'] = $orderid;
  150. $payData['use_coin_sn'] = ''; //平台币劵订单号
  151. $payData['amount'] = $amount;
  152. $payData['real_amount'] = $amount;
  153. $payData['real_ptb'] = 0;
  154. $payData['real_ptb_amount'] = 0;
  155. $payData['userid'] = $member_id;
  156. $payData['username'] = $memberInfo['username'];
  157. $payData['roleid'] = $roleid;
  158. $payData['rolename'] = $rolename;
  159. $payData['rolelevel'] = $rolelevel;
  160. $payData['paytype'] = 'ios-iap';
  161. $payData['productname'] = $productname;
  162. $payData['serverid'] = $serverid;
  163. $payData['gameid'] = $gameid;
  164. $payData['status'] = 0;
  165. $payData['ip'] = request()->ip();
  166. $payData['imeil'] = $imeil;
  167. $payData['create_time'] = NOW_TIMESTAMP;
  168. $payData['channel_id'] = $channel_id;
  169. $payData['recheck_status'] = $gameInfo['order_recheck'];
  170. $payData['attach'] = $attach;
  171. // 启动事务
  172. Db::startTrans();
  173. try{
  174. $result = ''; //返回值
  175. $payModel->insert($payData);
  176. //给Cp的用户名用子账号名
  177. $paycp_username = (!empty($this->input('sub_username')) ? $this->input('sub_username') : $memberInfo['username']);
  178. $str = "orderid=".urlencode($orderid)."&username=".urlencode($paycp_username)."&gameid=".$gameid."&roleid=".urlencode($roleid)."&serverid=".urlencode($serverid)."&paytype=".urlencode('ios-iap')."&amount=".$amount."&paytime=".NOW_TIMESTAMP."&attach=".urlencode($attach);
  179. $param = $str."&appkey=".urlencode($this->appInfo['appkey']);
  180. $md5params = md5($param);
  181. $params = $str . "&sign=".urlencode($md5params);
  182. $paycpData['orderid'] = $orderid;
  183. $paycpData['fcallbackurl'] = $cpurl;
  184. $paycpData['params'] = $params;
  185. $paycpData['create_time'] = NOW_TIMESTAMP;
  186. $paycpData['payflag'] = $cp_payflag;
  187. //写入cy_paycpinfo表数据
  188. $payCpinfoModel->insert($paycpData);
  189. $this->redis->del('pay_duplicate_'.$member_id.'_'.$gameid);
  190. // 提交事务
  191. Db::commit();
  192. } catch (\Exception $e) {
  193. // 回滚事务
  194. Db::rollback();
  195. $this->jsonResult('', 0, '订单生成失败'.$e->getMessage());
  196. }
  197. $result['orderid'] = $orderid;
  198. $this->jsonResult($result, 1, '订单生成成功',false);
  199. //先响应客户端请求,返回后,下面代码继续执行
  200. fastcgi_finish_request();
  201. //收集设备信息
  202. $platform = $this->input('platform');
  203. $this->saveDevices($gameid,$imeil,$platform);
  204. //更新游戏区服信息
  205. $this->saveGameServer($member_id,$gameid,$imeil,'pay');
  206. exit;
  207. }
  208. // 接收通知
  209. public function notify() {
  210. $receipt_data = $this->input('receipt_data');
  211. $orderid = $this->input('orderid');
  212. $transaction_id = $this->input('transaction_id');
  213. $product_id = $this->input('product_id');
  214. //写入日志
  215. log_message('apple receipt-data: '.$receipt_data,'error',LOG_PATH . '../paylog/');
  216. $payModel = new PayModel;
  217. $payCpinfoModel = new PayCpinfo;
  218. $membersModel = new Members;
  219. if ( empty($orderid) ) {
  220. $this->jsonResult('', 0, '订单编号不能为空');
  221. }
  222. if ( empty($receipt_data) ) {
  223. $this->jsonResult('', 0, '苹果购买凭证不能为空');
  224. }
  225. if ( empty($transaction_id) ) {
  226. $this->jsonResult('', 0, '苹果购买记录的唯一标识不能为空');
  227. }
  228. if ( empty($product_id) ) {
  229. $this->jsonResult('', 0, '苹果购买记录的ProductID不能为空');
  230. }
  231. $payInfo = $payModel->field('id,status,amount,real_amount,userid,gameid,channel_id')->where(['orderid'=>$orderid,'userid'=>$this->input('userid')])->find();
  232. //消费订单信息
  233. if(!$payInfo)
  234. {
  235. $this->jsonResult('', 0, '订单记录不存在');
  236. }
  237. elseif($payInfo['status']!=0){
  238. $this->jsonResult('', 0, '该订单记录交易状态已结束');
  239. }
  240. //log_message('apple base64_decode receipt-data: '.base64_decode($receipt_data),'error',LOG_PATH . '../paylog/');
  241. //苹果返回的记录
  242. $response = $this->_getReceiptData($receipt_data,$transaction_id,$product_id);
  243. // 判断购买是否成功
  244. if ( ! isset($response['status']) || $response['status'] != 0 ) {
  245. $this->jsonResult('', 0, '支付失败,Status Code:'.$response['status']);
  246. }
  247. // 判断购买数量
  248. if (! isset($response['quantity']) || 0 > $response['quantity'] ) {
  249. $this->jsonResult('', 0, '购买的数量不能小于1');
  250. }
  251. // 判断产品ID 是否在白名单里
  252. if (! isset($this->_apple_product_ids[$response['product_id']]) ) {
  253. $this->jsonResult('', 0, '非法的ProductID');
  254. }
  255. // 判断产品ID的金额是否一致
  256. if ($this->_apple_product_ids[$response['product_id']]!=$payInfo['amount'] ) {
  257. $this->jsonResult('', 0, '订单金额不匹配');
  258. }
  259. // 判断服务端产品ID和客户端的产品ID是否一致
  260. elseif($response['product_id']!=$product_id)
  261. {
  262. $this->jsonResult('', 0, '苹果购买记录的ProductID不匹配');
  263. }
  264. // 启动事务
  265. Db::startTrans();
  266. try{
  267. $payModel->where(['id'=>$payInfo['id']])->update(['status'=>1,'pay_time'=>NOW_TIMESTAMP]);
  268. //cy_paycpinfo表记录状态更新
  269. $payCpinfoModel->where(['orderid'=>$orderid])->update(['payflag'=>1]);
  270. $result = true;
  271. // 提交事务
  272. Db::commit();
  273. } catch (\Exception $e) {
  274. // 回滚事务
  275. Db::rollback();
  276. $result = false;
  277. //写入日志
  278. log_message('订单编号:'.$orderid.'订单状态更新失败'.$e->getMessage(),'error',LOG_PATH . '../paylog/');
  279. }
  280. if($result==false){
  281. $this->jsonResult('', 0, '订单状态更新失败');
  282. }
  283. // 通知CP,支付成功
  284. (new PayCallback())->callBackToCp($orderid);
  285. $this->jsonResult('', 1, '订单支付成功',false);
  286. //先响应客户端请求,返回后,下面代码继续执行
  287. fastcgi_finish_request();
  288. //更新member表的total_pay_amount
  289. $membersModel->where(['id'=>$payInfo['userid']])->update(['total_pay_amount'=>Db::raw('total_pay_amount+'.$payInfo['amount'])]);
  290. //预警邮件
  291. (new PayLogic())->payWarning($payInfo['userid'], $payInfo['channel_id'], $payInfo['gameid'], $payInfo['amount']);
  292. }
  293. /**
  294. * 发起苹果验证请求
  295. *
  296. * @param string $receipt 苹果回调数据
  297. * @param $transaction_id 苹果购买记录的唯一标识
  298. * @param $product_id 苹果的ProductID
  299. * @param bool $isSandbox 是否沙盒模式
  300. *
  301. * @return array
  302. */
  303. private function _getReceiptData($receipt,$transaction_id,$product_id, $isSandbox = false) {
  304. if ( $isSandbox ) {
  305. $endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
  306. } else {
  307. $endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
  308. }
  309. $postData = json_encode(['receipt-data' => $receipt]);
  310. $curl = curl_init($endpoint);
  311. curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  312. curl_setopt($curl, CURLOPT_POST, true);
  313. curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
  314. curl_setopt($curl, CURLOPT_TIMEOUT, 5); // 设置超时限制防止死循环
  315. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); //这两行一定要加,不加会报SSL 错误
  316. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
  317. $response = curl_exec($curl);
  318. $errno = curl_errno($curl);
  319. $errmsg = curl_error($curl);
  320. curl_close($curl);
  321. // 判断时候出错,抛出异常
  322. if ( $errno != 0 ) {
  323. $this->jsonResult('', 0, 'curl 请求错误 '.$errno);
  324. }
  325. //写入日志
  326. log_message('apple response: '.$response,'error',LOG_PATH . '../paylog/');
  327. $data = json_decode($response, true);
  328. // 判断返回的数据是否数组
  329. if ( ! is_array($data) ) {
  330. $this->jsonResult('', 0, 'Invalid response data');
  331. }
  332. //支付验证成功时
  333. if($data['status']==0){
  334. $in_app = [];
  335. if(!isset($data['receipt']['in_app']) || count($data['receipt']['in_app'])==0){
  336. $this->jsonResult('', 0, 'response in_app is An empty array');
  337. }
  338. else{
  339. foreach($data['receipt']['in_app'] as $value)
  340. {
  341. if($value['transaction_id']==$transaction_id)
  342. {
  343. $in_app = $value;
  344. break;
  345. }
  346. }
  347. }
  348. if(empty($in_app)){
  349. $this->jsonResult('', 0, '没有匹配的交易记录');
  350. }
  351. return [
  352. 'status' => $data['status'],
  353. 'quantity' => $in_app['quantity'],
  354. 'product_id' => $in_app['product_id'],
  355. 'transaction_id' => $in_app['transaction_id'],
  356. 'purchase_date' => $in_app['purchase_date'],
  357. ];
  358. }
  359. // 如果是沙盒数据 则验证沙盒模式
  360. elseif($data['status']=='21007'){
  361. // 请求验证
  362. return $this->_getReceiptData($receipt,$transaction_id,$product_id,true);
  363. }
  364. else{
  365. $this->jsonResult('', 0, 'apple response status:'.$data['status']);
  366. }
  367. }
  368. }