TechniciansService.php 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571
  1. <?php
  2. namespace addons\ReservationService\common\services;
  3. use addons\ReservationService\common\enums\ReservationServiceEnum;
  4. use addons\ReservationService\common\models\ReservationServiceGoods;
  5. use addons\ReservationService\common\models\ReservationServiceOccupy;
  6. use addons\ReservationService\common\models\ReservationServiceOrder;
  7. use addons\ReservationService\common\models\ReservationServiceStoreGoods;
  8. use addons\ReservationService\common\models\ReservationServiceStoreSecondaryCard;
  9. use addons\ReservationService\common\models\ReservationServiceUser;
  10. use addons\ReservationService\common\models\ReservationServiceUserBookTime;
  11. use addons\ReservationService\common\models\ReservationServiceWorkTime;
  12. use addons\SecondaryCard\common\enums\SecondaryCardEnum;
  13. use addons\SecondaryCard\common\models\SecondaryCard;
  14. use addons\SecondaryCard\common\models\SecondaryCardRelateStore;
  15. use addons\SecondaryCard\common\models\SecondaryCardReservationService;
  16. use addons\Store\common\models\Store;
  17. use addons\Store\common\models\StoreGoods;
  18. use addons\Store\common\models\StoreGoodsReserve;
  19. use addons\Store\common\models\StoreGoodsReserveSku;
  20. use addons\Store\common\models\StoreOrderReserve;
  21. use addons\Store\common\models\StoreReserveTechnicianScheduling;
  22. use addons\Store\common\services\StoreReserveSettingService;
  23. use common\enums\AddonsEnum;
  24. use common\enums\GoodsTypeEnum;
  25. use common\enums\StatusEnum;
  26. use common\helpers\DateHelper;
  27. use common\helpers\LocationHelper;
  28. use common\models\goods\GoodsReserve;
  29. use common\models\goods\GoodsReserveSku;
  30. use common\models\goods\GoodsReserveTime;
  31. use common\models\mall\MallAddons;
  32. use common\models\order\OrderReserve;
  33. use common\models\order\OrderReserveComments;
  34. use common\traits\ErrorTrait;
  35. class TechniciansService
  36. {
  37. use ErrorTrait;
  38. public static function search($params,$page,$limit)
  39. {
  40. try{
  41. if(!$params['lat'] || !$params['lng']){
  42. throw new \Exception('经纬度缺失');
  43. }
  44. $mall_id = $params['mall_id'];
  45. $basic = \Yii::$app->services->setting->addons->get($mall_id, ReservationServiceEnum::ADDONS_NAME, 'basic') ?? [];
  46. if (empty($basic['is_enable']) && empty($params['store_id'])) {
  47. throw new \Exception('未开启预约服务');
  48. }
  49. $max_distance = $basic['max_distance'] ?? 20; //限制
  50. $is_working = $params['is_working'] ?? 1;
  51. $working_field = empty($params['store_id']) ? 'is_working' : 'store_status';
  52. $where[] = ['mall_id' => $params['mall_id'],'is_working'=> 1,'status' => StatusEnum::ENABLED,'check_status'=>StatusEnum::ENABLED ];
  53. $min_age = $params['min_age'] ?? 0;
  54. $max_age = $params['max_age'] ?? 0;
  55. $gender = $params['gender'] ?? 0;
  56. $name = $params['name'] ?? '';
  57. $goods_id = $params['goods_id'] ?? 0;
  58. $sku_id = $params['sku_id'] ?? 0;
  59. $service_min = 30;
  60. $user_ids = [];
  61. $longitude = 'longitude';
  62. $latitude = 'latitude';
  63. $s_longitude = 'longitude';
  64. $s_latitude = 'latitude';
  65. if($goods_id && $sku_id){
  66. $select_date = $params['date'] ?? date('Y-m-d');
  67. $select_time = $params['time'] ?? '';
  68. if (!empty($params['store_id'])) {
  69. // 取出门店下面的技师
  70. if(empty($params['id'])){
  71. $user_ids = ReservationServiceUser::find()->where(['mall_id' => $params['mall_id'], 'store_id' => $params['store_id'],'status'=>StatusEnum::ENABLED])->select('user_id')->column();
  72. if (empty($user_ids)) throw new \Exception("当前门店暂无技师");
  73. }else{
  74. $user_ids[] = $params['id'];
  75. }
  76. $sku = StoreGoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  77. $longitude = 'store_lng';
  78. $latitude = 'store_lat';
  79. $s_longitude = 'store_lng as longitude';
  80. $s_latitude = 'store_lat as latitude';
  81. } else {
  82. if(empty($params['id'])){
  83. $support_user_list = ReservationServiceGoods::find()->where(['mall_id' => $params['mall_id'],'goods_id' => $goods_id,'status'=>StatusEnum::ENABLED])->select('user_id')->asArray()->all();
  84. $user_ids = array_column($support_user_list,'user_id');
  85. $user_ids = array_unique($user_ids);
  86. if(!$user_ids) throw new \Exception('没有技师支持该项目');
  87. }else{
  88. $user_ids[] = $params['id'];
  89. }
  90. $sku = GoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  91. if (!empty($select_time)) {
  92. $tech_ids = OrderReserve::find()
  93. ->where(['<=', 'service_start_at', strtotime($select_date . ' ' . $select_time . ':00')])
  94. ->andWhere(['>', 'service_end_at', strtotime($select_date . ' ' . $select_time . ':00')])
  95. ->andWhere(['!=', 'tech_id', 0])
  96. ->andWhere(['!=', 'order_status', -4])
  97. ->select('tech_id')->column();
  98. $booked_user_ids = ReservationServiceUser::find()->where(['id' => $tech_ids, 'status' => StatusEnum::ENABLED])->select('user_id')->column();
  99. }
  100. }
  101. $service_min = $sku['service_min'] ?? 30; //服务分钟
  102. if($select_time){
  103. $select_timestamp = strtotime($select_date.' '.$select_time);
  104. $select_time_arr = explode(':',$select_time);
  105. $start_time = $select_time_arr[0]*100+ $select_time_arr[1] *1;
  106. $end_time = $start_time + $service_min;
  107. $select_timestamp_end = $select_timestamp + 60 * $service_min;
  108. $select_week_day = date('w',strtotime($select_date));
  109. $user_times = ReservationServiceWorkTime::find()->where(['mall_id' => $mall_id,'user_id' => $user_ids,'status'=>StatusEnum::ENABLED])
  110. ->andWhere(['week_day' => $select_week_day])
  111. ->andWhere(['<=','start_time',$start_time])
  112. ->andWhere(['>=', 'end_time', $start_time])
  113. ->select('user_id')
  114. ->asArray()->all();
  115. if(!$user_times) throw new \Exception('该时间下没有技师支持该项目');
  116. $user_ids = array_column($user_times,'user_id');
  117. }
  118. //排除当前有项目的技师
  119. $user_ids = self::checkCanBook($mall_id,$user_ids,$select_timestamp,$select_timestamp_end);
  120. if(!$user_ids) throw new \Exception('该时间已满员');
  121. $scheduling_user_ids = self::getSchedulingUserIds($mall_id, $select_date, $select_time);
  122. }
  123. $sql = "SELECT id,mall_id,user_id,`level`, {$s_longitude}, {$s_latitude},`name`,service_rating,order_num,gender,age,`desc`,avatar,store_id, address,
  124. round(
  125. 2 * 6378.137 * ASIN(
  126. SQRT(
  127. POW(
  128. SIN(
  129. PI() * ({$params['lng']} - {$longitude}) / 360
  130. ),
  131. 2
  132. ) + COS(PI() * {$params['lat']} / 180) * COS({$latitude} * PI() / 180) * POW(
  133. SIN(
  134. PI() * ({$params['lat']} - {$latitude}) / 360
  135. ),
  136. 2
  137. )
  138. )
  139. )
  140. ,1) AS distance
  141. FROM `qimall_addons_reservation_service_user` WHERE (`mall_id`={$mall_id}) AND (`status`=1) AND (`is_working`=1) AND (`check_status`= 1) AND (`{$working_field}` ={$is_working}) HAVING (`distance`<={$max_distance})";
  142. if ($gender > 0){
  143. $gender = $gender > 1 ? 2: 1;
  144. $sql .= " AND `gender` = " . $gender;
  145. }
  146. if ($min_age > 0 || $max_age > 0){
  147. $sql .= " AND `age` >= " . $min_age;
  148. $sql .= " AND `age` <= " . $max_age;
  149. }
  150. if($name){
  151. $name = trim($name);
  152. $sql .= " AND `name` LIKE '%" . $name . "%'";
  153. }
  154. if($user_ids){
  155. $sql .= " AND `user_id` in (" . implode(',',$user_ids) . ")";
  156. }
  157. if (!empty($booked_user_ids)) {
  158. $sql .= " AND `user_id` not in (" . join(',', $booked_user_ids) . ")";
  159. }
  160. if (!empty($scheduling_user_ids)) {
  161. $sql .= " AND `user_id` not in (" . join(',', $scheduling_user_ids) . ")";
  162. }
  163. $count_sql = $sql;
  164. $orderBy = 'distance asc,id desc'; //1-距离
  165. if (isset($params['sort']) && $params['sort'] > 1){
  166. //1-距离,2-订单量 order_num ,3-好评 service_rating
  167. if($params['sort'] == 1){
  168. $orderBy = 'distance asc'; //1-距离
  169. }else{
  170. $orderBy = $params['sort'] == 2 ? 'order_num desc' : 'service_rating desc';
  171. }
  172. }
  173. $total_data = \Yii::$app->db->createCommand($count_sql)->queryAll();
  174. $count = count($total_data);
  175. $sql .= " ORDER BY {$orderBy}";
  176. $sql .= " limit " . ($page - 1) * $limit . ",{$limit}";
  177. $list = \Yii::$app->db->createCommand($sql)->queryAll();
  178. $default_jishi = [];
  179. if (!empty($list)) {
  180. $location_obj = new LocationHelper();
  181. $store_ids = array_column($list,'store_id');
  182. $store_ids = array_unique($store_ids);
  183. if (MallAddons::isInstall($mall_id, 'Store')) {
  184. $store_list = Store::find()->where(['id' => $store_ids, 'mall_id' => $mall_id])->select('id,store_name')->indexBy('id')->asArray()->all();
  185. }
  186. $comments = OrderReserveComments::lists([
  187. 'where' => [
  188. ['tech_user_id' => array_column($list, 'user_id')],
  189. ['reserve_type' => StatusEnum::ENABLED],
  190. ['store_id'=>0]
  191. ],
  192. 'select'=> 'COUNT(*) as total, tech_user_id',
  193. 'group' => 'tech_user_id',
  194. 'index' => 'tech_user_id'
  195. ]);
  196. foreach ($list as &$item) {
  197. $item['store_name'] = $store_list[$item['store_id']]['store_name'] ?? '平台自营';
  198. $distance = $location_obj->getDistanceByGeo($item['latitude'], $item['longitude'],$params['lat'] , $params['lng']);
  199. // $item['distance'] = bcmul(bcmul($distance, 1000, 4), 1, 1); //米
  200. $item['distance'] = LocationHelper::formatDistance($distance); //米
  201. $item['comment_num'] = $comments[$item['user_id']]['total'] ?? 0;
  202. $item['commentNum'] = $comments[$item['user_id']]['total'] ?? 0;
  203. $item['serviceNum'] = $item['order_num'];
  204. $item['detail'] = $item['desc'];
  205. $item['gender_txt'] = ReservationServiceEnum::GENDER_MAP[$item['gender']];
  206. }
  207. if($goods_id && $sku_id){
  208. //默认技师,时间
  209. // $default_jishi = $list[0];
  210. $service_user = new UserService();
  211. foreach ($list as $key1 => $item1) {
  212. $default_jishi = $item1;
  213. $default_jishi_times = $service_user->getWorkingTime($mall_id, $item1['user_id']);
  214. if (!empty($default_jishi_times)) break;
  215. }
  216. unset($list[$key1]);
  217. array_unshift($list, $default_jishi);
  218. $time1 = date('Y-m-d 00:00:00');
  219. $time2 = date('Y-m-d 23:59:59');
  220. $buff = [];
  221. $service_min = $service_min <= 0 ? 30 : $service_min;
  222. for($i=strtotime($time1) ;$i<=strtotime($time2) ; $i=$i+60*$service_min){
  223. $buff[]= ['time' => date("H:i",$i),'time_num' => 100 * date("H",$i)+date("i",$i)];
  224. }
  225. $data = BackendService::getSetting($mall_id);
  226. $range_start_time = self::getRangeStartTime($data['basic']['range_start'] ?? 0);
  227. $time = time();
  228. $today_day = date('Y-m-d', $time);
  229. if ($range_start_time) {
  230. $time += $range_start_time;
  231. }
  232. //最近7天是否可约
  233. for($i = 0;$i < 7;$i++){
  234. $w = date('w',strtotime("+$i days"));
  235. $date = date('Y-m-d',strtotime("+$i days"));
  236. $day_times = $default_jishi_times[$w] ?? [];
  237. if($day_times){
  238. foreach($day_times as $day_time_info){
  239. foreach($buff as $one_buff_key => $one_buff){
  240. if ($today_day == $date) {
  241. $temp = strtotime($date . ' ' . $one_buff['time'] . ':00');
  242. if ($temp < $time) {
  243. continue;
  244. }
  245. }
  246. $next_buff = $buff[$one_buff_key + 1] ?? [];
  247. if($day_time_info['start_time'] >= $one_buff['time_num'] && $next_buff && $day_time_info['start_time'] < $next_buff['time_num']){
  248. //check
  249. $end_timestamp = strtotime($date.' '.$one_buff['time']) + 60 * $service_min;
  250. $check = self::checkCanBook($mall_id,[$default_jishi['user_id']],strtotime($date.' '.$one_buff['time']),$end_timestamp);
  251. if(!$check) continue;
  252. // 如果是今天
  253. $now_time_number = intval(date('Hi'));
  254. if ($date == date('Y-m-d') && ($day_time_info['end_time']) < $now_time_number) continue;
  255. //符合
  256. $min = intval(date('i'));
  257. $hour = intval(date('H'));
  258. $t = $one_buff['time_num'] > $now_time_number ? $one_buff['time'] : ($min >= 30 ? sprintf("%s:00", $hour+1) : sprintf("%s:30", $hour));
  259. if ($one_buff['time_num'] > $now_time_number && $hour == 24 && $min >= 30) {
  260. $date = strtotime("+1 days", strtotime($date));
  261. $t = "00:00";
  262. }
  263. $default_jishi['lately_time_arr'] = [
  264. 'date' => $date,
  265. 'time' => $t,
  266. 'week_day' => GoodsReserveTime::getWeekDay($date)
  267. ];
  268. break 3;
  269. }
  270. }
  271. }
  272. }
  273. }
  274. }
  275. }
  276. $list_page['list'] = $list;
  277. $list_page['page'] = $page;
  278. $list_page['page_size'] = $limit;
  279. $list_page['count'] = $count;
  280. $list_page['page_count'] = ceil($count / $limit);
  281. $list_page['default_jishi'] = $default_jishi;
  282. return $list_page;
  283. }catch(\Exception $e){
  284. $list_page['list'] = [];
  285. $list_page['page'] = 0;
  286. $list_page['page_size'] = 10;
  287. $list_page['count'] = 0;
  288. $list_page['page_count'] = 0;
  289. $list_page['error_msg'] = $e->getMessage();
  290. return $list_page;
  291. }
  292. }
  293. /**
  294. * 获取该时间内的有门店排班的技师user_id
  295. * @param $mall_id
  296. * @param $select_date
  297. * @param $select_time
  298. * @return array
  299. */
  300. public static function getSchedulingUserIds($mall_id, $select_date, $select_time)
  301. {
  302. $select_time = strtotime($select_date . ' ' . $select_time . ':00');
  303. $date_time = strtotime($select_date . ' 00:00:00');
  304. $scheduling_user_ids = [];//在门店排班内的技师user_id
  305. if (MallAddons::isInstall($mall_id, 'Store')) {
  306. $scheduling_list = StoreReserveTechnicianScheduling::lists([
  307. 'where' => [
  308. ['mall_id' => $mall_id],
  309. ['status' => StatusEnum::ENABLED],
  310. ['date_time' => $date_time],
  311. ['>', 'user_id', 0],
  312. ],
  313. 'with' => ['schedulingTime'],
  314. ]);
  315. if (!empty($scheduling_list)) {
  316. $store_ids = [];
  317. foreach ($scheduling_list as $scheduling) {
  318. if ($scheduling['is_all_day'] == 1) {
  319. $store_ids[] = $scheduling['store_id'];
  320. }
  321. }
  322. if (!empty($store_ids)) {
  323. $store_list = Store::lists([
  324. 'where' => [
  325. ['mall_id' => $mall_id],
  326. ['id' => $store_ids],
  327. ],
  328. 'index' => 'id'
  329. ]);
  330. }
  331. foreach ($scheduling_list as $scheduling) {
  332. if ($scheduling['is_all_day'] == 1) {
  333. $store_info = $store_list[$scheduling['store_id']] ?? [];
  334. $business_time_start = $store_info['business_time_start'] ?? '';
  335. $business_time_end = $store_info['business_time_end'] ?? '';
  336. $business_time_start = strtotime($select_date . ' ' . $business_time_start . ':00');
  337. $business_time_end = strtotime($select_date . ' ' . $business_time_end . ':00');
  338. if ($select_time >= $business_time_start && $select_time <= $business_time_end) {
  339. $scheduling_user_ids[] = $scheduling['user_id'];
  340. }
  341. } else {
  342. if (!empty($scheduling['schedulingTime'])) {
  343. foreach ($scheduling['schedulingTime'] as $val) {
  344. if ($select_time >= $val['start_time'] && $select_time <= $val['end_time']) {
  345. $scheduling_user_ids[] = $scheduling['user_id'];
  346. break;
  347. }
  348. }
  349. }
  350. }
  351. }
  352. }
  353. }
  354. return $scheduling_user_ids;
  355. }
  356. public static function getRangeStartTime($range_start)
  357. {
  358. $range_start_time = 0;
  359. switch ($range_start) {
  360. case 2:
  361. $range_start_time = 30 * 60;
  362. break;
  363. case 3:
  364. $range_start_time = 60 * 60;
  365. break;
  366. }
  367. return $range_start_time;
  368. }
  369. public static function searchBySecondaryCard($params,$page,$limit){
  370. try{
  371. $mall_id = $params['mall_id'];
  372. if(!$params['lat'] || !$params['lng']){
  373. throw new \Exception('经纬度缺失');
  374. }
  375. if(!MallAddons::isInstall($mall_id,'SecondaryCard')){
  376. throw new \Exception('没有安装次卡插件');
  377. }
  378. $select = 'id,reservation_time_type,reservation_time,store_type,use_type,reservation_model';
  379. $secondary_card = SecondaryCard::getOne([['id'=>$params['secondary_card_id']]],true,[],false,$select);
  380. switch ($secondary_card['store_type']){
  381. case 1:
  382. if (!MallAddons::isInstall($mall_id, 'Store')) {
  383. throw new \Exception('没有安装o2o门店插件');
  384. }
  385. break;
  386. }
  387. if (!empty($params['store_id'] && $secondary_card['reservation_model'] == 2)) {
  388. $store_reserve_setting = (new StoreReserveSettingService(['mall_id' => $mall_id, 'store_id' => $params['store_id']]))->detail();
  389. $employee_selection = $store_reserve_setting['employee_selection'] ?? 1;
  390. if ($employee_selection == 3) {
  391. $list_page['list'] = [];
  392. $list_page['page'] = 0;
  393. $list_page['page_size'] = 10;
  394. $list_page['count'] = 0;
  395. $list_page['page_count'] = 0;
  396. $list_page['error_msg'] = '技师不可选';
  397. return $list_page;
  398. }
  399. }
  400. if($secondary_card['use_type'] != SecondaryCardEnum::USE_TYPE_ALL){
  401. $secondary_card_relate_store_list = SecondaryCardRelateStore::lists([
  402. 'where'=>[
  403. ['mall_id'=>$mall_id],
  404. ['secondary_card_id'=>$params['secondary_card_id']],
  405. ['status'=>StatusEnum::ENABLED]
  406. ]
  407. ]);
  408. $store_id_arr = array_column($secondary_card_relate_store_list,'store_id');
  409. }
  410. $longitude= $params['lng'];
  411. $latitude= $params['lat'];
  412. $use_type = $secondary_card['use_type'];
  413. switch ($secondary_card['store_type']) {
  414. //o2o门店
  415. case 1:
  416. //预约模式:1:预约门店;2:预约服务人
  417. switch ($secondary_card['reservation_model']) {
  418. case 1:
  419. $offset = ($page - 1) * $limit;
  420. $sql = " SELECT id,store_name,shop_mobile,latitude,longitude,logo_img,sales_volume,praise,address,business_time_start,business_time_end,
  421. round(
  422. 2 * 6378.137 * ASIN(
  423. SQRT(
  424. POW(
  425. SIN(
  426. PI() * ({$longitude} - longitude) / 360
  427. ),
  428. 2
  429. ) + COS(PI() * {$latitude} / 180) * COS(latitude * PI() / 180) * POW(
  430. SIN(
  431. PI() * ({$latitude} - latitude) / 360
  432. ),
  433. 2
  434. )
  435. )
  436. )
  437. ,1) AS distance
  438. FROM `qimall_addons_store` WHERE (`mall_id`={$mall_id})";
  439. if (!empty($use_type)) {
  440. if (!empty($store_id_arr)) {
  441. $o2o_store_ids = join(',', $store_id_arr);
  442. } else {
  443. $o2o_store_ids = 0;
  444. }
  445. $sql .= " AND (`id` in ({$o2o_store_ids})) ";
  446. }
  447. if(!empty($params['store_cate_id'])){
  448. $sql .= " AND `c_id` = ".$params['store_cate_id'];
  449. }
  450. if(!empty($params['name'])){
  451. $sql .= " AND `store_name` like '%".$params['name']."%' ";
  452. }
  453. $count_sql = $sql;
  454. $total_data = \Yii::$app->db->createCommand($count_sql)->queryAll();
  455. $count = count($total_data);
  456. $orderBy = 'distance asc,id desc '; //1-距离
  457. if (isset($params['sort']) && $params['sort'] > 1){
  458. //1-距离,2-订单量 order_num ,3-好评 service_rating
  459. if($params['sort'] == 1){
  460. $orderBy = 'distance asc'; //1-距离
  461. }else{
  462. $orderBy = $params['sort'] == 2 ? 'sales_volume desc' : 'praise desc ';
  463. }
  464. }
  465. $sql .= " ORDER BY {$orderBy} limit {$limit} offset {$offset} ";
  466. $list = \Yii::$app->db->createCommand($sql)->queryAll();
  467. $list_page['list'] = $list;
  468. $list_page['page'] = $page;
  469. $list_page['page_size'] = $limit;
  470. $list_page['count'] = $count;
  471. $list_page['page_count'] = ceil($count / $limit);
  472. return $list_page;
  473. case 2:
  474. break;
  475. }
  476. break;
  477. }
  478. $basic = \Yii::$app->services->setting->addons->get($mall_id, ReservationServiceEnum::ADDONS_NAME, 'basic') ?? [];
  479. $max_distance = $basic['max_distance'] ?? 20; //限制
  480. $is_working = $params['is_working'] ?? 1;
  481. $working_field = empty($params['store_id']) ? 'is_working' : 'store_status';
  482. $where[] = ['mall_id' => $params['mall_id'],'is_working'=> 1,'status' => StatusEnum::ENABLED,'check_status'=>StatusEnum::ENABLED ];
  483. $min_age = $params['min_age'] ?? 0;
  484. $max_age = $params['max_age'] ?? 0;
  485. $gender = $params['gender'] ?? 0;
  486. $name = $params['name'] ?? '';
  487. $service_min = $secondary_card['appointment_interval_duration'] ?? 30;
  488. $user_ids = [];
  489. $longitude = 'longitude';
  490. $latitude = 'latitude';
  491. $s_longitude = 'longitude';
  492. $s_latitude = 'latitude';
  493. if (!empty($params['store_id']) || $secondary_card['use_type'] == SecondaryCardEnum::USE_TYPE_ALL) {
  494. $date = $params['date'] . ' ' . $params['time'] . ':00';
  495. $start_time = strtotime($date);
  496. $end_time = $start_time + $service_min * 60;
  497. // 取出门店下面的技师
  498. $where = ['mall_id' => $params['mall_id'],'status'=>StatusEnum::ENABLED];
  499. $where['store_id'] = $params['store_id'];
  500. $where['secondary_card_id'] = $params['secondary_card_id'];
  501. $user_ids = ReservationServiceStoreSecondaryCard::find()->where($where)->select('user_id')->column();
  502. if (empty($user_ids)) {
  503. throw new \Exception("当前门店暂无技师");
  504. }
  505. $longitude = 'store_lng';
  506. $latitude = 'store_lat';
  507. $s_longitude = 'store_lng as longitude';
  508. $s_latitude = 'store_lat as latitude';
  509. $new_user_ids = self::getNewUserIds($user_ids, $params['date'], $start_time, $end_time);
  510. $reserve_user_ids = self::getReserveUserIds($mall_id, $params['store_id'], $date);
  511. $occupy_user_ids = self::getOccupyUserIds($mall_id, $params['store_id'], $start_time, $end_time);
  512. $user_ids = array_diff($new_user_ids, $reserve_user_ids, $occupy_user_ids);
  513. }
  514. if (empty($user_ids)) {
  515. throw new \Exception('无可预约技师');
  516. }
  517. $sql = "SELECT id,mall_id,user_id,`level`, {$s_longitude}, {$s_latitude},`name`,service_rating,order_num,gender,age,`desc`,avatar,store_id, address,
  518. round(
  519. 2 * 6378.137 * ASIN(
  520. SQRT(
  521. POW(
  522. SIN(
  523. PI() * ({$params['lng']} - {$longitude}) / 360
  524. ),
  525. 2
  526. ) + COS(PI() * {$params['lat']} / 180) * COS({$latitude} * PI() / 180) * POW(
  527. SIN(
  528. PI() * ({$params['lat']} - {$latitude}) / 360
  529. ),
  530. 2
  531. )
  532. )
  533. )
  534. ,1) AS distance
  535. FROM `qimall_addons_reservation_service_user` WHERE (`mall_id`={$mall_id}) AND (`status`=1) AND (`check_status`= 1) AND (`{$working_field}` ={$is_working}) HAVING (`distance`<={$max_distance})";
  536. if ($gender > 0){
  537. $gender = $gender > 1 ? 2: 1;
  538. $sql .= " AND `gender` = " . $gender;
  539. }
  540. if ($min_age > 0 || $max_age > 0){
  541. $sql .= " AND `age` >= " . $min_age;
  542. $sql .= " AND `age` <= " . $max_age;
  543. }
  544. if($name){
  545. $name = trim($name);
  546. $sql .= " AND `name` LIKE '%" . $name . "%'";
  547. }
  548. $sql .= " AND `user_id` in (" . implode(',', $user_ids) . ")";
  549. if (!empty($params['store_id'])) {
  550. $sql .= " AND `store_id` = " . $params['store_id'];
  551. }
  552. $count_sql = $sql;
  553. $orderBy = 'distance asc,id desc'; //1-距离
  554. if (isset($params['sort']) && $params['sort'] > 1){
  555. //1-距离,2-订单量 order_num ,3-好评 service_rating
  556. if($params['sort'] == 1){
  557. $orderBy = 'distance asc'; //1-距离
  558. }else{
  559. $orderBy = $params['sort'] == 2 ? 'order_num desc' : 'service_rating desc';
  560. }
  561. }
  562. $total_data = \Yii::$app->db->createCommand($count_sql)->queryAll();
  563. $count = count($total_data);
  564. $sql .= " ORDER BY {$orderBy}";
  565. $sql .= " limit " . ($page - 1) * $limit . ",{$limit}";
  566. $list = \Yii::$app->db->createCommand($sql)->queryAll();
  567. $default_jishi = [];
  568. if (!empty($list)) {
  569. $location_obj = new LocationHelper();
  570. $store_ids = array_column($list,'store_id');
  571. $store_ids = array_unique($store_ids);
  572. $store_list = Store::find()->where(['id' => $store_ids,'mall_id' => $mall_id])->select('id,store_name')->indexBy('id')->asArray()->all();
  573. $comments = OrderReserveComments::lists([
  574. 'where' => [
  575. ['tech_user_id' => array_column($list, 'user_id')],
  576. ['reserve_type' => StatusEnum::ENABLED]
  577. ],
  578. 'select'=> 'COUNT(*) as total, tech_user_id',
  579. 'group' => 'tech_user_id',
  580. 'index' => 'tech_user_id'
  581. ]);
  582. foreach ($list as &$item) {
  583. $item['store_name'] = $store_list[$item['store_id']]['store_name'] ?? '平台自营';
  584. $distance = $location_obj->getDistanceByGeo($item['latitude'], $item['longitude'],$params['lat'] , $params['lng']);
  585. $item['distance'] = LocationHelper::formatDistance($distance); //米
  586. $item['comment_num'] = $comments[$item['user_id']]['total'] ?? 0;
  587. $item['commentNum'] = $comments[$item['user_id']]['total'] ?? 0;
  588. $item['serviceNum'] = $item['order_num'];
  589. $item['detail'] = $item['desc'];
  590. }
  591. }
  592. $list_page['list'] = $list;
  593. $list_page['page'] = $page;
  594. $list_page['page_size'] = $limit;
  595. $list_page['count'] = $count;
  596. $list_page['page_count'] = ceil($count / $limit);
  597. $list_page['default_jishi'] = $default_jishi;
  598. return $list_page;
  599. }catch(\Exception $e){
  600. $list_page['list'] = [];
  601. $list_page['page'] = 0;
  602. $list_page['page_size'] = 10;
  603. $list_page['count'] = 0;
  604. $list_page['page_count'] = 0;
  605. $list_page['error_msg'] = $e->getMessage();
  606. return $list_page;
  607. }
  608. }
  609. /**
  610. * 获取在排班时间内的技师user_id
  611. * @param $user_ids
  612. * @param $date
  613. * @param $start_time
  614. * @param $end_time
  615. * @return array
  616. */
  617. protected static function getNewUserIds($user_ids, $date, $start_time, $end_time)
  618. {
  619. $day_start_time = strtotime($date . ' 00:00:00');
  620. $new_user_ids = [];
  621. $user_ids_all = StoreReserveTechnicianScheduling::find()->where(['user_id' => $user_ids, 'is_all_day' => 1, 'date_time' => $day_start_time, 'status' => StatusEnum::ENABLED])
  622. ->select('user_id')->column();
  623. $new_user_ids = array_merge($new_user_ids, $user_ids_all);
  624. $scheduling_list = StoreReserveTechnicianScheduling::find()->where(['user_id' => $user_ids, 'is_all_day' => 0, 'date_time' => $day_start_time, 'status' => StatusEnum::ENABLED])
  625. ->with(['schedulingTime'])->all();
  626. foreach ($scheduling_list as $val) {
  627. foreach ($val['schedulingTime'] as $v) {
  628. if (($v['start_time'] <= $start_time && $start_time <= $v['end_time']) && ($v['start_time'] <= $end_time)) {
  629. $new_user_ids[] = $val['user_id'];
  630. break;
  631. }
  632. }
  633. }
  634. return $new_user_ids;
  635. }
  636. /**
  637. * 获取已预约的技师user_id
  638. * @param $mall_id
  639. * @param $store_id
  640. * @param $date
  641. * @return array
  642. */
  643. protected static function getReserveUserIds($mall_id, $store_id, $date)
  644. {
  645. $store_order_reserve_list = StoreOrderReserve::find()->where(['store_id' => $store_id, 'mall_id' => $mall_id])
  646. ->andWhere(['!=', 'order_status', -4])
  647. ->andWhere(['>=', 'service_start_at', time()])
  648. ->all();
  649. $tech_id_ids = [];
  650. $date_time = strtotime($date);
  651. foreach ($store_order_reserve_list as $value) {
  652. if ($value['service_start_at'] <= $date_time && $value['service_end_at'] > $date_time) {
  653. $tech_id_ids[] = $value['tech_id'];
  654. }
  655. }
  656. $reserve_user_ids = ReservationServiceUser::find()->where(['id' => $tech_id_ids])->select('user_id')->column();//已被预约的技师id
  657. return $reserve_user_ids;
  658. }
  659. /**
  660. * 获取已占用的技师user_id
  661. * @param $mall_id
  662. * @param $store_id
  663. * @param $start_time
  664. * @param $end_time
  665. * @return array
  666. */
  667. protected static function getOccupyUserIds($mall_id, $store_id, $start_time, $end_time)
  668. {
  669. $occupy_list = ReservationServiceOccupy::lists([
  670. 'where' => [
  671. ['mall_id' => $mall_id],
  672. ['store_id' => $store_id],
  673. ['status' => StatusEnum::ENABLED],
  674. ]
  675. ]
  676. );//已占用时间
  677. $occupy_user_ids = [];
  678. foreach ($occupy_list as $val) {
  679. if ($start_time >= $val['start_at'] && $start_time <= $val['end_at'] && $end_time <= $val['end_at']) {
  680. $occupy_user_ids[] = $val['staff_user_id'];
  681. }
  682. }
  683. return $occupy_user_ids;
  684. }
  685. /**
  686. * 检测用户时间段内是否可约
  687. * @param $mall_id
  688. * @param $user_ids
  689. * @param $start_time
  690. * @param $end_time
  691. * @return array
  692. */
  693. public static function checkCanBook($mall_id,$user_ids,$start_time,$end_time)
  694. {
  695. $book_user_ids = ReservationServiceUserBookTime::find()->where(['mall_id' => $mall_id,'user_id' => $user_ids,'status'=>StatusEnum::ENABLED])
  696. ->andWhere(['or',['and',['>','start_time',$start_time],['<','start_time',$end_time]],['and',['>','end_time',$start_time],['<','end_time',$end_time]]])
  697. ->select('user_id')
  698. ->column();
  699. if($book_user_ids){
  700. $user_ids = array_diff($user_ids,$book_user_ids);
  701. }
  702. return $user_ids;
  703. }
  704. /**
  705. * 检测技师是否可预约
  706. * @param $mall_id
  707. * @param $tech_id
  708. * @param string $select_time 预约日期时间
  709. * @param $goods_id
  710. * @param $sku_id
  711. * @param $lat
  712. * @param $lng
  713. * @param int $store_id
  714. * @return bool
  715. */
  716. public static function checkCanOrder($mall_id,$tech_id,$select_time,$goods_id,$sku_id,$lat,$lng,$store_id = 0, $addon = '')
  717. {
  718. try{
  719. $user_service = new UserService();
  720. $tech = $user_service->fetchById($mall_id,$tech_id);
  721. if(!is_array($tech)) throw new \Exception($tech);
  722. if(!$tech['is_working']) throw new \Exception('当前技师尚未开启接单');
  723. $basic = \Yii::$app->services->setting->addons->get($mall_id, ReservationServiceEnum::ADDONS_NAME, 'basic') ?? [];
  724. $max_distance = $basic['max_distance'] ?? 20; //限制
  725. $location_obj = new LocationHelper();
  726. $distance = $location_obj->getDistanceByGeo($tech['latitude'], $tech['longitude'],$lat , $lng);
  727. if($distance > bcmul($max_distance, 1000, 2)) throw new \Exception('超出技师接单范围,请重新选择');
  728. //时间判断
  729. $select_timestamp = strtotime($select_time);
  730. if (!empty($store_id)) {
  731. switch ($addon) {
  732. case AddonsEnum::ADDONS_NAME_LAO_DA_FU:
  733. $support = \addons\LaoDaFu\common\models\StoreGoods::find()->where(['mall_id' => $mall_id])
  734. ->andWhere(['id' => $goods_id])
  735. ->andWhere(['status' => StatusEnum::ENABLED])
  736. ->andWhere(['goods_type' => GoodsTypeEnum::GoodsTypeReserved])->asArray()->one();
  737. if(!$support) throw new \Exception('当前技师不支持该项目');
  738. $sku = \addons\LaoDaFu\common\models\StoreGoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  739. break;
  740. default:
  741. $support = StoreGoods::find()->where(['mall_id' => $mall_id])
  742. ->andWhere(['id' => $goods_id])
  743. ->andWhere(['status' => StatusEnum::ENABLED])
  744. ->andWhere(['store_id' => $store_id])
  745. ->andWhere(['goods_type' => GoodsTypeEnum::GoodsTypeReserved])->asArray()->one();
  746. if(!$support) throw new \Exception('当前技师不支持该项目');
  747. $sku = StoreGoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  748. $service_min = $sku['service_min'] ?? 0; //服务分钟
  749. $select_timestamp_end = $select_timestamp + 60 * $service_min;
  750. $reserve_date_time = $select_time . ',' . date('Y-m-d H:i', $select_timestamp_end);
  751. $res = StoreOrderReserve::find()->where(['tech_id' => $tech['id'], 'reserve_date_time' => $reserve_date_time])
  752. ->andWhere(['!=', 'order_status', -4])
  753. ->one();
  754. if ($res) throw new \Exception('该时间段已有预定');
  755. }
  756. } else {
  757. $support =ReservationServiceGoods::find()->where(['mall_id' => $mall_id,'goods_id' => $goods_id,
  758. 'status'=>StatusEnum::ENABLED,'user_id' => $tech['user_id']])->asArray()->one();
  759. if(!$support) throw new \Exception('当前技师不支持该项目');
  760. $sku = GoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  761. $service_min = $sku['service_min'] ?? 0; //服务分钟
  762. $select_timestamp_end = $select_timestamp + 60 * $service_min;
  763. $has_book = ReservationServiceUserBookTime::find()->where(['mall_id' => $mall_id, 'user_id' => $tech['user_id'], 'status' => StatusEnum::ENABLED])
  764. ->andWhere(['or', ['and', ['>', 'start_time', $select_timestamp], ['<', 'start_time', $select_timestamp_end]], ['and', ['>', 'end_time', $select_timestamp], ['<', 'end_time', $select_timestamp_end]]])
  765. ->all();
  766. if ($has_book) throw new \Exception('该时间段已有预定');
  767. }
  768. return true;
  769. }catch(\Exception $e){
  770. self::$static_error = $e->getMessage();
  771. return false;
  772. }
  773. }
  774. /**
  775. * 获取技师可预约时间
  776. * @param array $tech
  777. * @return array|string
  778. */
  779. public static function getTechLatelyCanReserveAt(array $tech)
  780. {
  781. $work_times = ReservationServiceWorkTime::lists([
  782. 'where' => [
  783. ['user_id' => $tech['user_id']],
  784. ['status' => StatusEnum::ENABLED]
  785. ],
  786. 'select'=> 'week_day, text',
  787. 'order' => 'week_day'
  788. ]);
  789. $times = [];
  790. for ($i = 0; $i < 1; $i++) {
  791. $day_time = time() + (86400 * $i);
  792. $week = date('w', $day_time);
  793. if (!empty($work_times[$week])) {
  794. $ts = explode("-", $work_times[$week]['text']);
  795. $day = date('Y-m-d', $day_time);
  796. $t_s = strtotime("{$day} {$ts[0]}:00");
  797. $t_e = strtotime("{$day} {$ts[1]}:00");
  798. // 这个时间段是否可约
  799. $select_timestamp = $t_s;
  800. $select_timestamp_end = $t_e;
  801. $has_book = ReservationServiceUserBookTime::find()->where(['user_id' => $tech['user_id'],'status'=>StatusEnum::ENABLED])
  802. ->andWhere(['or',['and',['>','start_time',$select_timestamp],['<','start_time',$select_timestamp_end]],['and',['>','end_time',$select_timestamp],['<','end_time',$select_timestamp_end]]])
  803. ->all();
  804. if(empty($has_book)) {
  805. return DateHelper::dayChineseText(strtotime($day));
  806. }
  807. }
  808. }
  809. return null;
  810. }
  811. public static function checkDate($params,$page=1,$limit=1)
  812. {
  813. try{
  814. if(!$params['lat'] || !$params['lng']){
  815. throw new \Exception('经纬度缺失');
  816. }
  817. $mall_id = $params['mall_id'];
  818. $basic = \Yii::$app->services->setting->addons->get($mall_id, ReservationServiceEnum::ADDONS_NAME, 'basic') ?? [];
  819. $max_distance = $basic['max_distance'] ?? 20; //限制
  820. $is_working = $params['is_working'] ?? 1;
  821. $working_field = empty($params['store_id']) ? 'is_working' : 'store_status';
  822. $goods_id = $params['goods_id'] ?? 0;
  823. $sku_id = $params['sku_id'] ?? 0;
  824. $user_ids = [];
  825. $longitude = 'longitude';
  826. $latitude = 'latitude';
  827. $s_longitude = 'longitude';
  828. $s_latitude = 'latitude';
  829. if($goods_id && $sku_id){
  830. if (!empty($params['store_id'])) {
  831. // 取出门店下面的技师
  832. $user_ids = ReservationServiceUser::find()->where(['mall_id' => $params['mall_id'], 'store_id' => $params['store_id'],'status'=>StatusEnum::ENABLED])->select('user_id')->column();
  833. if (empty($user_ids)) throw new \Exception("当前门店暂无技师");
  834. $sku = StoreGoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  835. $longitude = 'store_lng';
  836. $latitude = 'store_lat';
  837. $s_longitude = 'store_lng as longitude';
  838. $s_latitude = 'store_lat as latitude';
  839. } else {
  840. $support_user_list = ReservationServiceGoods::find()->where(['mall_id' => $params['mall_id'],'goods_id' => $goods_id,'status'=>StatusEnum::ENABLED])->select('user_id')->asArray()->all();
  841. $user_ids = array_column($support_user_list,'user_id');
  842. $user_ids = array_unique($user_ids);
  843. if(!$user_ids) throw new \Exception('没有技师支持该项目');
  844. $sku = GoodsReserveSku::findOne(['id' => $sku_id,'goods_id' => $goods_id]);
  845. }
  846. $select_date = $params['date'] ?? date('Y-m-d');
  847. $start_time = strtotime($select_date.' 00:00:00');
  848. $end_time = strtotime($select_date.' 23:59:59');
  849. $select_week_day = date('w',strtotime($select_date));
  850. $user_times = ReservationServiceWorkTime::find()->where(['mall_id' => $mall_id,'user_id' => $user_ids,'status'=>StatusEnum::ENABLED])
  851. ->andWhere(['week_day' => $select_week_day])
  852. ->andWhere(['<=','start_time',$start_time])
  853. ->andWhere(['>=','end_time',$end_time])
  854. ->select('user_id')
  855. ->asArray()->all();
  856. if(!$user_times) throw new \Exception('暂无预约');
  857. $user_ids = array_column($user_times,'user_id');
  858. //排除当前有项目的技师
  859. $user_ids = self::checkCanBook($mall_id,$user_ids,$start_time,$end_time);
  860. if(!$user_ids) throw new \Exception('暂无预约');
  861. }
  862. $sql = "SELECT id,mall_id,user_id,`level`, {$s_longitude}, {$s_latitude},`name`,service_rating,order_num,gender,age,`desc`,avatar,store_id, address,
  863. round(
  864. 2 * 6378.137 * ASIN(
  865. SQRT(
  866. POW(
  867. SIN(
  868. PI() * ({$params['lng']} - {$longitude}) / 360
  869. ),
  870. 2
  871. ) + COS(PI() * {$params['lat']} / 180) * COS({$latitude} * PI() / 180) * POW(
  872. SIN(
  873. PI() * ({$params['lat']} - {$latitude}) / 360
  874. ),
  875. 2
  876. )
  877. )
  878. )
  879. ,1) AS distance
  880. FROM `qimall_addons_reservation_service_user` WHERE (`mall_id`={$mall_id}) AND (`status`=1) AND (`is_working`=1) AND (`check_status`= 1) AND (`{$working_field}` ={$is_working}) HAVING (`distance`<={$max_distance})";
  881. if($user_ids){
  882. $sql .= " AND `user_id` in (" . implode(',',$user_ids) . ")";
  883. }
  884. $sql .= " limit " . ($page - 1) * $limit . ",{$limit}";
  885. $list = \Yii::$app->db->createCommand($sql)->queryAll();
  886. if(empty($list)) throw new \Exception('暂无预约');
  887. return true;
  888. }catch(\Exception $e){
  889. throw new \Exception($e->getMessage());
  890. }
  891. }
  892. /**
  893. * 获取技师某日的时间段
  894. * @param $params
  895. * @return array
  896. * @throws \Exception
  897. */
  898. public static function getTimes($params)
  899. {
  900. if(empty($params['tech_user_id'])){
  901. $res = self::search($params,$params['page'],$params['limit']);
  902. $user_ids = [];
  903. if(!empty($res['list'])){
  904. $user_ids = array_column($res['list'],'user_id');
  905. }
  906. }else{
  907. $user_ids = $params['tech_user_id'];
  908. }
  909. $time = time();
  910. if (empty($params['store_id'])) {
  911. $buff = self::platformStaffTimeList($params,$user_ids,$time);
  912. }else{
  913. $buff = self::storeStaffTimeList($params,$user_ids,$time);
  914. }
  915. return $buff;
  916. }
  917. /**
  918. * 获取平台技师某天的工作时间段
  919. * @param $params
  920. * @param $user_ids
  921. * @param $time
  922. * @return array
  923. * @throws \Exception
  924. */
  925. protected static function platformStaffTimeList($params,$user_ids,$time)
  926. {
  927. $buff = [];
  928. $goods_reserve = GoodsReserve::findOne(['goods_id' => $params['goods_id'], 'reserve_type' => GoodsReserve::RESERVE_TYPE_SERVICE]);
  929. if (!$goods_reserve) {
  930. throw new \Exception('项目不存在');
  931. }
  932. $sku = GoodsReserveSku::findOne(['id' => $params['sku_id'], 'goods_id' => $params['goods_id']]);
  933. if (!$sku) {
  934. throw new \Exception('项目不存在');
  935. }
  936. $work_time_list = ReservationServiceWorkTime::lists([
  937. 'where'=>[
  938. ['user_id'=>$user_ids],
  939. ['week_day'=> date('w',strtotime($params['date'].' 00:00:00'))],
  940. ['mall_id'=>$params['mall_id']], ['status'=>StatusEnum::ENABLED]
  941. ]
  942. ]);
  943. if(!empty($params['tech_user_id'])) {
  944. $list = ReservationServiceOrder::lists([
  945. 'where' => [
  946. ['mall_id' => $goods_reserve['mall_id']],
  947. ['staff_user_id' => $user_ids],
  948. ['store_id' => 0],
  949. ['>=','service_start_at',strtotime($params['date'].' 00:00:00')],
  950. ['<=','service_end_at',strtotime($params['date'].' 23:59:59')],
  951. ['!=','service_status',ReservationServiceEnum::RESERVE_ORDER_STATUS_CANCEL]
  952. ]
  953. ]);
  954. $scheduled_time_arr = [];//已预约时间
  955. foreach ($list as $value){
  956. $scheduled_time_arr[date("H:i",$value['service_start_at'])] = 1;
  957. }
  958. }
  959. $service_min = $sku['service_min'] ?: 10; //服务分钟
  960. $time1 = date('Y-m-d 00:00:00');
  961. $time2 = date('Y-m-d 23:59:59');
  962. for($i=strtotime($time1) ;$i<=strtotime($time2) ; $i=$i+60*$service_min){
  963. $is_true = 0;
  964. $temp_time = $i;
  965. foreach ($work_time_list as $item){
  966. $time_arr = explode('-',$item['text']);
  967. $start_time = strtotime($params['date'].' '.$time_arr[0].':00');
  968. $end_time = strtotime($params['date'].' '.$time_arr[1].':00');
  969. if($temp_time>=$start_time && $temp_time<=$end_time){
  970. $is_true = 1;
  971. break;
  972. }
  973. }
  974. if($is_true==1 && $i>$time){
  975. $buff_time = date("H:i",$i);
  976. $is_reserve = 1;
  977. if(!empty($params['tech_user_id']) && isset($scheduled_time_arr[$buff_time])) {
  978. $is_reserve = 0;
  979. }
  980. $buff[]=['time'=>$buff_time,'is_reserve'=>$is_reserve];
  981. }
  982. }
  983. return $buff;
  984. }
  985. /**
  986. * 获取门店技师某天的工作时间段
  987. * @param $params
  988. * @param $user_ids
  989. * @param $time
  990. * @return array
  991. * @throws \Exception
  992. */
  993. protected static function storeStaffTimeList($params,$user_ids,$time)
  994. {
  995. $buff = [];
  996. $goods_reserve = StoreGoodsReserve::findOne(['goods_id' => $params['goods_id'], 'reserve_type' => GoodsReserve::RESERVE_TYPE_SERVICE]);
  997. if (!$goods_reserve) {
  998. throw new \Exception('项目不存在1');
  999. }
  1000. $sku = StoreGoodsReserveSku::findOne(['id' => $params['sku_id'], 'goods_id' => $params['goods_id']]);
  1001. if (!$sku) {
  1002. throw new \Exception('项目不存在');
  1003. }
  1004. if(!empty($params['tech_user_id'])) {
  1005. $reservation_service_order_list = ReservationServiceOrder::lists([
  1006. 'where' => [
  1007. ['mall_id' => $goods_reserve['mall_id']],
  1008. ['staff_user_id' => $user_ids],
  1009. ['store_id' => $params['store_id']],
  1010. ['>=','service_start_at',strtotime($params['date'].' 00:00:00')],
  1011. ['<=','service_end_at',strtotime($params['date'].' 23:59:59')],
  1012. ['!=','server_status',ReservationServiceEnum::CANCELED]
  1013. ]
  1014. ]);
  1015. }
  1016. $scheduling_list = StoreReserveTechnicianScheduling::lists([
  1017. 'where'=>[
  1018. ['store_id'=>$goods_reserve['store_id']], ['user_id'=>$user_ids],
  1019. ['date_time'=>strtotime($params['date'].' 00:00:00')]
  1020. ],
  1021. 'with'=>'schedulingTime'
  1022. ]);
  1023. $occupy_list = ReservationServiceOccupy::lists([
  1024. 'where' => [
  1025. ['mall_id' => $goods_reserve['mall_id']],
  1026. ['store_id' => $params['store_id']],
  1027. ['staff_user_id' => $user_ids],
  1028. ['status'=>StatusEnum::ENABLED]
  1029. ]
  1030. ]
  1031. );//已占用时间
  1032. $service_min = $sku['service_min'] ?: 10; //服务分钟
  1033. $store = Store::findOne(['id'=>$goods_reserve['store_id']]);
  1034. $business_time_start = $store['business_time_start'];
  1035. $business_time_end = $store['business_time_end'];
  1036. $time1 = strtotime($params['date'].' '.$business_time_start.':00');
  1037. $time2 =strtotime($params['date'].' '.$business_time_end.':00');
  1038. for($i=$time1 ;$i<=$time2 ; $i=$i+60*$service_min){
  1039. $is_true = 0;
  1040. $temp_time = $i;
  1041. foreach ($scheduling_list as $item){
  1042. //全天
  1043. if($item['is_all_day']){
  1044. $is_true = 1;
  1045. break;
  1046. }else{
  1047. //设置的时间段
  1048. foreach ($item['schedulingTime'] as $val){
  1049. if($temp_time>=$val['start_time'] && $temp_time<=$val['end_time']){
  1050. $is_true = 1;
  1051. break;
  1052. }
  1053. }
  1054. }
  1055. }
  1056. if($is_true==1 && $i>$time){
  1057. $buff_time = date("H:i",$i);
  1058. $is_reserve = 1;
  1059. if(!empty($params['tech_user_id'])) {
  1060. //判断该时间段是否被人预约
  1061. foreach ($reservation_service_order_list as $service_order) {
  1062. if ($service_order['service_start_at'] <= $temp_time && $service_order['service_end_at'] >= $temp_time) {
  1063. $is_reserve = 0;
  1064. }
  1065. }
  1066. if ($is_reserve == 1) {
  1067. //判断该时间段是否被占用
  1068. foreach ($occupy_list as $occupy) {
  1069. if ($occupy['start_time'] <= $temp_time && $occupy['end_time'] >= $temp_time) {
  1070. $is_reserve = 0;
  1071. }
  1072. }
  1073. }
  1074. }
  1075. $buff[]=['time'=>$buff_time,'is_reserve'=>$is_reserve];
  1076. }
  1077. }
  1078. return $buff;
  1079. }
  1080. public static function getDate($params)
  1081. {
  1082. $data = $params['data'];
  1083. $range_end = $data['basic']['range_end']??1;
  1084. $range_end_arr = ReservationServiceEnum::RANGE_END_ARR_MAP;
  1085. $date_list = [];
  1086. $time = time();
  1087. $i=0;
  1088. $days = $range_end_arr[$range_end]??7;
  1089. while (true){
  1090. if($i>=$days){
  1091. break;
  1092. }
  1093. $temp_time = $time + $i*86400;
  1094. $date = date('Y-m-d',$temp_time);
  1095. $date_list[]=['date'=>$date,'is_reserve'=>1, 'week_day' => GoodsReserveTime::getWeekDay($date)];
  1096. $i++;
  1097. }
  1098. if(!empty($params['start'])){
  1099. $start_time = strtotime($params['start'].' 00:00:00');
  1100. $i=0;
  1101. $temp_date_list = array_column($date_list,null,'date');
  1102. $date_list = [];
  1103. while (true){
  1104. if($i>=$days){
  1105. break;
  1106. }
  1107. $temp_time = $start_time + $i*86400;
  1108. $date = date('Y-m-d',$temp_time);
  1109. $is_reserve = 0;
  1110. if(isset($temp_date_list[$date])){
  1111. $is_reserve = 1;
  1112. }
  1113. $date_list[]=['date'=>$date,'is_reserve'=>$is_reserve, 'week_day' => GoodsReserveTime::getWeekDay($date)];
  1114. $i++;
  1115. }
  1116. }
  1117. return $date_list;
  1118. }
  1119. /**
  1120. * 获取某个技师可以预约日期
  1121. * @param $params
  1122. * @return array|void
  1123. */
  1124. public static function getReservationDate($params)
  1125. {
  1126. if(empty($params['store_id'])){
  1127. $work_time = ReservationServiceWorkTime::lists([
  1128. 'where'=>[
  1129. ['user_id'=>$params['tech_user_id']],
  1130. ['mall_id'=>$params['mall_id']],
  1131. ['status'=>StatusEnum::ENABLED]
  1132. ],
  1133. 'index'=>'week_day'
  1134. ]);
  1135. $data = BackendService::getSetting($params['mall_id']);
  1136. $range_end = $data['basic']['range_end']??1;
  1137. $range_end_arr = ReservationServiceEnum::RANGE_END_ARR_MAP;
  1138. $date_list = [];
  1139. $start_time = time();
  1140. $today_end_time = strtotime(date('Y-m-d 23:59:59'));
  1141. $tomorrow_start_time = strtotime(date('Y-m-d 00:00:00',strtotime('+1 day')));
  1142. $tomorrow_end_time = strtotime(date('Y-m-d 23:59:59',strtotime('+1 day')));
  1143. $after_tomorrow_start_time = strtotime(date('Y-m-d 00:00:00',strtotime('+2 day')));
  1144. $after_tomorrow_end_time = strtotime(date('Y-m-d 23:59:59',strtotime('+2 day')));
  1145. $i=0;
  1146. $week_date_map = ReservationServiceEnum::WEEK_DATE_MAP;
  1147. while (true){
  1148. $range_end_limit = $range_end_arr[$range_end];
  1149. if($i>=$range_end_limit){
  1150. break;
  1151. }
  1152. $time = $start_time + $i*86400;
  1153. $week_day = date('w',$time);
  1154. if(!isset($work_time[$week_day])){
  1155. continue;
  1156. }
  1157. $date = date('m-d',$time);
  1158. if($time<=$today_end_time){
  1159. $text = '今天';
  1160. }elseif($time>=$tomorrow_start_time && $time<=$tomorrow_end_time){
  1161. $text = '明天';
  1162. }elseif($time>=$after_tomorrow_start_time && $time<=$after_tomorrow_end_time){
  1163. $text = '后天';
  1164. }else{
  1165. $text = $week_date_map[$week_day]??'';
  1166. }
  1167. $date_list[]=['date_day'=>[
  1168. 'date'=>$date,'text'=>$text
  1169. ]];
  1170. $i++;
  1171. }
  1172. return $date_list;
  1173. }
  1174. }
  1175. /**
  1176. * 平台预约商品详情-服务时间列表
  1177. * @param $params
  1178. * @return array
  1179. * @throws \Exception
  1180. */
  1181. public static function serviceTimeList($params)
  1182. {
  1183. $reservation_service_user = ReservationServiceUser::findOne(['user_id' => $params['id'], 'status' => StatusEnum::ENABLED]);
  1184. if (empty($reservation_service_user)) {
  1185. throw new \Exception('技师不存在');
  1186. }
  1187. $data = BackendService::getSetting($params['mall_id']);
  1188. $params['data'] = $data;
  1189. $list = self::getDate($params);
  1190. $sku = GoodsReserveSku::findOne(['id' => $params['sku_id']]);
  1191. if (!$sku) {
  1192. throw new \Exception('项目不存在');
  1193. }
  1194. if (!empty($reservation_service_user['store_id'])) {
  1195. $store = Store::findOne(['id' => $reservation_service_user['store_id']]);
  1196. if (!empty($params['start'])) {
  1197. $date = $params['start'];
  1198. } else {
  1199. $date = date('Y-m-d', time());
  1200. }
  1201. $times = strtotime($date);
  1202. $business_time_start = strtotime($date . ' ' . $store['business_time_start'] . ':00');
  1203. $business_time_end = strtotime($date . ' ' . $store['business_time_end'] . ':00');
  1204. $scheduling = StoreReserveTechnicianScheduling::lists([
  1205. 'where' => [
  1206. ['user_id' => $reservation_service_user['user_id']],
  1207. ['>=', 'date_time', $times]
  1208. ],
  1209. 'with' => ['schedulingTime'],
  1210. 'index' => 'date_time'
  1211. ]
  1212. );//技师在门店的排班时间
  1213. }
  1214. $service_min = $sku['service_min'];
  1215. $week_day_arr = [];
  1216. $start_time = 0;
  1217. $end_time = 0;
  1218. $time = time();
  1219. $today = date('Y-m-d',$time);
  1220. foreach ($list as $k=>$item){
  1221. $temp_time = strtotime($item['date'].' 00:00:00');
  1222. if($k==0){
  1223. $start_time =$temp_time;
  1224. }
  1225. $end_time =$temp_time;
  1226. $week_day_arr[]=date('w',$temp_time);
  1227. }
  1228. $work_time_arr = ReservationServiceWorkTime::lists([
  1229. 'where'=>[
  1230. ['user_id'=>$params['id']],
  1231. ['week_day'=> $week_day_arr],
  1232. ['mall_id'=>$params['mall_id']], ['status'=>StatusEnum::ENABLED]
  1233. ],
  1234. ]);
  1235. $work_time_list = [];
  1236. foreach ($work_time_arr as $val) {
  1237. $work_time_list[$val['week_day']][] = $val;
  1238. }
  1239. $order_list = OrderReserve::find()
  1240. ->where(['tech_id' => $reservation_service_user['id'], 'mall_id' => $reservation_service_user['mall_id'],])
  1241. ->andWhere(['!=', 'order_status', -4])
  1242. ->andWhere(['>=', 'service_start_at', time()])
  1243. ->asArray()->all();
  1244. $range_start_time = self::getRangeStartTime($data['basic']['range_start'] ?? 0);
  1245. $time += $range_start_time;
  1246. foreach ($list as &$item) {
  1247. $date_time = strtotime($item['date']);
  1248. $temp_time = $date_time;
  1249. $temp_week_day = date('w',$temp_time);
  1250. $time_list = [];
  1251. if (!empty($reservation_service_user['store_id'])) {
  1252. $business_time_start = strtotime($item['date'] . ' ' . $store['business_time_start'] . ':00');
  1253. $business_time_end = strtotime($item['date'] . ' ' . $store['business_time_end'] . ':00');
  1254. }
  1255. $day_is_reserve = 0;
  1256. if (isset($work_time_list[$temp_week_day])) {
  1257. foreach ($work_time_list[$temp_week_day] as $value) {
  1258. $time_arr = explode('-', $value['text']);
  1259. $work_start_time_temp = strtotime($item['date'] . ' ' . $time_arr[0] . ':00');
  1260. $work_end_time_temp = strtotime($item['date'] . ' ' . $time_arr[1] . ':00');
  1261. $work_start_time = strtotime($item['date'] . '00:00:00');
  1262. $work_end_time = strtotime($item['date'] . '23:59:59');
  1263. for ($i = $work_start_time; $i <= $work_end_time; $i = $i + 60 * $service_min) {
  1264. if ($today == $item['date']) {
  1265. if ($i < $time) {
  1266. continue;
  1267. }
  1268. }
  1269. if (!empty($reservation_service_user['store_id'])) {
  1270. if (isset($scheduling) && isset($scheduling[$work_start_time])) {
  1271. if (!empty($scheduling[$work_start_time]['is_all_day'])) {
  1272. if ($i >= $business_time_start && $i <= $business_time_end) {
  1273. $time_list[$i] = ['time' => date("H:i", $i), 'is_reserve' => 0];
  1274. continue;
  1275. }
  1276. } else {
  1277. $is_continue = false;
  1278. if (isset($scheduling[$work_start_time]['schedulingTime'])) {
  1279. foreach ($scheduling[$work_start_time]['schedulingTime'] as $val) {
  1280. $te_start_time = $val['start_time'];
  1281. $te_end_time = $val['end_time'];
  1282. if ($te_start_time < $business_time_start) {
  1283. $te_start_time = $business_time_start;
  1284. }
  1285. if ($te_end_time > $business_time_end) {
  1286. $te_end_time = $business_time_end;
  1287. }
  1288. if ($i >= $te_start_time && $i <= $te_end_time) {
  1289. $is_continue = true;
  1290. }
  1291. }
  1292. }
  1293. if ($is_continue) {
  1294. $time_list[$i] = ['time' => date("H:i", $i), 'is_reserve' => 0];
  1295. continue;
  1296. }
  1297. }
  1298. }
  1299. }
  1300. if ($i < $work_start_time_temp || $i > $work_end_time_temp) {
  1301. continue;
  1302. }
  1303. if ($item['date'] == $today) {
  1304. if ($i < $time) {
  1305. $time_list[$i] = ['time' => date("H:i", $i), 'is_reserve' => 0];
  1306. continue;
  1307. }
  1308. }
  1309. $buff_time = date("H:i", $i);
  1310. $is_reserve = 1;
  1311. if (!empty($order_list)) {
  1312. foreach ($order_list as $v) {
  1313. if ($i >= $v['service_start_at'] && $i < $v['service_end_at']) {
  1314. $is_reserve = 0;
  1315. break;
  1316. }
  1317. }
  1318. }
  1319. if ($is_reserve) {
  1320. $day_is_reserve = 1;
  1321. }
  1322. $time_list[$i] = ['time' => $buff_time, 'is_reserve' => $is_reserve];
  1323. }
  1324. }
  1325. }
  1326. sort($time_list);
  1327. $item['time_list'] = $time_list;
  1328. $item['is_reserve'] = $day_is_reserve;
  1329. }
  1330. return $list;
  1331. }
  1332. /**
  1333. * h5端-预约时间列表-到店分配/技师不选项
  1334. * @param $params
  1335. * @return array
  1336. * @throws \Exception
  1337. */
  1338. public static function getReservationTimeList(int $mall_id, $params)
  1339. {
  1340. $config = BackendService::getSetting($mall_id);
  1341. $range_start = $config['range_start'];
  1342. $range_end = $config['range_end'];
  1343. $store_goods_reserve_sku = GoodsReserveSku::getOne([
  1344. ['id' => $params['sku_id']]
  1345. ], true, ['reserve']);
  1346. if (empty($store_goods_reserve_sku)) {
  1347. throw new \Exception('商品不存在');
  1348. }
  1349. $service_min = self::getServiceMin(1, $store_goods_reserve_sku['service_min'] ?? 0);
  1350. if (empty($service_min)) {
  1351. throw new \Exception('该商品没有设置服务时长');
  1352. }
  1353. $date = $params['date'];
  1354. $appointments_number = $store_goods_reserve_sku['reserve']['appointments_number'] ?? 0;
  1355. if (!empty($appointments_number)) {
  1356. $reservation_service_order_list = OrderReserve::lists([
  1357. 'where' => [
  1358. ['mall_id' => $mall_id],
  1359. ['goods_id' => $store_goods_reserve_sku['goods_id']],
  1360. ['>=', 'service_start_at', strtotime($date . ' 00:00:00')],
  1361. ['<=', 'service_end_at', strtotime($date . ' 23:59:59')],
  1362. ['!=', 'order_status', -4],
  1363. ['create_type' => 1],
  1364. ],
  1365. 'group' => 'service_start_at',
  1366. 'select' => 'count(*) as counts,service_start_at',
  1367. 'index' => 'service_start_at'
  1368. ]);
  1369. }
  1370. $business_time_start = "08:00";
  1371. $business_time_end = "22:00";
  1372. $start_time = self::getRangeStartTime($range_start);
  1373. $time1 = "{$date} {$business_time_start}:00";
  1374. $time2 = "{$date} {$business_time_end}:59";
  1375. $buff = [];
  1376. $time = time();
  1377. $start = strtotime($time1);
  1378. $end = strtotime($time2);
  1379. //切割时间
  1380. for ($i = $start; $i <= $end; $i = $i + 60 * $service_min) {
  1381. $is_reserve = 1;
  1382. $hour_minute = date("H:i", $i);
  1383. $buff[] = ['time' => $hour_minute, 'is_reserve' => $is_reserve];
  1384. }
  1385. $range_end_arr = ReservationServiceEnum::RANGE_END_ARR_MAP;
  1386. $days = $range_end_arr[$range_end] ?? 7;
  1387. $date_list = [];
  1388. $today_time = strtotime(date('Y-m-d', $time) . ' 00:00:00');
  1389. if (!empty($params['date'])) {
  1390. $time = strtotime($params['date'] . ' 00:00:00');
  1391. } else {
  1392. $time = $today_time;
  1393. }
  1394. $end_time = $time + $days * 24 * 3600;
  1395. for ($i = $time; $i < $end_time; $i += 24 * 60 * 60) {
  1396. $is_reserve = 1;
  1397. $time_list = $buff;
  1398. $date = date('Y-m-d', $i);
  1399. $new_time_list = [];
  1400. foreach ($time_list as &$item) {
  1401. $temp_time_start = strtotime("{$date} " . $item['time'] . ":00");
  1402. if (!empty($appointments_number)) {
  1403. if (isset($reservation_service_order_list[$temp_time_start]) && $reservation_service_order_list[$temp_time_start]['counts'] >= $appointments_number) {
  1404. $item['is_reserve'] = 0;
  1405. }
  1406. }
  1407. $te_time = strtotime($date . ' ' . $item['time'] . ':00');
  1408. if ($te_time < time()) {
  1409. continue;
  1410. }
  1411. if ((time() + $start_time) > $te_time) {
  1412. continue;
  1413. }
  1414. $new_time_list[] = $item;
  1415. }
  1416. unset($item);
  1417. $arr = ['date' => $date, 'week_day' => GoodsReserveTime::getWeekDay($date), 'time_list' => $new_time_list, 'is_reserve' => $is_reserve];
  1418. $date_list[] = $arr;
  1419. }
  1420. return $date_list;
  1421. }
  1422. public static function getServiceMin($appointment_interval, $min)
  1423. {
  1424. switch ($appointment_interval) {
  1425. case 1:
  1426. $service_min = $min;
  1427. break;
  1428. default:
  1429. $service_min = $min ?? 30;
  1430. }
  1431. return $service_min;
  1432. }
  1433. }