BroadcastRoomRepository.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2024 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\common\repositories\store\broadcast;
  12. use app\common\dao\store\broadcast\BroadcastRoomDao;
  13. use app\common\model\store\broadcast\BroadcastRoom;
  14. use app\common\repositories\BaseRepository;
  15. use crmeb\jobs\SendSmsJob;
  16. use crmeb\services\DownloadImageService;
  17. use crmeb\services\MiniProgramService;
  18. use crmeb\services\SwooleTaskService;
  19. use EasyWeChat\Core\Exceptions\HttpException;
  20. use Exception;
  21. use FormBuilder\Exception\FormBuilderException;
  22. use FormBuilder\Factory\Elm;
  23. use FormBuilder\Form;
  24. use think\db\exception\DataNotFoundException;
  25. use think\db\exception\DbException;
  26. use think\db\exception\ModelNotFoundException;
  27. use think\exception\ValidateException;
  28. use think\facade\Db;
  29. use think\facade\Queue;
  30. use think\facade\Route;
  31. /**
  32. * 直播间
  33. */
  34. class BroadcastRoomRepository extends BaseRepository
  35. {
  36. /**
  37. * @var BroadcastRoomDao
  38. */
  39. protected $dao;
  40. /**
  41. * BroadcastRoomRepository constructor.
  42. * @param BroadcastRoomDao $dao
  43. */
  44. public function __construct(BroadcastRoomDao $dao)
  45. {
  46. $this->dao = $dao;
  47. }
  48. /**
  49. * 根据条件获取指定商户的数据列表
  50. *
  51. * 本函数用于查询数据库中与指定商户相关的数据列表。它支持分页查询,并返回符合条件的数据总数和具体数据列表。
  52. * 主要用于后台管理或其他需要查询特定商户数据的场景。
  53. *
  54. * @param int $merId 商户ID,用于指定查询的商户
  55. * @param array $where 查询条件数组,用于进一步筛选数据
  56. * @param int $page 当前页码,用于分页查询
  57. * @param int $limit 每页数据条数,用于分页查询
  58. * @return array 返回包含数据总数(count)和数据列表(list)的数组
  59. */
  60. public function getList($merId, array $where, $page, $limit)
  61. {
  62. // 将指定的商户ID添加到查询条件中
  63. $where['mer_id'] = $merId;
  64. // 构建查询语句,根据创建时间降序排序
  65. $query = $this->dao->search($where)->order('create_time DESC');
  66. // 计算符合条件的数据总数
  67. $count = $query->count();
  68. // 执行分页查询,并获取具体的数据列表
  69. $list = $query->page($page, $limit)->select();
  70. // 将数据总数和数据列表打包成数组返回
  71. return compact('count', 'list');
  72. }
  73. /**
  74. * 获取用户列表
  75. *
  76. * 根据给定的条件和分页信息,查询用户列表,包括满足条件的用户数量和用户详细信息。
  77. * 特别地,关注了直播状态为非关闭的状态,以及对直播时间进行了格式化处理。
  78. *
  79. * @param array $where 查询条件,用于筛选用户。
  80. * @param int $page 当前页码,用于分页查询。
  81. * @param int $limit 每页显示的用户数量,用于分页查询。
  82. * @return array 返回包含用户数量和用户列表的数组。
  83. */
  84. public function userList(array $where, $page, $limit)
  85. {
  86. // 设置显示标签为1的条件,表示只查询显示中的用户
  87. $where['show_tag'] = 1;
  88. // 构建查询用户的信息,包括用户自身信息和关联的广播信息
  89. $query = $this->dao->search($where)->with([
  90. 'broadcast' => function ($query) {
  91. // 筛选正在销售的广播,并包含关联的商品信息
  92. $query->where('on_sale', 1);
  93. $query->with('goods');
  94. }
  95. ])
  96. // 筛选出房间ID大于0,且直播状态不为107(表示直播关闭)的用户
  97. ->where('room_id', '>', 0)
  98. ->whereNotIn('live_status', [107])
  99. // 按照星级、排序和创建时间倒序排列
  100. ->order('star DESC, sort DESC, create_time DESC');
  101. // 计算满足条件的用户总数
  102. $count = $query->count();
  103. // 分页查询用户信息
  104. $list = $query->page($page, $limit)->select();
  105. // 对查询结果中的每个用户,格式化其直播开始时间
  106. foreach ($list as $item) {
  107. $item->show_time = date('m/d H:i', strtotime($item->start_time));
  108. }
  109. // 返回用户总数和用户列表
  110. return compact('count', 'list');
  111. }
  112. /**
  113. * 获取管理员列表
  114. *
  115. * 根据给定的条件查询管理员信息,并包含关联的商户信息。支持分页查询。
  116. *
  117. * @param array $where 查询条件,用于筛选管理员。
  118. * @param int $page 当前页码,用于分页查询。
  119. * @param int $limit 每页显示的数量,用于分页查询。
  120. * @return array 返回包含管理员数量和管理员列表的数组。
  121. */
  122. public function adminList(array $where, $page, $limit)
  123. {
  124. // 构建查询语句,根据给定条件搜索管理员,并包含关联的商户信息,按特定字段排序
  125. $query = $this->dao->search($where)
  126. ->with(['merchant' => function ($query) {
  127. // 关联查询商户信息,只包含mer_name, mer_id, is_trader字段
  128. $query->field('mer_name,mer_id,is_trader');
  129. }])
  130. ->order('BroadcastRoom.star DESC, BroadcastRoom.sort DESC, BroadcastRoom.create_time DESC');
  131. // 计算满足条件的管理员总数
  132. $count = $query->count();
  133. // 分页查询管理员列表
  134. $list = $query->page($page, $limit)->select();
  135. // 返回管理员总数和管理员列表
  136. return compact('count', 'list');
  137. }
  138. /**
  139. * 创建直播间
  140. * @return Form
  141. * @throws FormBuilderException
  142. * @author xaboy
  143. * @day 2020/7/29
  144. */
  145. public function createForm()
  146. {
  147. return Elm::createForm(Route::buildUrl('merchantBroadcastRoomCreate')->build(), [
  148. Elm::input('name', '直播间名字:')->placeholder('请输入直播间名字')->required(),
  149. Elm::frameImage('cover_img', '背景图:', '/' . config('admin.merchant_prefix') . '/setting/uploadPicture?field=cover_img&type=1')
  150. ->info('建议像素1080*1920,大小不超过2M')->icon('el-icon-camera')->modal(['modal' => false])->width('1000px')->height('600px')->props(['footer' => false])->required(),
  151. Elm::frameImage('share_img', '分享图:', '/' . config('admin.merchant_prefix') . '/setting/uploadPicture?field=share_img&type=1')
  152. ->info('建议像素800*640,大小不超过1M')->icon('el-icon-camera')->modal(['modal' => false])->width('1000px')->height('600px')->props(['footer' => false])->required(),
  153. Elm::frameImage('feeds_img', '封面图:', '/' . config('admin.merchant_prefix') . '/setting/uploadPicture?field=feeds_img&type=1')
  154. ->info('建议像素800*800,大小不超过1M')->icon('el-icon-camera')->modal(['modal' => false])->width('1000px')->height('600px')->props(['footer' => false])->required(),
  155. Elm::input('anchor_name', '主播昵称:')->required()->placeholder('请输入主播昵称,主播需通过小程序直播认证,否则会提交失败。'),
  156. Elm::input('anchor_wechat', '主播微信号:')->required()->placeholder('请输入主播微信号,主播需通过小程序直播认证,否则会提交失败。'),
  157. Elm::input('phone', '联系电话')->placeholder('请输入联系电话')->required(),
  158. Elm::dateTimeRange('start_time', '直播时间:')->value([])->required(),
  159. Elm::radio('type', '直播间类型:', 0)->options([['value' => 0, 'label' => '手机直播'], ['value' => 1, 'label' => '推流']]),
  160. Elm::radio('screen_type', '显示样式:', 0)->options([['value' => 0, 'label' => '竖屏'], ['value' => 1, 'label' => '横屏']]),
  161. Elm::switches('close_like', '是否开启点赞:', 0)
  162. ->activeValue(0)->inactiveValue(1)
  163. ->activeText('开')->inactiveText('关'),
  164. Elm::switches('close_goods', '是否开启货架:', 0)
  165. ->activeValue(0)->inactiveValue(1)
  166. ->activeText('开')->inactiveText('关'),
  167. Elm::switches('close_comment', '是否开启评论:', 0)
  168. ->activeValue(0)->inactiveValue(1)
  169. ->activeText('开')->inactiveText('关'),
  170. Elm::switches('replay_status', '是否开启回放:', 0)
  171. ->activeValue(1)->inactiveValue(0)
  172. ->activeText('开')->inactiveText('关'),
  173. Elm::switches('close_share', '是否开启分享:', 0)
  174. ->activeValue(0)->inactiveValue(1)
  175. ->activeText('开')->inactiveText('关'),
  176. Elm::switches('close_kf', '是否开启客服:', 0)
  177. ->activeValue(0)->inactiveValue(1)
  178. ->activeText('开')->inactiveText('关'),
  179. Elm::switches('is_feeds_public', '是否开启官方收录:', 1)
  180. ->activeValue(1)->inactiveValue(0)
  181. ->activeText('开')->inactiveText('关'),
  182. ])->setTitle('创建直播间');
  183. }
  184. /**
  185. * 创建编辑直播间的表单
  186. *
  187. * 本函数用于生成一个用于编辑直播间的表单。它首先通过$id$从数据层获取直播间的信息,
  188. * 然后将开始时间和结束时间组合为一个数组,以便在表单中以特定的方式显示。
  189. * 最后,它构建并返回一个填充了直播间数据的表单实例,表单的动作是更新直播间信息的路由。
  190. *
  191. * @param int $id 直播间的唯一标识符,用于获取直播间的信息。
  192. * @return Form 生成的表单实例,包含了直播间的信息和编辑操作的动作。
  193. */
  194. public function updateForm($id)
  195. {
  196. // 通过$id$获取直播间的信息,并转换为数组格式
  197. $data = $this->dao->get($id)->toArray();
  198. // 将开始时间和结束时间组合为一个数组,方便在表单中处理
  199. $data['start_time'] = [$data['start_time'], $data['end_time']];
  200. // 创建表单,设置表单的动作为更新直播间信息的路由,并填充直播间数据
  201. // 设置表单标题为“编辑直播间”
  202. return $this->createForm()->setAction(Route::buildUrl('merchantBroadcastRoomUpdate', compact('id'))->build())->formData($data)->setTitle('编辑直播间');
  203. }
  204. /**
  205. * 创建直播房间
  206. *
  207. * @param string $merId 商户ID
  208. * @param array $data 房间相关数据
  209. * @return Room|bool 创建的房间对象或操作结果
  210. *
  211. * 本函数负责根据提供的商户ID和房间数据创建直播房间。它会根据商户是否是主播房间
  212. * 来设定房间的状态,并在创建房间后根据房间状态执行不同的后续操作,如设置房间ID
  213. * 和发送管理员通知。
  214. */
  215. public function create($merId, array $data)
  216. {
  217. // 根据商户是否是主播房间,设置房间状态
  218. $data['status'] = request()->merchant()->is_bro_room == 1 ? 0 : 1;
  219. $data['mer_id'] = $merId;
  220. // 使用事务处理来确保数据的一致性
  221. return Db::transaction(function () use ($data) {
  222. $room = $this->dao->create($data);
  223. // 如果房间是待审核状态,则进行房间ID的设置和状态更新
  224. if ($data['status'] == 1) {
  225. $room->room_id = $this->wxCreate($room);
  226. $room->status = 2;
  227. $room->save();
  228. } else {
  229. // 如果房间是待审核状态以外的状态,则发送管理员通知
  230. SwooleTaskService::admin('notice', [
  231. 'type' => 'new_broadcast',
  232. 'data' => [
  233. 'title' => '新直播间申请',
  234. 'message' => '您有1个新的直播间审核,请及时处理!',
  235. 'id' => $room->broadcast_room_id
  236. ]
  237. ]);
  238. }
  239. return $room;
  240. });
  241. }
  242. /**
  243. * 更新直播间信息并发送通知。
  244. *
  245. * 本函数用于处理直播间信息的更新,并在更新完成后发送管理员通知。它首先通过$merId和$id查询到对应的直播间,
  246. * 然后更新直播间的状态和其他信息。最后,通过SwooleTaskService发送一条新直播间申请的通知给管理员。
  247. *
  248. * @param string $merId 商户ID,用于查询直播间所属的商户。
  249. * @param int $id 直播间ID,用于查询具体的直播间。
  250. * @param array $data 包含需要更新的直播间信息的数据数组。
  251. */
  252. public function updateRoom($merId, $id, array $data)
  253. {
  254. // 设置直播间状态为待审核
  255. $data['status'] = 0;
  256. // 根据商户ID和直播间ID查询直播间信息
  257. $room = $this->dao->getWhere(['mer_id' => $merId, 'broadcast_room_id' => $id]);
  258. // 更新直播间的信息
  259. $room->save($data);
  260. // 发送管理员通知,告知有新的直播间申请待处理
  261. SwooleTaskService::admin('notice', [
  262. 'type' => 'new_broadcast',
  263. 'data' => [
  264. 'title' => '新直播间申请',
  265. 'message' => '您有1个新的直播间审核,请及时处理!',
  266. 'id' => $room->broadcast_room_id
  267. ]
  268. ]);
  269. }
  270. /**
  271. * 创建直播间申请表单
  272. *
  273. * 本函数用于生成一个包含直播间申请审核状态的表单。表单中包含一个单选按钮组,
  274. * 用于选择审核状态(未通过或通过),如果选择未通过,还需要提供未通过的原因。
  275. *
  276. * @param int $id 直播间申请的ID,用于构建表单的提交URL。
  277. * @return Elm|Form
  278. */
  279. public function applyForm($id)
  280. {
  281. // 构建表单提交的URL,使用紧凑模式传递ID参数
  282. $url = Route::buildUrl('systemBroadcastRoomApply', compact('id'))->build();
  283. // 创建表单,设置表单标题为“审核直播间”
  284. return Elm::createForm($url, [
  285. // 添加单选按钮组,用于选择审核状态,初始值为通过(1)
  286. Elm::radio('status', '审核状态:', '1')
  287. ->options([['value' => '-1', 'label' => '未通过'], ['value' => '1', 'label' => '通过']])
  288. ->control([
  289. // 当选择未通过时,显示文本区域,用于输入未通过的原因
  290. ['value' => '-1', 'rule' => [
  291. Elm::textarea('msg', '未通过原因:', '信息有误,请完善')
  292. ->placeholder('请输入未通过原因')
  293. ->required()
  294. ]]
  295. ])
  296. ])
  297. ->setTitle('审核直播间');
  298. }
  299. /**
  300. * 处理直播间的申请操作。
  301. *
  302. * 根据传入的状态对直播间进行相应的处理,包括修改直播间状态、记录错误信息、创建直播间、发送通知等。
  303. * 当状态为-1时,表示申请未通过,会记录失败原因;其他状态表示申请通过并进行相应的直播间创建和通知发送操作。
  304. *
  305. * @param int $id 直播间ID。
  306. * @param int $status 直播间状态,用于确定具体的处理流程。
  307. * @param string $msg 当状态为-1时,用于记录申请未通过的原因。
  308. */
  309. public function apply($id, $status, $msg = '')
  310. {
  311. // 根据ID获取直播间信息
  312. $room = $this->dao->get($id);
  313. // 开启数据库事务,确保一系列操作的原子性
  314. Db::transaction(function () use ($msg, $status, $room) {
  315. // 更新直播间状态
  316. $room->status = $status;
  317. // 当状态为-1时,记录未通过的原因
  318. if ($status == -1) {
  319. $room->error_msg = $msg;
  320. } else {
  321. // 通过微信接口创建直播间,并更新直播间ID和状态
  322. $room_id = $this->wxCreate($room);
  323. $room->room_id = $room_id;
  324. $room->status = 2;
  325. // 如果直播间类型需要,生成并记录推流地址
  326. if ($room->type) {
  327. $path = MiniProgramService::create()->miniBroadcast()->getPushUrl($room_id);
  328. $room->push_url = $path->pushAddr;
  329. }
  330. }
  331. // 保存更新后的直播间信息
  332. $room->save();
  333. // 发送审核状态通知给商家
  334. SwooleTaskService::merchant('notice', [
  335. 'type' => 'broadcast_status_' . ($status == -1 ? 'fail' : 'success'),
  336. 'data' => [
  337. 'title' => '直播间审核通知',
  338. 'message' => $status == -1 ? '您的直播间审核未通过!' : '您的直播间审核已通过',
  339. 'id' => $room->broadcast_room_id
  340. ]
  341. ], $room->mer_id);
  342. // 当状态为-1时,将未通过审核的信息推送到短信队列
  343. if ($status == -1) {
  344. Queue::push(SendSmsJob::class, [
  345. 'tempId' => 'BROADCAST_ROOM_FAIL',
  346. 'id' => $room['broadcast_room_id']
  347. ]);
  348. }
  349. });
  350. }
  351. /**
  352. * 创建微信直播房间
  353. *
  354. * @param BroadcastRoom $room 直播房间信息对象
  355. * @return string 创建的直播房间ID
  356. * @throws ValidateException 如果房间已经存在,则抛出验证异常
  357. */
  358. public function wxCreate(BroadcastRoom $room)
  359. {
  360. // 检查房间ID是否存在,如果存在则表示房间已创建,抛出异常
  361. if ($room['room_id']) {
  362. throw new ValidateException('直播间已创建');
  363. }
  364. // 将BroadcastRoom对象转换为数组
  365. $room = $room->toArray();
  366. // 创建小程序服务实例
  367. $miniProgramService = MiniProgramService::create();
  368. // 创建图片下载服务实例
  369. $DownloadImageService = app()->make(DownloadImageService::class);
  370. // 下载并获取封面图片路径
  371. $coverImg = './public' . $DownloadImageService->downloadImage($room['cover_img'], 'def', '', 1)['path'];
  372. // 下载并获取分享图片路径
  373. $shareImg = './public' . $DownloadImageService->downloadImage($room['share_img'], 'def', '', 1)['path'];
  374. // 下载并获取Feed流图片路径
  375. $feedsImg = './public' . $DownloadImageService->downloadImage($room['feeds_img'], 'def', '', 1)['path'];
  376. // 准备直播房间相关信息
  377. $data = [
  378. 'name' => $room['name'], // 直播间名称
  379. 'coverImg' => $miniProgramService->material()->uploadImage($coverImg)->media_id, // 封面图片ID
  380. 'startTime' => strtotime($room['start_time']), // 直播开始时间戳
  381. 'endTime' => strtotime($room['end_time']), // 直播结束时间戳
  382. 'anchorName' => $room['anchor_name'], // 主播姓名
  383. 'anchorWechat' => $room['anchor_wechat'], // 主播微信号
  384. 'shareImg' => $miniProgramService->material()->uploadImage($shareImg)->media_id, // 分享图片ID
  385. 'feedsImg' => $miniProgramService->material()->uploadImage($feedsImg)->media_id, // Feed流图片ID
  386. 'type' => $room['type'], // 直播类型
  387. 'closeLike' => $room['close_like'], // 是否关闭点赞
  388. 'closeGoods' => $room['close_goods'], // 是否关闭商品
  389. 'closeComment' => $room['close_comment'], // 是否关闭评论
  390. 'screenType' => $room['screen_type'], // 屏幕类型
  391. 'closeShare' => $room['close_share'], // 是否关闭分享
  392. 'closeKf' => $room['close_kf'], // 是否关闭客服
  393. 'closeReplay' => $room['replay_status'] == 1 ? 0 : 1, // 是否关闭回放
  394. 'isFeedsPublic' => $room['is_feeds_public'] == 1 ? 0 : 1, // 是否关闭Feed流公开
  395. ];
  396. // 删除本地临时图片文件
  397. @unlink($coverImg);
  398. @unlink($shareImg);
  399. @unlink($feedsImg);
  400. try {
  401. // 创建直播房间,并获取房间ID
  402. $roomId = $miniProgramService->miniBroadcast()->createLiveRoom($data)->roomId;
  403. } catch (Exception $e) {
  404. // 如果创建失败,抛出验证异常,异常信息为微信返回的错误信息
  405. throw new ValidateException($e->getMessage());
  406. }
  407. // 将发送短信的任务推入队列
  408. Queue::push(SendSmsJob::class, [
  409. 'tempId' => 'BROADCAST_ROOM_CODE',
  410. 'id' => $room['broadcast_room_id']
  411. ]);
  412. // 返回创建的直播房间ID
  413. return $roomId;
  414. }
  415. /**
  416. * 更新展示状态
  417. * 根据管理员身份决定更新的是全局展示状态还是商家展示状态
  418. *
  419. * @param int $id 数据标识符
  420. * @param int $isShow 展示状态值,通常为0(不展示)或1(展示)
  421. * @param bool $admin 管理员身份标记,默认为false,表示非管理员
  422. * @return bool 更新操作的结果,true表示成功,false表示失败
  423. */
  424. public function isShow($id, $isShow, bool $admin = false)
  425. {
  426. // 根据管理员身份选择更新的字段,管理员更新is_show字段,非管理员更新is_mer_show字段
  427. return $this->dao->update($id, [($admin ? 'is_show' : 'is_mer_show') => $isShow]);
  428. }
  429. /**
  430. * 更新记录的标记。
  431. *
  432. * 本函数通过调用DAO层的update方法,更新指定ID的记录的mark字段。
  433. * 主要用于在系统中对特定资源进行标记或状态更新,例如标记一项任务为完成。
  434. *
  435. * @param int $id 需要更新的记录的ID。这是一个主键标识,用于精确定位到要更新的数据。
  436. * @param string $mark 新的标记值。这个值将替换原有记录的mark字段,用于表示资源的最新状态或标记。
  437. * @return bool 返回更新操作的结果。成功更新时返回true,更新失败则返回false。
  438. */
  439. public function mark($id, $mark)
  440. {
  441. // 调用DAO层的update方法,传入ID和新的标记值,尝试更新记录。
  442. return $this->dao->update($id, compact('mark'));
  443. }
  444. /**
  445. * 导出商品到直播间
  446. * 该方法用于将指定的商品关联到指定的直播间。它首先验证所选商品的有效性,然后检查直播间的状态,
  447. * 最后将商品与直播间建立关联。
  448. *
  449. * @param int $merId 商家ID,用于权限验证和商品查询。
  450. * @param array $ids 商品ID列表,表示需要导出到直播间的商品ID。
  451. * @param int $roomId 直播间ID,表示商品将被导出到的直播间。
  452. * @throws ValidateException 如果验证失败,抛出异常提示用户。
  453. */
  454. public function exportGoods($merId, array $ids, $roomId)
  455. {
  456. // 实例化直播商品仓库,用于查询商品信息。
  457. $broadcastGoodsRepository = app()->make(BroadcastGoodsRepository::class);
  458. // 验证所选商品是否存在并属于该商家。
  459. if (count($ids) != count($goods = $broadcastGoodsRepository->goodsList($merId, $ids)))
  460. throw new ValidateException('请选择正确的直播商品');
  461. // 验证直播间是否存在并处于有效状态。
  462. if (!$room = $this->dao->validRoom($roomId, $merId))
  463. throw new ValidateException('直播间状态有误');
  464. // 实例化直播房间商品仓库,用于查询直播间商品关联信息。
  465. $broadcastRoomGoodsRepository = app()->make(BroadcastRoomGoodsRepository::class);
  466. // 获取当前直播间已关联的商品ID列表。
  467. $goodsId = $broadcastRoomGoodsRepository->goodsId($room->broadcast_room_id);
  468. $ids = [];
  469. $data = [];
  470. // 遍历商品列表,找出未关联到当前直播间的商品,准备建立关联。
  471. foreach ($goods as $item) {
  472. if (!in_array($item->broadcast_goods_id, $goodsId)) {
  473. $data[] = [
  474. 'broadcast_room_id' => $room->broadcast_room_id,
  475. 'broadcast_goods_id' => $item->broadcast_goods_id
  476. ];
  477. $ids[] = $item->goods_id;
  478. }
  479. }
  480. // 如果没有需要新增关联的商品,则直接返回。
  481. if (!count($ids)) return;
  482. // 使用事务确保数据操作的完整性。
  483. Db::transaction(function () use ($ids, $broadcastRoomGoodsRepository, $goods, $room, $data) {
  484. // 批量插入新的商品与直播间关联记录。
  485. $broadcastRoomGoodsRepository->insertAll($data);
  486. // 调用小程序服务,将商品添加到直播间(注意:这里的代码可能需要根据实际服务位置进行调整)。
  487. MiniProgramService::create()->miniBroadcast()->addGoods(['roomId' => $room->room_id, 'ids' => $ids]);
  488. });
  489. }
  490. /**
  491. * 删除导出的商品
  492. * 该方法用于从直播房间中移除指定的商品。它首先验证商家和直播房间的存在性,然后通过商品ID和房间ID删除相关商品。
  493. * 如果商家或直播房间不存在,则抛出一个验证异常。
  494. *
  495. * @param int $merId 商家ID 用于验证商家是否存在
  496. * @param int $roomId 直播房间ID 用于验证直播房间是否存在
  497. * @param int $id 商品ID 用于从直播房间中删除指定商品
  498. * @throws ValidateException 如果商家或直播房间不存在,则抛出此异常
  499. */
  500. public function rmExportGoods($merId, $roomId, $id)
  501. {
  502. // 验证商家和直播房间是否存在
  503. if (!$this->dao->merExists($roomId, $merId))
  504. throw new ValidateException('直播间不存在');
  505. // 删除指定房间中的商品
  506. app()->make(BroadcastRoomGoodsRepository::class)->rmGoods($id, $roomId);
  507. }
  508. /**
  509. * 同步房间状态
  510. * 该方法用于从小程序广播接口获取房间状态,并更新本地数据库中对应房间的状态。考虑到性能和接口限制,采用分页方式批量获取和更新。
  511. */
  512. public function syncRoomStatus()
  513. {
  514. // 初始化起始位置和每批处理的数量
  515. $start = 0;
  516. $limit = 50;
  517. // 创建小程序广播客户端
  518. $client = MiniProgramService::create()->miniBroadcast();
  519. do {
  520. // 分批获取房间信息
  521. $data = $client->getRooms($start, $limit)->room_info;
  522. $start += 50; // 更新起始位置,准备下一批处理
  523. // 根据获取的房间ID,从本地数据库中批量获取房间详情
  524. $rooms = $this->getRooms(array_column($data, 'roomid'));
  525. // 遍历获取的房间信息,对比并更新本地数据库中的房间状态
  526. foreach ($data as $room) {
  527. // 如果本地有该房间记录且状态不同,则更新房间状态
  528. if (isset($rooms[$room['roomid']]) && $room['live_status'] != $rooms[$room['roomid']]['live_status']) {
  529. $this->dao->update($rooms[$room['roomid']]['broadcast_room_id'], ['live_status' => $room['live_status']]);
  530. }
  531. }
  532. } while (count($data) >= $limit); // 如果当前批次的数据量达到或超过限制,继续处理下一批
  533. }
  534. /**
  535. * 商家删除操作
  536. *
  537. * 本函数用于执行商家的删除操作。删除操作由商家ID标识。
  538. * 注意:此处的删除操作可能是逻辑删除,即标记为删除,而不是物理删除。
  539. *
  540. * @param int $id 商家ID,用于指定需要删除的商家。
  541. * @return bool 返回删除操作的结果,通常是操作是否成功的布尔值。
  542. *
  543. * @throws ValidateException 如果商家状态不正确,则抛出验证异常。
  544. */
  545. public function merDelete($id)
  546. {
  547. // 通过商家ID执行删除操作
  548. // 此处注释掉的代码块原本用于检查商家的状态是否允许删除
  549. // 如果商家状态不正确,则不应该执行删除操作,并抛出异常
  550. // 在实际执行中,这些检查可能被实现为更复杂的业务逻辑,以确保数据的安全性和一致性
  551. // 调用DAO层的方法执行商家删除操作
  552. return $this->dao->merDelete($id);
  553. }
  554. /**
  555. * 关闭直播相关信息的函数
  556. *
  557. * 本函数用于处理直播间的关闭操作,包括关闭客服、评论、公开性以及商品上架状态的变更。
  558. * 在执行操作前,会检查直播间的审核状态及权限,确保只有在允许的情况下才能进行变更。
  559. * 使用事务确保数据库操作的一致性。
  560. *
  561. * @param int $id 直播间ID
  562. * @param string $type 关闭类型,包括关闭客服('close_kf')、关闭评论('close_comment')、设置feed是否公开('is_feeds_public')和商品上架状态('on_sale')
  563. * @param int $status 新的状态值,用于关闭或开启相关功能
  564. * @param bool $check 是否检查平台状态,默认为true。如果平台已关闭,则不允许进行修改。
  565. * @param array $data 当类型为'on_sale'时,需要提供商品相关信息,包括商品ID。
  566. * @throws ValidateException 当直播间状态不正确、数据不存在或操作权限受限时抛出异常。
  567. */
  568. public function closeInfo($id, string $type, int $status, $check = true, $data = [])
  569. {
  570. // 根据ID获取直播间信息
  571. $room = $this->dao->get($id);
  572. // 检查直播间是否已通过审核
  573. if ($room->status !== 2) throw new ValidateException('直播间还未审核通过,无法修改');
  574. // 检查直播间是否存在
  575. if (!$room) throw new ValidateException('数据不存在');
  576. // 如果需要检查平台状态且直播间对应类型已关闭,则抛出异常
  577. if ($check && $room[$type] == -1) {
  578. throw new ValidateException('平台已关闭,您无法修改');
  579. }
  580. // 使用事务处理数据库操作
  581. Db::transaction(function () use ($room, $id, $type, $status, $data) {
  582. // 根据类型创建对应的操作客户端
  583. $client = MiniProgramService::create()->miniBroadcast();
  584. // 根据类型执行相应的关闭操作
  585. switch ($type) {
  586. case 'close_kf':
  587. // 关闭客服
  588. $client->closeKf($room->room_id, $status);
  589. $room->close_kf = $status;
  590. break;
  591. case 'close_comment':
  592. // 关闭评论
  593. $client->banComment($room->room_id, $status);
  594. $room->close_comment = $status;
  595. break;
  596. case 'is_feeds_public':
  597. // 设置feed公开性
  598. $client->updateFeedPublic($room->room_id, $status);
  599. $room->is_feeds_public = $status;
  600. break;
  601. case 'on_sale':
  602. // 商品上架状态变更
  603. $ret = app()->make(BroadcastRoomGoodsRepository::class)->getWhere([
  604. 'broadcast_room_id' => $id,
  605. 'broadcast_goods_id' => $data['goods_id'],
  606. ], '*', ['goods']);
  607. // 检查商品是否存在
  608. if (!isset($ret['goods']['goods_id'])) throw new ValidateException('数据不存在');
  609. // 更新商品上架状态
  610. $ret->on_sale = $status;
  611. $ret->save();
  612. // 商品上架或下架
  613. $client->goodsOnsale($room->room_id, $ret['goods']['goods_id'], $status);
  614. break;
  615. }
  616. // 更新直播间信息
  617. $room->save();
  618. });
  619. }
  620. /**
  621. * 创建商家助手编辑表单
  622. *
  623. * 该方法用于生成一个用于编辑商家助手的表单。商家助手是在直播间中协助商家进行互动的工具。
  624. * 表单中包含一个选择字段,用于选择可用的小助手。
  625. *
  626. * @param int $id 商家直播间的ID
  627. * @param int $merId 商家的ID
  628. * @return \EasyWeChat\Kernel\Messages\Miniprogram|Form
  629. *
  630. * @throws ValidateException 如果直播间未通过审核,则抛出异常
  631. */
  632. public function assistantForm(int $id, int $merId)
  633. {
  634. // 实例化广播助手仓库
  635. $make = app()->make(BroadcastAssistantRepository::class);
  636. // 根据ID获取直播间信息
  637. $get = $this->dao->get($id);
  638. // 如果直播间的状态不是2(未审核通过),则抛出异常
  639. if ($get->status !== 2) throw new ValidateException('直播间还未审核通过,无法操作');
  640. // 获取可选的助手列表
  641. $data = $make->options($merId);
  642. // 检查当前商家是否已添加过助手
  643. $has = $make->intersection($get->assistant_id, $merId);
  644. // 创建表单,设置表单的URL和标题,并添加选择助手的字段
  645. return Elm::createForm(Route::buildUrl('merchantBroadcastAddAssistant', compact('id'))->build(),
  646. [
  647. Elm::selectMultiple('assistant_id', '小助手:')->options(function () use ($data) {
  648. $options = [];
  649. // 如果有可用的助手数据,遍历生成选项
  650. if ($data) {
  651. foreach ($data as $value => $label) {
  652. $options[] = compact('value', 'label');
  653. }
  654. }
  655. return $options;
  656. })
  657. ])->setTitle('修改小助手');
  658. }
  659. /**
  660. * 编辑助手信息
  661. *
  662. * 该方法用于更新指定房间ID下的助手列表。它首先检查新提供的助手ID列表是否与现有列表有差异,
  663. * 然后根据差异添加或移除助手,并最终更新数据库中的助手ID组合。
  664. *
  665. * @param int $id 房间ID
  666. * @param int $merId 商家ID,用于权限控制或记录操作来源
  667. * @param array $data 新的助手ID列表
  668. */
  669. public function editAssistant(int $id, int $merId, array $data)
  670. {
  671. // 实例化广播助手仓库,用于后续的操作
  672. $make = app()->make(BroadcastAssistantRepository::class);
  673. // 检查传入的数据是否在数据库中全部存在,用于防止非法数据操作
  674. $make->existsAll($data, $merId);
  675. // 使用数据库事务来确保操作的原子性
  676. Db::transaction(function () use ($id, $data) {
  677. // 获取当前房间的信息
  678. $get = $this->dao->get($id);
  679. // 将现有的助手ID字符串转换为数组
  680. $old = explode(',', $get->assistant_id);
  681. // 计算需要移除的助手ID和需要添加的助手ID
  682. $remove = array_diff($old, $data);
  683. $add = array_diff($data, $old);
  684. // 分别调用方法来添加和移除助手
  685. $this->addAssistant($get->room_id, $add);
  686. $this->removeAssistant($get->room_id, $remove);
  687. // 更新房间的助手ID列表,并保存到数据库
  688. $get->assistant_id = implode(',', $data);
  689. $get->save();
  690. });
  691. }
  692. /**
  693. * 从指定的房间中移除助手。
  694. *
  695. * 本函数用于处理从特定房间中移除多个助手的操作。它首先通过提供的助手ID查询到相关助手的信息,
  696. * 然后依次调用接口将这些助手从指定的房间中移除。
  697. *
  698. * @param int $roomId 房间ID,指定要从哪个房间移除助手。
  699. * @param array $ids 助手ID数组,指定要移除的助手的ID列表。
  700. */
  701. public function removeAssistant($roomId, array $ids)
  702. {
  703. // 实例化广播助手仓库,用于后续查询助手信息。
  704. $make = app()->make(BroadcastAssistantRepository::class);
  705. // 根据提供的助手ID查询助手信息。
  706. $data = $make->getSearch(['assistant_ids' => $ids])->select();
  707. // 遍历查询到的助手信息,逐个从房间中移除助手。
  708. foreach ($data as $datum) {
  709. // 创建小程序服务实例,并调用其广播相关方法,实现在指定房间中移除助手的功能。
  710. MiniProgramService::create()->miniBroadcast()->removeAssistant($roomId, $datum->username);
  711. }
  712. }
  713. /**
  714. * 添加助手到直播间
  715. *
  716. * 本函数用于将指定的助手用户添加到指定的直播间中。它首先通过助手ID查询用户信息,
  717. * 然后将这些信息发送给小程序服务端,以在直播间中添加助手。
  718. *
  719. * @param int $roomId 直播间ID
  720. * @param array $ids 助手用户的ID列表
  721. */
  722. public function addAssistant($roomId, array $ids)
  723. {
  724. // 通过依赖注入获取广播助手仓库实例
  725. $make = app()->make(BroadcastAssistantRepository::class);
  726. // 根据助手ID查询用户用户名和昵称
  727. $data = $make->getSearch(['assistant_ids' => $ids])->column('username,nickname');
  728. // 构建添加助手的参数
  729. $params = [
  730. 'roomId' => $roomId,
  731. 'users' => $data
  732. ];
  733. // 调用小程序服务,添加助手到直播间
  734. MiniProgramService::create()->miniBroadcast()->addAssistant($params);
  735. }
  736. /**
  737. * 向特定房间的用户推送消息。
  738. *
  739. * 本函数旨在向小程序中特定房间的用户推送消息。它首先通过房间ID获取房间信息,
  740. * 然后分页获取该房间的所有关注者列表,并将消息推送给这些用户。
  741. *
  742. * @param int $id 房间的ID,用于定位要推送消息的特定房间。
  743. * @throws ValidateException 如果获取关注者列表或推送消息时发生错误,则抛出异常。
  744. */
  745. public function pushMessage(int $id)
  746. {
  747. // 通过房间ID获取房间信息
  748. $get = $this->dao->get($id);
  749. // 创建小程序服务实例,并初始化广播功能
  750. $make = MiniProgramService::create()->miniBroadcast();
  751. // 初始化分页标识
  752. $page_break = '';
  753. do {
  754. // 分页获取关注者列表
  755. $data = $make->getFollowers($page_break);
  756. $restult = [];
  757. // 检查是否有错误发生,如果有则抛出异常
  758. if ($data['errcode'] !== 0) throw new ValidateException($data['errmsg']);
  759. // 遍历关注者列表,筛选出属于指定房间的用户
  760. foreach ($data['followers'] as $datum) {
  761. if ($datum['room_id'] == $get->room_id) {
  762. $restult[] = $datum['openid'];
  763. }
  764. }
  765. // 如果有符合的用户,则向他们推送消息
  766. if ($restult) {
  767. $make->pushMessage($get->room_id, $restult);
  768. }
  769. // 更新分页标识,准备获取下一页数据
  770. $page_break = $data['page_break'] ?? '';
  771. } while ($page_break);
  772. }
  773. }