Payment.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <?php
  2. namespace app\api\controller;
  3. use think\Db;
  4. use think\Request;
  5. /**
  6. * 支付/充值 API
  7. *
  8. * 提供支付配置查询、发起支付、支付回调通知、卡密充值、
  9. * 积分购买内容权限、会员升级等功能。
  10. */
  11. class Payment extends Base
  12. {
  13. use PublicApi;
  14. public function __construct()
  15. {
  16. parent::__construct();
  17. // notify 回调不做 API 开关检查,其他方法需要
  18. $ac = request()->action();
  19. if ($ac !== 'notify') {
  20. $this->check_config();
  21. }
  22. }
  23. /**
  24. * 支付通道是否已在后台填齐必填项(与 extend/pay 内实际使用字段一致)
  25. *
  26. * @param string $key 小写,如 alipay、weixin
  27. * @param array $cfg pay 下单项配置
  28. */
  29. public static function isPayChannelReady($key, array $cfg)
  30. {
  31. $key = strtolower((string) $key);
  32. $t = function ($v) {
  33. return trim((string) ($v ?? ''));
  34. };
  35. switch ($key) {
  36. case 'alipay':
  37. return $t($cfg['appid'] ?? '') !== '' && $t($cfg['account'] ?? '') !== '';
  38. case 'weixin':
  39. return $t($cfg['appid'] ?? '') !== '' && $t($cfg['mchid'] ?? '') !== '' && $t($cfg['appkey'] ?? '') !== '';
  40. case 'epay':
  41. return $t($cfg['api_url'] ?? '') !== '' && $t($cfg['appid'] ?? '') !== '' && $t($cfg['appkey'] ?? '') !== '';
  42. case 'codepay':
  43. case 'zhapay':
  44. return $t($cfg['appid'] ?? '') !== '' && $t($cfg['appkey'] ?? '') !== '';
  45. default:
  46. return $t($cfg['appid'] ?? '') !== '';
  47. }
  48. }
  49. /**
  50. * 与后台支付设置(extend/pay 扩展 Tab + maccms.pay)一致:不硬编码通道、不与扩展重复
  51. *
  52. * @return array<int, array{key:string,name:string,enabled:int,paytypes?:array}>
  53. */
  54. public static function payMethodsForConfig(array $payConfig)
  55. {
  56. $extends = mac_extends_list('pay');
  57. $split = function ($s) {
  58. $p = preg_split('/\s*,\s*/', (string) $s, -1, PREG_SPLIT_NO_EMPTY);
  59. if (!is_array($p)) {
  60. return [];
  61. }
  62. return array_values(array_unique(array_map('trim', $p)));
  63. };
  64. $methods = [];
  65. foreach ($extends['ext_list'] ?? [] as $classKey => $displayName) {
  66. $key = strtolower((string) $classKey);
  67. $cfg = (isset($payConfig[$key]) && is_array($payConfig[$key])) ? $payConfig[$key] : [];
  68. $enabled = self::isPayChannelReady($key, $cfg) ? 1 : 0;
  69. $row = [
  70. 'key' => $key,
  71. 'name' => is_string($displayName) ? $displayName : $key,
  72. 'enabled' => $enabled,
  73. ];
  74. if ($key === 'codepay') {
  75. $map = [
  76. '1' => ['支付宝', 'Alipay'],
  77. '2' => ['QQ钱包', 'QQ Wallet'],
  78. '3' => ['微信', 'WeChat'],
  79. ];
  80. $pts = [];
  81. foreach ($split($cfg['type'] ?? '') as $v) {
  82. $lab = isset($map[$v]) ? $map[$v] : [$v, $v];
  83. $pts[] = ['value' => $v, 'label' => $lab[0], 'label_en' => $lab[1]];
  84. }
  85. $row['paytypes'] = $pts;
  86. } elseif ($key === 'zhapay') {
  87. $map = [
  88. '1' => ['微信', 'WeChat'],
  89. '2' => ['支付宝', 'Alipay'],
  90. ];
  91. $pts = [];
  92. foreach ($split($cfg['type'] ?? '') as $v) {
  93. $lab = isset($map[$v]) ? $map[$v] : [$v, $v];
  94. $pts[] = ['value' => $v, 'label' => $lab[0], 'label_en' => $lab[1]];
  95. }
  96. $row['paytypes'] = $pts;
  97. }
  98. $methods[] = $row;
  99. }
  100. usort($methods, function ($a, $b) {
  101. return strcmp($a['key'], $b['key']);
  102. });
  103. return $methods;
  104. }
  105. /**
  106. * 辅助:检查登录
  107. */
  108. private function _checkLogin()
  109. {
  110. $check = model('User')->checkLogin();
  111. if ($check['code'] > 1) {
  112. return ['ok' => false, 'user_id' => 0, 'user' => null,
  113. 'response' => json(['code' => 1401, 'msg' => lang('api/please_login_first')])];
  114. }
  115. $uid = intval($check['info']['user_id']);
  116. $user = Db::name('User')->where('user_id', $uid)->find();
  117. if (!$user) {
  118. return ['ok' => false, 'user_id' => 0, 'user' => null,
  119. 'response' => json(['code' => 1002, 'msg' => lang('api/user_not_found')])];
  120. }
  121. return ['ok' => true, 'user_id' => $uid, 'user' => $user, 'response' => null];
  122. }
  123. /**
  124. * 获取支付配置(可用支付方式列表)
  125. * GET /api.php/payment/get_config
  126. *
  127. * @return JSON {code:1, msg:'获取成功', info:{min, scale, methods, card_config, is_login, user_points}}
  128. */
  129. public function get_config(Request $request)
  130. {
  131. $pay_config = config('maccms.pay');
  132. $loginCheck = model('User')->checkLogin();
  133. $isLogin = (intval($loginCheck['code']) === 1) && intval($loginCheck['info']['user_id'] ?? 0) > 0;
  134. $userPoints = $isLogin ? intval($loginCheck['info']['user_points'] ?? 0) : 0;
  135. $methods = self::payMethodsForConfig($pay_config);
  136. // 卡密充值
  137. $card_config = [
  138. 'enabled' => !empty($pay_config['card']['url']) ? 1 : 0,
  139. 'card_url' => $pay_config['card']['url'] ?? '',
  140. ];
  141. return json([
  142. 'code' => 1,
  143. 'msg' => lang('obtain_ok'),
  144. 'info' => [
  145. 'min' => floatval($pay_config['min'] ?? 1),
  146. 'scale' => intval($pay_config['scale'] ?? 1),
  147. 'methods' => $methods,
  148. 'card_config' => $card_config,
  149. 'is_login' => $isLogin ? 1 : 0,
  150. 'user_points' => $userPoints,
  151. ],
  152. ]);
  153. }
  154. /**
  155. * 发起支付(跳转第三方支付)
  156. * POST /api.php/payment/gopay
  157. *
  158. * @param order_code string 必填,订单号
  159. * @param order_id int 必填,订单ID
  160. * @param payment string 必填,支付方式(alipay/weixin/codepay/epay/zhapay 等)
  161. * @return JSON {code:1, msg:'...', info:{payment, payment_data:{...}}}
  162. *
  163. * 说明:
  164. * - 微信支付返回 code_url(用于生成二维码)
  165. * - 支付宝等返回 pay_url(构造好的跳转链接)或 html(表单HTML)
  166. * - 前端根据 payment 类型决定展示方式
  167. */
  168. public function gopay(Request $request)
  169. {
  170. $auth = $this->_checkLogin();
  171. if (!$auth['ok']) return $auth['response'];
  172. $param = $request->param();
  173. $validate = validate($request->controller());
  174. if (!$validate->scene($request->action())->check($param)) {
  175. return json(['code' => 1001, 'msg' => lang('api/param_validate', [$validate->getError()])]);
  176. }
  177. $order_code = htmlspecialchars(urldecode(trim($param['order_code'] ?? '')));
  178. $order_id = intval($param['order_id'] ?? 0);
  179. $payment = strtolower(htmlspecialchars(urldecode(trim($param['payment'] ?? ''))));
  180. if (empty($order_code) || empty($order_id) || empty($payment)) {
  181. return json(['code' => 1001, 'msg' => lang('api/param_order_fields')]);
  182. }
  183. $pay_config = config('maccms.pay');
  184. if (empty($pay_config[$payment]['appid'])) {
  185. return json(['code' => 1002, 'msg' => lang('api/payment/payment_disabled')]);
  186. }
  187. // 核实订单
  188. $where = [];
  189. $where['order_id'] = $order_id;
  190. $where['order_code'] = $order_code;
  191. $where['user_id'] = $auth['user_id'];
  192. $res = model('Order')->infoData($where);
  193. if ($res['code'] > 1) {
  194. return json(['code' => 1003, 'msg' => lang('api/payment/order_not_found')]);
  195. }
  196. if ($res['info']['order_status'] == 1) {
  197. return json(['code' => 1004, 'msg' => lang('api/payment/order_paid')]);
  198. }
  199. $order_info = $res['info'];
  200. // 调用支付扩展
  201. $cp = 'app\\common\\extend\\pay\\' . ucfirst($payment);
  202. if (!class_exists($cp)) {
  203. return json(['code' => 1005, 'msg' => lang('api/payment/payment_missing', [$payment])]);
  204. }
  205. // API 模式:传入 return_only=true,让支付扩展返回 HTML 而非 echo+die
  206. $c = new $cp;
  207. $payment_res = $c->submit($auth['user'], $order_info, $param, true);
  208. // 根据不同支付方式构建返回
  209. $payment_data = [];
  210. if ($payment === 'weixin' && is_array($payment_res) && !empty($payment_res['code_url'])) {
  211. // 微信支付:返回二维码 URL
  212. $payment_data = [
  213. 'type' => 'qrcode',
  214. 'code_url' => $payment_res['code_url'],
  215. 'total_fee' => $payment_res['total_fee'] ?? $order_info['order_price'],
  216. 'out_trade_no' => $payment_res['out_trade_no'] ?? $order_code,
  217. ];
  218. } elseif ($payment === 'weixin' && $payment_res === false) {
  219. return json(['code' => 1006, 'msg' => lang('api/payment/weixin_qr_fail')]);
  220. } elseif (is_string($payment_res) && !empty($payment_res)) {
  221. // 支付宝返回 HTML 表单 / 其他支付返回 JS 跳转脚本
  222. $payment_data = [
  223. 'type' => 'html',
  224. 'html' => $payment_res,
  225. ];
  226. } elseif (is_array($payment_res)) {
  227. // 其他返回数组的支付方式
  228. $payment_data = [
  229. 'type' => 'data',
  230. 'data' => $payment_res,
  231. ];
  232. } else {
  233. $payment_data = [
  234. 'type' => 'unknown',
  235. 'data' => $payment_res,
  236. ];
  237. }
  238. return json([
  239. 'code' => 1,
  240. 'msg' => lang('api/payment/pay_started_ok'),
  241. 'info' => [
  242. 'payment' => $payment,
  243. 'order_code' => $order_code,
  244. 'order_price' => $order_info['order_price'],
  245. 'payment_data' => $payment_data,
  246. ],
  247. ]);
  248. }
  249. /**
  250. * 支付回调通知(第三方支付服务器调用)
  251. * GET/POST /api.php/payment/notify?pay_type=alipay
  252. *
  253. * 说明:此接口由第三方支付平台异步调用,不需要用户登录。
  254. * 可将回调地址配置为 /api.php/payment/notify/pay_type/{type}
  255. * 或 /api.php/payment/notify?pay_type={type}
  256. */
  257. public function notify()
  258. {
  259. $param = input();
  260. $pay_type = $param['pay_type'] ?? '';
  261. if (empty($pay_type)) {
  262. echo 'pay_type is required';
  263. exit;
  264. }
  265. $pay_config = config('maccms.pay');
  266. if (empty($pay_config[$pay_type]['appid'])) {
  267. echo lang('index/payment_status');
  268. exit;
  269. }
  270. $cp = 'app\\common\\extend\\pay\\' . ucfirst($pay_type);
  271. if (class_exists($cp)) {
  272. $c = new $cp;
  273. $c->notify();
  274. } else {
  275. echo lang('index/payment_not');
  276. exit;
  277. }
  278. }
  279. /**
  280. * 卡密充值
  281. * POST /api.php/payment/use_card
  282. *
  283. * @param card_no string 必填,充值卡卡号
  284. * @param card_pwd string 必填,充值卡密码
  285. * @return JSON {code:1, msg:'充值成功,增加积分【xxx】'}
  286. */
  287. public function use_card(Request $request)
  288. {
  289. $auth = $this->_checkLogin();
  290. if (!$auth['ok']) return $auth['response'];
  291. $param = $request->param();
  292. $validate = validate($request->controller());
  293. if (!$validate->scene($request->action())->check($param)) {
  294. return json(['code' => 1001, 'msg' => lang('api/param_validate', [$validate->getError()])]);
  295. }
  296. $card_no = htmlspecialchars(urldecode(trim($param['card_no'] ?? '')));
  297. $card_pwd = htmlspecialchars(urldecode(trim($param['card_pwd'] ?? '')));
  298. $res = model('Card')->useData($card_no, $card_pwd, $auth['user']);
  299. return json($res);
  300. }
  301. /**
  302. * 积分购买内容权限(观看/下载/阅读付费内容)
  303. * POST /api.php/payment/buy_popedom
  304. *
  305. * @param mid int 必填,模型(1=视频, 2=文章)
  306. * @param id int 必填,资源ID(vod_id 或 art_id)
  307. * @param type int 必填,操作类型(1=文章阅读, 4=播放, 5=下载)
  308. * @param sid int 可选,播放源编号
  309. * @param nid int 可选,集编号
  310. * @return JSON {code:1, msg:'购买成功'}
  311. */
  312. public function buy_popedom(Request $request)
  313. {
  314. $auth = $this->_checkLogin();
  315. if (!$auth['ok']) return $auth['response'];
  316. $param = $request->param();
  317. $validate = validate($request->controller());
  318. if (!$validate->scene($request->action())->check($param)) {
  319. return json(['code' => 1001, 'msg' => lang('api/param_validate', [$validate->getError()])]);
  320. }
  321. $data = [];
  322. $data['ulog_mid'] = intval($param['mid'] ?? 1) <= 0 ? 1 : intval($param['mid']);
  323. $data['ulog_rid'] = intval($param['id'] ?? 0);
  324. $data['ulog_sid'] = intval($param['sid'] ?? 0);
  325. $data['ulog_nid'] = intval($param['nid'] ?? 0);
  326. $data['ulog_type'] = intval($param['type']);
  327. $data['user_id'] = $auth['user_id'];
  328. // 查询资源信息以获取所需积分
  329. if ($param['type'] == '1') {
  330. // 文章
  331. $where = ['art_id' => $data['ulog_rid']];
  332. $res = model('Art')->infoData($where);
  333. if ($res['code'] > 1) {
  334. return json($res);
  335. }
  336. $col = 'art_points_detail';
  337. if ($GLOBALS['config']['user']['art_points_type'] == '1') {
  338. $col = 'art_points';
  339. $data['ulog_sid'] = 0;
  340. $data['ulog_nid'] = 0;
  341. }
  342. } else {
  343. // 视频
  344. $where = ['vod_id' => $data['ulog_rid']];
  345. $res = model('Vod')->infoData($where);
  346. if ($res['code'] > 1) {
  347. return json($res);
  348. }
  349. $col = 'vod_points_' . ($param['type'] == '4' ? 'play' : 'down');
  350. if ($GLOBALS['config']['user']['vod_points_type'] == '1') {
  351. $col = 'vod_points';
  352. $data['ulog_sid'] = 0;
  353. $data['ulog_nid'] = 0;
  354. }
  355. }
  356. $data['ulog_points'] = intval($res['info'][$col]);
  357. // 检查是否已购买
  358. $exists = model('Ulog')->infoData($data);
  359. if ($exists['code'] == 1) {
  360. return json(['code' => 1, 'msg' => lang('api/payment/already_owned')]);
  361. }
  362. // 检查积分是否足够(先做快速检查,事务内再做原子扣除)
  363. if ($data['ulog_points'] > $auth['user']['user_points']) {
  364. return json([
  365. 'code' => 1005,
  366. 'msg' => lang('api/payment/points_need_remain', [(string) $data['ulog_points'], (string) $auth['user']['user_points']]),
  367. 'info' => [
  368. 'need_points' => $data['ulog_points'],
  369. 'current_points' => intval($auth['user']['user_points']),
  370. ],
  371. ]);
  372. }
  373. // 使用事务 + 条件更新防止并发刷积分
  374. Db::startTrans();
  375. try {
  376. // 带条件的原子扣除:只有积分足够时才扣除
  377. $affected = Db::name('user')
  378. ->where('user_id', $auth['user_id'])
  379. ->where('user_points', '>=', $data['ulog_points'])
  380. ->setDec('user_points', $data['ulog_points']);
  381. if ($affected === 0 || $affected === false) {
  382. Db::rollback();
  383. return json(['code' => 1005, 'msg' => lang('api/payment/points_insufficient')]);
  384. }
  385. // 积分日志
  386. $plog = [];
  387. $plog['user_id'] = $auth['user_id'];
  388. $plog['plog_type'] = 8;
  389. $plog['plog_points'] = $data['ulog_points'];
  390. model('Plog')->saveData($plog);
  391. // 分销佣金
  392. model('User')->reward($data['ulog_points']);
  393. // 写入购买记录
  394. $save_res = model('Ulog')->saveData($data);
  395. Db::commit();
  396. return json($save_res);
  397. } catch (\Exception $e) {
  398. Db::rollback();
  399. return json(['code' => 1006, 'msg' => lang('api/payment/operation_retry')]);
  400. }
  401. }
  402. /**
  403. * 会员升级(用积分升级用户组/VIP)
  404. * POST /api.php/payment/upgrade
  405. *
  406. * @param group_id int 必填,目标用户组ID(>=3)
  407. * @param long string 必填,时长周期(day|week|month|year)
  408. * @return JSON {code:1, msg:'升级成功'}
  409. *
  410. * 说明:
  411. * - 积分 = 对应用户组的 group_points_{long} 字段值
  412. * - 用户必须拥有足够积分
  413. * - 升级后 user_end_time 自动延长
  414. */
  415. public function upgrade(Request $request)
  416. {
  417. $auth = $this->_checkLogin();
  418. if (!$auth['ok']) return $auth['response'];
  419. $param = $request->param();
  420. $validate = validate($request->controller());
  421. if (!$validate->scene($request->action())->check($param)) {
  422. return json(['code' => 1001, 'msg' => lang('api/param_validate', [$validate->getError()])]);
  423. }
  424. $res = model('User')->upgrade($param);
  425. return json($res);
  426. }
  427. /**
  428. * 获取可升级的用户组列表(含积分价格)
  429. * GET /api.php/payment/get_groups
  430. *
  431. * @return JSON {code:1, msg:'获取成功', info:[{group_id, group_name, group_points_day, ...}]}
  432. */
  433. public function get_groups(Request $request)
  434. {
  435. $group_list = model('Group')->getCache();
  436. $result = [];
  437. foreach ($group_list as $g) {
  438. // 只返回自定义付费组(group_id >= 3)
  439. if ($g['group_id'] < 3) continue;
  440. if ($g['group_status'] == 0) continue;
  441. $result[] = [
  442. 'group_id' => $g['group_id'],
  443. 'group_name' => $g['group_name'],
  444. 'group_status' => $g['group_status'],
  445. 'group_points_day' => $g['group_points_day'] ?? 0,
  446. 'group_points_week' => $g['group_points_week'] ?? 0,
  447. 'group_points_month' => $g['group_points_month'] ?? 0,
  448. 'group_points_year' => $g['group_points_year'] ?? 0,
  449. ];
  450. }
  451. return json([
  452. 'code' => 1,
  453. 'msg' => lang('obtain_ok'),
  454. 'info' => $result,
  455. ]);
  456. }
  457. /**
  458. * 获取用户充值卡使用记录
  459. * GET /api.php/payment/get_cards?page=1&limit=20
  460. *
  461. * @param page int 可选,页码,默认1
  462. * @param limit int 可选,每页条数,默认20,最大100
  463. * @return JSON {code:1, msg:'获取成功', info:{page, pagecount, limit, total, list:[...]}}
  464. */
  465. public function get_cards(Request $request)
  466. {
  467. $auth = $this->_checkLogin();
  468. if (!$auth['ok']) return $auth['response'];
  469. $param = $request->param();
  470. $validate = validate($request->controller());
  471. if (!$validate->scene($request->action())->check($param)) {
  472. return json(['code' => 1001, 'msg' => lang('api/param_validate', [$validate->getError()])]);
  473. }
  474. $page = max(1, intval($param['page'] ?? 1));
  475. $limit = max(1, min(100, intval($param['limit'] ?? 20)));
  476. $where = [];
  477. $where['user_id'] = $auth['user_id'];
  478. $where['card_use_status'] = 1;
  479. $order = 'card_id desc';
  480. $res = model('Card')->listData($where, $order, $page, $limit);
  481. return json([
  482. 'code' => 1,
  483. 'msg' => lang('obtain_ok'),
  484. 'info' => $res,
  485. ]);
  486. }
  487. }