AddonsCourseChapter.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <?php
  2. namespace addons\Course\common\models;
  3. use addons\Course\common\enums\CourseEnum;
  4. use common\components\export\CsvTool;
  5. use common\enums\RabbitMqEnum;
  6. use common\enums\StatusEnum;
  7. use common\helpers\CurlHelper;
  8. use common\helpers\FfmpegHelper;
  9. use common\models\api\AccessToken;
  10. use common\models\common\Attachment;
  11. use common\models\goods\Goods;
  12. use common\models\mall\Mall;
  13. /**
  14. * This is the model class for table "{{%addons_course_chapter}}".
  15. *
  16. * @property int $id
  17. * @property int $mall_id 商城id
  18. * @property int $course_id 课程id
  19. * @property int $dir_id 课程目录id,如果此字段值为0,说明没有二级目录
  20. * @property string $name 课程章节名称
  21. * @property string $video_url 课程章节视频url地址
  22. * @property int $type 视频类型 默认是0(视频) 0:视频 1:音频
  23. * @property string $goods_list 商品列表,用逗号连接
  24. * @property int $can_see 是否允许试看 默认允许 1:允许 0:不允许
  25. * @property int $views 课程章节实际播放次数
  26. * @property int $sort 序号 按asc升序排列
  27. * @property string $thumb_url 视频首图
  28. * @property int $status 状态 0禁用,1启用,-1是删除
  29. * @property int $created_at
  30. * @property int $updated_at
  31. * @property int $video_length 视频时长
  32. * @property int $can_see_time 试看时长,单位:秒
  33. */
  34. class AddonsCourseChapter extends \common\models\base\BaseActiveRecord
  35. {
  36. /**
  37. * {@inheritdoc}
  38. */
  39. public static function tableName()
  40. {
  41. return '{{%addons_course_chapter}}';
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. public function rules()
  47. {
  48. return [
  49. [['mall_id', 'course_id', 'dir_id', 'type', 'can_see', 'views', 'sort', 'status', 'can_see_time', 'created_at', 'updated_at','select_type'], 'integer'],
  50. [['name', 'thumb_url'], 'string', 'max' => 255],
  51. [['video_url'], 'string', 'max' => 1000],
  52. [['goods_list'], 'string', 'max' => 1000],
  53. ['video_length', 'string', 'max' => 50],
  54. ];
  55. }
  56. /**
  57. * {@inheritdoc}
  58. */
  59. public function attributeLabels()
  60. {
  61. return [
  62. 'id' => 'ID',
  63. 'mall_id' => '商城id',
  64. 'course_id' => '课程id',
  65. 'dir_id' => '课程目录id,如果此字段值为0,说明没有二级目录',
  66. 'name' => '课程章节名称',
  67. 'video_url' => '课程章节视频url地址',
  68. 'type' => '视频类型 默认是1(视频) 1:视频 2:音频',
  69. 'goods_list' => '商品列表,用逗号连接',
  70. 'can_see' => '是否允许试看 默认允许 1:允许 0:不允许',
  71. 'views' => '课程章节实际播放次数',
  72. 'sort' => '序号 按asc升序排列',
  73. 'thumb_url' => '视频首图',
  74. 'status' => '状态 0禁用,1启用,-1是删除',
  75. 'created_at' => 'Created At',
  76. 'updated_at' => 'Updated At',
  77. 'video_length' => '视频时长',
  78. 'can_see_time' => '试看时长,单位:秒',
  79. ];
  80. }
  81. public static function setData(int $mall_id, array $data)
  82. {
  83. if (!empty($data['goods_list']) && is_array($data['goods_list'])) {
  84. $data['goods_list'] = implode(',', $data['goods_list']);
  85. }
  86. if (!empty($data['id'])) {
  87. $model = self::findOne(['mall_id' => $mall_id, 'status' => StatusEnum::ENABLED, 'id' => $data['id']]);
  88. if (empty($model)) {
  89. self::$static_error = '数据异常';
  90. return false;
  91. }
  92. } else {
  93. $model = new self();
  94. $model->loadDefaultValues();
  95. $model->mall_id = $mall_id;
  96. $model->status = StatusEnum::ENABLED;
  97. }
  98. foreach ($data as $key => $val) {
  99. $model->$key = $val;
  100. }
  101. $is_obtain_duration = false;
  102. $upload_file_info = Attachment::find()
  103. ->select('id,url,duration_str,thumb_url')
  104. ->where(['mall_id' => $mall_id, 'upload_type' => Attachment::UPLOAD_TYPE_VIDEOS, 'url' => $model->video_url])
  105. ->limit(1)
  106. ->one();
  107. if (empty($upload_file_info)) {
  108. if (!empty($data['type']) && CourseEnum::CHAPTER_TYPE_VIDEO == $data['type']) {
  109. $is_obtain_duration = true;
  110. }
  111. } else {
  112. // $model->video_length = $upload_file_info->duration_str;
  113. $model->thumb_url = $upload_file_info->thumb_url;
  114. }
  115. if ($is_obtain_duration) {
  116. // 需要异步获取视频时长
  117. $params = [
  118. 'id' => $model->id,
  119. 'mall_id' => $mall_id,
  120. ];
  121. \Yii::$app->services->rabbitMq->push(RabbitMqEnum::EXCHANGE_ORDER, RabbitMqEnum::ORDER_QUEUE, $params, self::class, 'obtainVideoDuration');
  122. }
  123. if (!$model->save()) {
  124. self::$static_error = '保存数据失败:' . $model->getErrorMessage();
  125. return false;
  126. }
  127. return $model;
  128. }
  129. /**
  130. * 批量获取商品详情
  131. * @param $goods_id_list string 用逗号连接的商品ID
  132. * @param bool $first boolean 获取第一个商品 true 是 false 否
  133. * @return array
  134. */
  135. public static function getAllGoods($goods_id_list, $first = true)
  136. {
  137. if (is_string($goods_id_list)) {
  138. $goods_ids = explode(',', $goods_id_list);
  139. } else {
  140. $goods_ids = $goods_id_list;
  141. }
  142. $list = Goods::find()
  143. ->select( 'id,stock as good_stock,goods_name as name,detail,cover_pic,price,sales_num as sales' )
  144. ->where(['status' => StatusEnum::ENABLED, 'id' => $goods_ids])
  145. ->asArray()
  146. ->all();
  147. if ($first) {
  148. $list = $list[0];
  149. }
  150. return $list;
  151. }
  152. /**
  153. * 拆分商品ID列表数据
  154. * @param $goods_id_list string
  155. * @return array
  156. */
  157. public static function explodeGoodsIdList($goods_id_list)
  158. {
  159. $result = [];
  160. $goods_id_list = trim($goods_id_list, ",");
  161. if (!empty($goods_id_list)) {
  162. $result = explode(",", $goods_id_list);
  163. }
  164. return $result;
  165. }
  166. /**
  167. * 获取单个商品详情
  168. * @param $goods_id int 商品ID
  169. * @return array
  170. */
  171. public static function getOneGoods($goods_id)
  172. {
  173. $result = [];
  174. $goods_info = Goods::findOne($goods_id);
  175. if (!empty($goods_info)) {
  176. $result = [
  177. 'id' => $goods_info->id,
  178. 'goods_stock' => $goods_info->stock,
  179. 'name' => $goods_info->goods_name,
  180. 'detail' => $goods_info->detail,
  181. 'cover_pic' => $goods_info->cover_pic,
  182. 'price' => $goods_info->price,
  183. 'sales' => $goods_info->virtual_sales,
  184. ];
  185. }
  186. return $result;
  187. }
  188. /**
  189. * @name:
  190. * @msg: 把秒转换为 时分秒 的表示形式
  191. * @param {int} $seconds
  192. * @return {string}
  193. */
  194. public static function convertTime(int $seconds): string
  195. {
  196. if (empty($seconds)) {
  197. return $seconds;
  198. }
  199. // 一个小时的秒数
  200. $hour_seconds = 60 * 60;
  201. $minute_seconds = 60;
  202. // 小时数
  203. $hours = floor($seconds / $hour_seconds);
  204. // 除去整小时后剩余秒数
  205. $hour_surplus_seconds = $seconds % $hour_seconds;
  206. // 分钟数
  207. $minutes = floor($hour_surplus_seconds / $minute_seconds);
  208. // 除去整分钟后剩余的秒数
  209. $minute_surplus_seconds = $hour_surplus_seconds % $minute_seconds;
  210. $res = '';
  211. if (!empty($hours)) {
  212. $hours = mb_strlen($hours) > 1 ? $hours : '0' . $hours;
  213. $res .= $hours . ':';
  214. } else {
  215. $res .= '00:';
  216. }
  217. if (!empty($minutes)) {
  218. $minutes = mb_strlen($minutes) > 1 ? $minutes : '0' . $minutes;
  219. $res .= $minutes . ':';
  220. } else {
  221. $res .= '00:';
  222. }
  223. if (!empty($minute_surplus_seconds)) {
  224. $minute_surplus_seconds = mb_strlen($minute_surplus_seconds) > 1 ? $minute_surplus_seconds : '0' . $minute_surplus_seconds;
  225. $res .= $minute_surplus_seconds;
  226. } else {
  227. $res .= '00';
  228. }
  229. return $res;
  230. }
  231. /**
  232. * 获取章节详情
  233. * @Author: guyu
  234. * @DateTime:2021-03-23 15:23
  235. * @param $id int 章节ID
  236. * @return mixed
  237. */
  238. public static function getDetail($id)
  239. {
  240. $result = self::findOne(['id' => $id, 'status' => StatusEnum::ENABLED]);
  241. if (!empty($result)) {
  242. $result->goods_list = self::explodeGoodsIdList($result->goods_list);
  243. }
  244. return $result;
  245. }
  246. /**
  247. * 更新章节的播放次数 课程的播放次数
  248. * @param $id int 章节ID
  249. * @param $course_id int 课程ID
  250. * @return boolean | \Exception
  251. */
  252. public static function updateViews($id, $course_id)
  253. {
  254. $trans = \Yii::$app->db->beginTransaction();
  255. try {
  256. $chapter = self::getDetail($id);
  257. $course = AddonsCourse::getDetail($course_id, $chapter->mall_id);
  258. //设置章节视频播放次数 + 1
  259. //$chapter->views = $chapter->views + 1;
  260. if (!$chapter->updateCounters(['views' => 1])) {
  261. throw new \Exception("保存课程章节视频播放次数失败");
  262. }
  263. //设置课程视频播放次数 + 1
  264. //$course->views = $course->views + 1;
  265. if (!$course->updateCounters(['views' => 1])) {
  266. throw new \Exception("保存课程播放次数失败");
  267. }
  268. $trans->commit();
  269. return true;
  270. } catch (\Exception $ex) {
  271. $trans->rollBack();
  272. throw new \Exception($ex->getMessage());
  273. }
  274. return false;
  275. }
  276. /**
  277. * @name:
  278. * @msg: 获取课程章节视频时长,异步队列调用
  279. * @param {array} $params id:章节 id;mall_id:商城 id
  280. * @return {*}
  281. */
  282. public static function obtainVideoDuration(array $params = null)
  283. {
  284. try {
  285. if (empty($params) || empty($params['id']) || empty($params['mall_id'])) {
  286. throw new \Exception('参数错误。params => ' . var_export($params, true));
  287. }
  288. $model = self::find()
  289. ->where(['mall_id' => $params['mall_id'], 'id' => $params['id']])
  290. ->one();
  291. if (empty($model) || empty($model->video_url)) {
  292. throw new \Exception('章节或视频不存在');
  293. }
  294. $path = '/tmp/course-chapter-video/';
  295. if (!file_exists($path)) {
  296. mkdir($path, 0755);
  297. }
  298. $fileinfo = pathinfo($model->video_url);
  299. // dd($fileinfo);
  300. $tmp_video_file = $path . $fileinfo['basename'];
  301. $tmp_thumb_file = $path . $fileinfo['filename'] . '.jpg';
  302. $video_str = file_get_contents($model->video_url);
  303. file_put_contents($tmp_video_file, $video_str);
  304. $video_info = FfmpegHelper::getVideoInfo($tmp_video_file);
  305. // dd($video_info);
  306. FfmpegHelper::imageResize($tmp_video_file, $tmp_thumb_file, '00:00:01');
  307. $datas = [
  308. 'id' => $params['id'],
  309. 'video_length' => $video_info['duration'],
  310. ];
  311. if (file_exists($tmp_thumb_file)) {
  312. $query = AccessToken::find()->where(['status' => StatusEnum::ENABLED])->orderBy('created_at desc');
  313. $token = '';
  314. foreach ($query->each(5) as $token_info) {
  315. $timestamp = (int)substr($token_info->access_token, strrpos($token_info->access_token, '_') + 1);
  316. $expire = 1296000;
  317. // 验证有效期
  318. if ($timestamp + $expire <= time()) {
  319. continue;
  320. }
  321. $token = $token_info->access_token;
  322. break;
  323. }
  324. $mall = Mall::findOne($params['mall_id']);
  325. $headers = [
  326. 'x-access-token' => $token,
  327. 'x-app-platform' => 'h5',
  328. 'x-mall-sign' => $mall->mall_sign ?? '',
  329. 'x-parent-id' => -1,
  330. 'x-source' => 0,
  331. ];
  332. $upload_url = \Yii::getAlias('@apiUrl') . '/index.php/v1/file/upload';
  333. $upload_res = CurlHelper::upload($upload_url, $tmp_thumb_file, $headers);
  334. if ($upload_res['code'] != 0) {
  335. throw new \Exception('首帧图片上传出错--' . $upload_res['msg']);
  336. }
  337. // dd($thumb_url);
  338. $datas['thumb_url'] = $upload_res['data']['url'];
  339. }
  340. $res = self::setData($params['mall_id'], $datas);
  341. if (false === $res) {
  342. throw new \Exception(self::getStaticError());
  343. }
  344. // 删除临时文件
  345. unlink($tmp_thumb_file);
  346. unlink($tmp_video_file);
  347. return true;
  348. } catch (\Exception $e) {
  349. \Yii::error('异步获取章节视频时长出错:' . $e->getMessage() . ', file: ' . $e->getFile() . ', line: ' . $e->getLine());
  350. \Yii::getLogger()->flush(true);
  351. }
  352. }
  353. }