AddonsCourseOrders.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <?php
  2. namespace addons\Course\common\models;
  3. use addons\Course\common\enums\CourseEnum;
  4. use addons\Course\common\events\order\CourseOrderPaidEvent;
  5. use addons\JoinPay\common\services\PaymentService;
  6. use common\enums\PaymentTradeOrderEnum;
  7. use common\enums\StatusEnum;
  8. use common\models\mall\MallAddons;
  9. use common\models\user\User;
  10. use Yii;
  11. use common\enums\PayTypeEnum;
  12. use common\logic\wechat\WechatMiniProgramLogic;
  13. use common\models\common\PaymentTrade;
  14. use common\models\common\PaymentTradeOrder;
  15. use common\models\order\Order;
  16. use common\models\user\UserInfo;
  17. use Exception;
  18. /**
  19. * This is the model class for table "{{%addons_course_orders}}".
  20. *
  21. * @property int $id
  22. * @property int $mall_id 商城id
  23. * @property int $user_id
  24. * @property int $course_id 课程id
  25. * @property string $order_no 订单编号
  26. * @property float $price 价格
  27. * @property float $pay_money 实际付款金额
  28. * @property int $pay_type
  29. * @property int $use_discount 是否使用折扣,1:是,0:否
  30. * @property int $discount_type 使用的折扣类型:0:未使用,1:积分,2:红包,3:活动打折,4:会员等级优惠,5:优惠劵,跟 use_discount 字段相关联,只有当 use_discount = 1 时,此字段的值有意义
  31. * @property float $discount_quota 折扣金额,跟 use_discount 字段相关联,只有当 use_discount = 1 时,此字段的值有意义
  32. * @property int $order_status 订单状态,1:未支付,2:已取消,3:已支付,4:待退款(会员已发起退款申请但平台未处理),5:退款完成,6:订单完成(从订单支付完成的时间点起,已过15天)
  33. * @property int $pay_time 支付时间
  34. * @property int $cancel_time 订单取消的时间,下单后未支付,经过一定的时间系统自动取消
  35. * @property string $remark 备注
  36. * @property int $status 状态 0禁用,1启用,-1是删除
  37. * @property int $created_at
  38. * @property int $updated_at
  39. */
  40. class AddonsCourseOrders extends \common\models\base\BaseActiveRecord
  41. {
  42. /**
  43. * {@inheritdoc}
  44. */
  45. public static function tableName()
  46. {
  47. return '{{%addons_course_orders}}';
  48. }
  49. /**
  50. * {@inheritdoc}
  51. */
  52. public function rules()
  53. {
  54. return [
  55. [['mall_id', 'user_id', 'course_id', 'pay_type', 'use_discount', 'discount_type', 'order_status', 'pay_time', 'cancel_time', 'status', 'created_at', 'updated_at', 'refund_at', 'refund_status'], 'integer'],
  56. [['course_id'], 'required'],
  57. [['price', 'pay_money', 'discount_quota'], 'number'],
  58. [['order_no'], 'string', 'max' => 50],
  59. [['remark', 'refund_desc'], 'string', 'max' => 255],
  60. ];
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function attributeLabels()
  66. {
  67. return [
  68. 'id' => 'ID',
  69. 'mall_id' => '商城id',
  70. 'user_id' => 'User ID',
  71. 'course_id' => '课程id',
  72. 'order_no' => '订单编号',
  73. 'price' => '价格',
  74. 'pay_money' => '实际付款金额',
  75. 'pay_type' => 'Pay Type',
  76. 'use_discount' => '是否使用折扣,1:是,0:否',
  77. 'discount_type' => '使用的折扣类型:0:未使用,1:积分,2:红包,3:活动打折,4:会员等级优惠,5:优惠劵,跟 use_discount 字段相关联,只有当 use_discount = 1 时,此字段的值有意义',
  78. 'discount_quota' => '折扣金额,跟 use_discount 字段相关联,只有当 use_discount = 1 时,此字段的值有意义',
  79. 'order_status' => '订单状态,1:未支付,2:已取消,3:已支付,4:待退款(会员已发起退款申请但平台未处理),5:退款完成,6:订单完成(从订单支付完成的时间点起,已过15天)',
  80. 'pay_time' => '支付时间',
  81. 'cancel_time' => '订单取消的时间,下单后未支付,经过一定的时间系统自动取消',
  82. 'remark' => '备注',
  83. 'status' => '状态 0禁用,1启用,-1是删除',
  84. 'created_at' => 'Created At',
  85. 'updated_at' => 'Updated At',
  86. ];
  87. }
  88. public function getUser(){
  89. return $this->hasOne(User::class, ['id' => 'user_id']);
  90. }
  91. public function getCourse(){
  92. return $this->hasOne(AddonsCourse::class, ['id' => 'course_id']);
  93. }
  94. public static function setData(array $data)
  95. {
  96. $model = new self();
  97. $model->load($data, '');
  98. $model->status = StatusEnum::ENABLED;
  99. if (!$model->save()) {
  100. self::$static_error = '保存数据失败:' . $model->getErrorMessage();
  101. return false;
  102. }
  103. return $model;
  104. }
  105. /**
  106. * 新增用户购买的课程
  107. *
  108. * @return boolean
  109. * @throws \Exception
  110. */
  111. public function addBought($id, $user_id)
  112. {
  113. $bought = AddonsCourseBought::findOne(["order_id" => $id, "buy_type" => 1, "user_id" => $user_id]);
  114. if (empty($bought)) {
  115. $bought = new AddonsCourseBought();
  116. $bought->mall_id = $this->mall_id;
  117. $bought->user_id = $this->user_id;
  118. $bought->course_id = $this->course_id;
  119. $bought->amount = $this->real_pay;
  120. $bought->buy_type = 1;
  121. $bought->order_id = $this->id;
  122. $bought->status = 2;
  123. if (!$bought->save()) throw new \Exception("保存课程失败");
  124. }
  125. return true;
  126. }
  127. public static function generateOrderNo($prefix = '')
  128. {
  129. $randLen = 6;
  130. $id = base_convert(substr(uniqid(), 0 - $randLen), 16, 10);
  131. if (strlen($id) > 10) {
  132. $id = substr($id, -10);
  133. } elseif (strlen($id) < 10) {
  134. $rLen = 10 - strlen($id);
  135. $id = $id . rand(pow(10, $rLen - 1), pow(10, $rLen) - 1);
  136. }
  137. $dateTimeStr = date('YmdHis');
  138. return $prefix . $dateTimeStr . $id;
  139. }
  140. /**
  141. * @throws \Exception
  142. */
  143. public static function preview($user, $mall_id, $post)
  144. {
  145. return self::handleData($user, $mall_id, $post, 1);
  146. }
  147. public static function createOrder($user, $mall_id, $params)
  148. {
  149. $t = \Yii::$app->db->beginTransaction();
  150. try {
  151. if (Order::isStopOrder($mall_id)) throw new Exception('站点维护中,暂时无法下单');
  152. $data = self::handleData($user, $mall_id, $params, 2);
  153. //生成订单
  154. $orderno = AddonsCourseOrders::generateOrderNo('C');
  155. $oldOrder = AddonsCourseOrders::getOne([['order_no' => $orderno]]);
  156. if ($oldOrder) {
  157. self::$static_error = '重复下单';
  158. return false;
  159. }
  160. $use_discount = 0;
  161. $discount_type = 0;
  162. foreach ($data['list'] as $orderItem) {
  163. if (!empty($orderItem['use_discount'])) {
  164. $use_discount = 1;
  165. $discount_type = 4;
  166. }
  167. }
  168. $params = [
  169. 'mall_id' => $mall_id,
  170. 'user_id' => $user['id'],
  171. 'course_id' => $data['list'][0]['id'],
  172. 'order_no' => $orderno,
  173. 'price' => $data['total_price'],
  174. 'pay_money' => $data['total_price'],
  175. 'remark' => isset($post["remark"]) ? $post["remark"] : "",
  176. 'pay_type' => CourseEnum::PAY_TYPE_NO_PAY,
  177. 'order_status' => CourseEnum::STATUS_WAIT_PAY,
  178. 'use_discount' => $use_discount,
  179. 'discount_type' => $discount_type,
  180. 'discount_quota' => !empty($data['discount_quota']) ? $data['discount_quota'] : 0.00,
  181. ];
  182. $state = AddonsCourseOrders::setData($params);
  183. if (!$state) {
  184. throw new \Exception("订单保存失败");
  185. }
  186. AddonsCourseBought::addBought($params, $state->id);
  187. $order_trade_info[] = [
  188. 'order_no' => $orderno,
  189. 'amount' => $data['total_price'],
  190. 'title' => '购买课程' . mb_strlen($data['list'][0]['name']) > 32 ? mb_substr($data['list'][0]['name'], 0, 32) : $data['list'][0]['name'],
  191. 'notify_class' => CourseEnum::NOTIFY_CLASS,
  192. 'order_type' => PaymentTradeOrderEnum::COURSE_ORDER,
  193. ];
  194. $t->commit();
  195. //生成支付流水
  196. $res = \Yii::$app->services->pay->createTrade($params['mall_id'], $params['user_id'], $order_trade_info);
  197. if ($res == false) throw new \Exception('生成支付流水失败');
  198. $arr['transaction'] = $res;
  199. $arr['orderno'] = $orderno;
  200. $arr['pay_success_page'] = Yii::$app->services->setting->mall->get($params['mall_id'], 'order.pay_after_link');// 支付成功后跳转的地址
  201. return $arr;
  202. } catch (\Exception $exception) {
  203. $t->rollBack();
  204. self::$static_error = $exception->getMessage();
  205. return false;
  206. }
  207. }
  208. /**
  209. * 支付
  210. * User: wniu
  211. * Date: 2022/3/18
  212. * Time: 3:49 下午
  213. * *
  214. * @param $params
  215. * @param $pay_type
  216. * @return AddonsCourseOrders|false
  217. */
  218. public static function pay($params, $pay_type)
  219. {
  220. $trans = Yii::$app->db->beginTransaction();
  221. try {
  222. $model = self::findOne(['id' => $params['id'], 'status' => [StatusEnum::ENABLED]]);
  223. if (empty($model)) throw new \Exception('订单不存在或已删除');
  224. if ($model->order_status != CourseEnum::STATUS_WAIT_PAY) throw new \Exception('订单已经被处理,无法进行支付');
  225. $model->order_status = CourseEnum::IS_PAY_YES;
  226. $model->pay_type = $pay_type;
  227. $model->pay_time = time();
  228. $res = $model->save();
  229. $course = AddonsCourse::getOne([['id' => $params['course_id']]]);
  230. if ($course) {
  231. $course->sales = $course->sales + 1;
  232. $course->save();
  233. }
  234. $bought = AddonsCourseBought::getOne([['order_id' => $params['id']]]);
  235. if ($bought) {
  236. $bought->is_pay = CourseEnum::IS_PAY_YES;
  237. $bought->save();
  238. }
  239. if ($res === false) throw new \Exception('修改订单失败:' . $model->getErrorMessage());
  240. $trans->commit();
  241. // 触发订单支付事件,事务提交完成后触发
  242. $event = new CourseOrderPaidEvent($model['mall_id']);
  243. $order['id'] = $model['id'];
  244. $order['operator_id'] = $params['operator_id'] ?? '';
  245. $order['operator_name'] = $params['operator_name'] ?? '';
  246. Yii::$app->services->events->dispatch($event, $order, $event->listeners);
  247. return $model;
  248. } catch (\Exception $e) {
  249. if (isset($trans)) $trans->rollback();
  250. self::$static_error = $e->getMessage();
  251. return false;
  252. }
  253. }
  254. /**
  255. * @param int $type 1 预览订单 2:提交订单
  256. * * @param $form_data
  257. * @param int $type
  258. * @throws \Exception
  259. */
  260. protected static function handleData($user, $mall_id, $form_data, $type = 1)
  261. {
  262. $listData = self::getCourseListData($form_data["list"], $user);
  263. $total_price = 0.00;
  264. $total_original_price = 0.00;
  265. foreach ($listData as &$priceItem) {
  266. $total_price += $priceItem['price'];
  267. $total_original_price += $priceItem['original_price'];
  268. }
  269. $discount_quota = 0.00;
  270. if ($total_original_price > $total_price) {
  271. $discount_quota = bcsub($total_original_price, $total_price, 2);
  272. }
  273. return [
  274. 'list' => $listData,
  275. 'total_price' => self::price_format($total_price),
  276. 'total_original_price' => self::price_format($total_original_price),
  277. 'discount_quota' => self::price_format($discount_quota),
  278. ];
  279. }
  280. /**
  281. * 课程列表数据
  282. * @param $courseList
  283. * @return array
  284. * @throws \Exception
  285. */
  286. protected static function getCourseListData($courseList, $user)
  287. {
  288. $list = [];
  289. if (!empty($courseList)) {
  290. foreach ($courseList as $i => $item) {
  291. $list[] = self::getOneCourseItemData($item, $user);
  292. }
  293. }
  294. return $list;
  295. }
  296. /**
  297. * 单个课程数据
  298. * @return array
  299. * @throws \Exception
  300. */
  301. protected static function getOneCourseItemData($item, $user)
  302. {
  303. $course = AddonsCourse::getOne([['id' => $item['id'], 'status' => StatusEnum::ENABLED]]);
  304. if (!$course) {
  305. throw new \Exception('课程不存在或已下架。');
  306. }
  307. //是否使用了折扣
  308. $use_discount = false;
  309. //会员价
  310. $price = $course->getMemberPrice($user, $use_discount);
  311. $itemData = [
  312. 'id' => $course->id,
  313. 'name' => $course->name,
  314. 'num' => $item['num'],
  315. 'price' => self::price_format($price * $item['num']),
  316. 'logo' => $course->logo,
  317. 'original_price' => self::price_format($course->price * $item['num']),
  318. 'use_discount' => $use_discount,
  319. 'line_off_price' => $course->line_off_price,
  320. ];
  321. return $itemData;
  322. }
  323. static function price_format($val, $decimals = 2)
  324. {
  325. $val = floatval($val);
  326. $result = number_format($val, $decimals, '.', '');
  327. return $result;
  328. }
  329. /**
  330. * 上传物流信息(微信小程序要求)
  331. * @param $order
  332. * @return true|void
  333. *
  334. */
  335. public static function uploadShippingInfo($order)
  336. {
  337. $order_no = $order['order_no'];
  338. $mall_id = $order['mall_id'];
  339. $course = $order['course'];
  340. try {
  341. $payment_trade_order_model = PaymentTradeOrder::findOne(['order_no'=>$order_no,'is_pay'=>1]);
  342. if(empty($payment_trade_order_model)) throw new \Exception("order_no:".$order_no.",payment_trade_order_model不存在");
  343. $pay_type = [PayTypeEnum::WECHAT, PayTypeEnum::JOINPAY, PayTypeEnum::HUIFUPAY, PayTypeEnum::UNIONPAY];
  344. $trade_type = ['JSAPI', 'WEIXIN_XCX', 'T_MINIAPP', 'W06-CASHIER', 'W06'];
  345. $payment_trade_model = PaymentTrade::findOne(['id'=>$payment_trade_order_model['trade_id'],'pay_type'=>$pay_type,'trade_type'=>$trade_type]);
  346. if(empty($payment_trade_model)) throw new \Exception("trade_id:".$payment_trade_order_model['trade_id'].",payment_trade_model不存在");
  347. if($payment_trade_model['is_pay_group']){
  348. //组合支付,查询对应的子流水
  349. $payment_trade_model = PaymentTrade::findOne(['parent_trade_id'=>$payment_trade_order_model['trade_id'],'pay_type'=>$pay_type,'trade_type'=>$trade_type]);
  350. }else{
  351. $payment_trade_model = PaymentTrade::findOne(['id'=>$payment_trade_order_model['trade_id'],'pay_type'=>$pay_type,'trade_type'=>$trade_type]);
  352. }
  353. $third_trade_no = '';
  354. switch ($payment_trade_model['pay_type']){
  355. case PayTypeEnum::WECHAT:
  356. //微信支付
  357. $third_trade_no = $payment_trade_model['third_trade_no'];
  358. break;
  359. case PayTypeEnum::JOINPAY:
  360. if(!MallAddons::isInstall($mall_id,'JoinPay')){
  361. return true;
  362. }
  363. $third_trade_no = $payment_trade_model['second_trade_no'];
  364. break;
  365. case PayTypeEnum::HUIFUPAY:
  366. if(!MallAddons::isInstall($mall_id,'HuifuPay')){
  367. return true;
  368. }
  369. $third_trade_no = $payment_trade_model['second_trade_no'];
  370. break;
  371. }
  372. if(empty($third_trade_no)){
  373. return true;
  374. }
  375. $params['mall_id'] = $mall_id;
  376. $params['item_desc'] = $course['name']??'';
  377. $params['user_id'] = $payment_trade_model['user_id'];
  378. $params['third_trade_no'] = $third_trade_no;
  379. $params['delivery_mode'] = 1;
  380. $params['is_all_delivered'] = false;
  381. WechatMiniProgramLogic::uploadShippingInfoWechatByVirtually($params);
  382. }catch (\Exception $e){
  383. Yii::error('uploadShippingInfo:' . $e->getMessage());
  384. }
  385. }
  386. }