MassagerService.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. <?php
  2. namespace app\api\service;
  3. use app\admin\model\dynamic\Dynamic;
  4. use app\admin\model\dynamic\Like;
  5. use app\api\model\massager\Collect;
  6. use app\api\model\massager\Comment;
  7. use app\api\model\massager\Massager;
  8. use app\api\model\massager\Work;
  9. use app\api\model\massager\WorkPeriod;
  10. use app\api\model\order\Order;
  11. use app\api\model\service\Service;
  12. class MassagerService extends BaseService
  13. {
  14. private $massagerModel;
  15. private $colletModel;
  16. private $commentModel;
  17. private $orderModel;
  18. private $workModel;
  19. public function __construct()
  20. {
  21. $this->massagerModel = new Massager();
  22. $this->colletModel = new Collect();
  23. $this->commentModel = new Comment();
  24. $this->orderModel = new Order();
  25. $this->workModel = new Work();
  26. }
  27. /**
  28. * 合并助教数据代码
  29. * @param $paginate
  30. * @param $user_id
  31. * @return array
  32. */
  33. private function mergeMassager($paginate, $user_id)
  34. {
  35. $items = $paginate->items();
  36. $collet_relation = [];
  37. if ($user_id) {
  38. $collet_relation = $this->colletModel->fetchByUserIdAndMassagerIds($user_id, array_map(function ($data) {
  39. return $data['id'];
  40. }, $items));
  41. $collet_relation = array_reduce($collet_relation, function ($p, $cur) {
  42. $p[$cur['massager_id']] = 1;
  43. return $p;
  44. }, []);
  45. }
  46. $services = (new \app\admin\model\Service())->where([
  47. "duration_minute" => 120,
  48. "type" => \E_SERVICE_TYPE::App,
  49. ])->select();
  50. $services = array_reduce($services, function ($p, $cur) {
  51. $p[$cur["level"]] = $cur["real_price"];
  52. return $p;
  53. }, []);
  54. foreach ($items as &$item) {
  55. $recently_work_date = $this->workDateRange(array_map(function ($data) {
  56. return [strtotime($data["service_start_date"]), strtotime($data["service_end_date"])];
  57. }, $item["nearly_two_days_orders"]), true);
  58. $item['recently_work_date'] = $recently_work_date;
  59. $item['is_can_collet'] = !isset($collet_relation[$item['id']]);
  60. $item["price"] = $services[$item['level']] ?? 260;
  61. unset($item['nearly_two_days_orders']);
  62. }
  63. return [
  64. array_map(function ($data) {
  65. return Massager::fmtMassager($data);
  66. }, $items),
  67. $paginate->total()
  68. ];
  69. }
  70. /**
  71. * 判断是否能够选择
  72. * @param $in_time
  73. * @param $ranges
  74. * @return bool
  75. */
  76. private function checkCanOption($in_time, $ranges)
  77. {
  78. foreach ($ranges as $range) {
  79. if ($in_time >= $range[0] && $in_time <= $range[1]) {
  80. return false;
  81. }
  82. }
  83. return true;
  84. }
  85. /**
  86. * 获取可助教工作的时间段
  87. * @param array $order_date_range
  88. * @param bool $recently
  89. * @return array|false|string|null
  90. */
  91. private function workDateRange(array $order_date_range, $recently = true)
  92. {
  93. $now_time = time();
  94. $range = [];
  95. $max_time = strtotime(date("Y-m-d 23:59:59"));
  96. $acc_time = $now_time;
  97. $recently_work_date = null;
  98. while (true) {
  99. if ($acc_time === $now_time) {
  100. $y_m_d = date("Y-m-d", $acc_time);
  101. $now_minute = date("i", $acc_time);
  102. if ($now_minute < 30) {
  103. $range["$y_m_d"] = ["date" => $y_m_d, "range" => [["value" => date("H:30", $acc_time), "can_option" => false]]];
  104. }
  105. }
  106. $acc_time += (60 * 60);
  107. $y_m_d = date("Y-m-d", $acc_time);
  108. $y_m_d_h = date("Y-m-d H:00:00", $acc_time);
  109. $v_1 = [
  110. "value" => date("H:00", strtotime($y_m_d_h)),
  111. "can_option" => !(($acc_time - (60 * 60)) === $now_time) && $this->checkCanOption(strtotime($y_m_d_h), $order_date_range)
  112. ];
  113. $v_2 = [
  114. "value" => date("H:30", $acc_time),
  115. "can_option" => $this->checkCanOption($acc_time + (30 * 60), $order_date_range)
  116. ];
  117. if ($recently) {
  118. if ($v_1["can_option"]) {
  119. $recently_work_date = "$y_m_d_h";
  120. break;
  121. }
  122. if ($v_2["can_option"]) {
  123. $recently_work_date = date("Y-m-d H:30:00", $acc_time);
  124. break;
  125. }
  126. }
  127. if (isset($range["$y_m_d"])) {
  128. array_push($range["$y_m_d"]["range"], $v_1, $v_2);
  129. } else {
  130. $range["$y_m_d"] = [
  131. "date" => $y_m_d,
  132. "range" => [$v_1, $v_2]
  133. ];
  134. }
  135. if ($acc_time >= $max_time) break;
  136. }
  137. if ($recently) {
  138. return $recently_work_date;
  139. }
  140. $format_range = [];
  141. $keys = array_keys($range);
  142. for ($i = 0; $i <= count($keys) - 1; $i++) {
  143. $key = $keys[$i];
  144. $format_range[$i] = $range[$key];
  145. }
  146. return $format_range;
  147. }
  148. /**
  149. * 获取App助教
  150. * @param null $user_id
  151. * @param array $params
  152. * @param $gender
  153. * @return array
  154. */
  155. public function fetchAppMassager($user_id, array $params, $gender)
  156. {
  157. $paginate = $this->massagerModel->fetchAppMassager(
  158. $params['city_code'],
  159. $params['lng_lat'],
  160. isset($params['search_text']) && mb_strlen($params['search_text']) > 0 ? $params['search_text'] : null,
  161. isset($params['distance']) && is_numeric($params["distance"]) ? $params['distance'] : 300,
  162. isset($params['service_status']) && is_numeric($params['service_status']) ? $params['service_status'] : null,
  163. $gender,
  164. isset($params['free_travel']) && is_numeric($params['free_travel']) ? $params['free_travel'] : null,
  165. isset($params['hot']) && is_numeric($params['hot']) ? $params['hot'] : null,
  166. isset($params['service_id']) && is_numeric($params['service_id']) ? $params['service_id'] : null,
  167. $params['sort'] ?? null,
  168. $params['page'] ?? 1,
  169. $params['size'] ?? 10,
  170. $user_id,
  171. isset($params['level']) && is_numeric($params['level']) ? $params['level'] : null
  172. );
  173. return $this->mergeMassager($paginate, $user_id);
  174. }
  175. /**
  176. * 获取球房助教
  177. * @param array $params
  178. * @param $gender
  179. * @param null $user_id
  180. * @return array
  181. */
  182. public function fetchStoreMassager(array $params, $gender, $user_id = null)
  183. {
  184. $paginate = $this->massagerModel->fetchStoreMassager(
  185. $params['store_id'],
  186. $params['service_id'] ?? null,
  187. $params['search_text'] ?? null,
  188. $params['hot'] ?? null,
  189. $gender,
  190. $params['page'] ?? 1,
  191. $params['size'] ?? 10
  192. );
  193. return $this->mergeMassager($paginate, $user_id);
  194. }
  195. /**
  196. * 获取助教上班的的时间
  197. * @param $id
  198. * @return |null
  199. */
  200. public function getWorkDateRange($id)
  201. {
  202. $massager = $this->massagerModel->getMassager($id, ["store", "nearlyTwoDaysOrders"]);
  203. if (!$massager)
  204. return $this->ok([]);
  205. $order_date_range = array_map(function ($data) {
  206. return [strtotime($data["service_start_date"]), strtotime($data["service_end_date"])];
  207. }, $massager["nearlyTwoDaysOrders"]);
  208. $no_work_ranges = (new WorkPeriod())->where("massager_id", $id)->where("end_time", ">", time())->select();
  209. $ranges_ = array_map(function ($data) {
  210. return [$data["start_time"], $data["end_time"]];
  211. }, $no_work_ranges);
  212. $range = $this->workDateRange(array_merge($order_date_range, $ranges_), false);
  213. return $this->ok([
  214. "id" => $massager->id,
  215. "range" => $range
  216. ]);
  217. }
  218. /**
  219. * 获取单个助教详情
  220. * @param $massager_id
  221. * @param null $user_id
  222. * @return |null
  223. */
  224. public function get($massager_id, $user_id = null, $is_add_clock = null)
  225. {
  226. $massager = $this->massagerModel->fetchHiddenAndNormal($massager_id);
  227. if (null === $massager)
  228. return null;
  229. $massager['is_can_collet'] = true;
  230. if ($user_id) {
  231. $relation = $this->colletModel->getByUserIdAndMassagerId($user_id, $massager_id);
  232. if ($relation)
  233. $massager['is_can_collet'] = false;
  234. }
  235. $serviceModel = new Service();
  236. $query = $serviceModel
  237. ->where([
  238. "level" => $massager["level"],
  239. "type" => \E_SERVICE_TYPE::App,
  240. "p_code" => parse_area($massager['city_code'])['p_code']
  241. ]);
  242. if ($is_add_clock != 1) {
  243. $query->where("is_add_clock", 0);
  244. }
  245. $exist = null;
  246. $items = collection($query
  247. ->order('sort', 'desc')
  248. ->order('real_price', 'ASC')->select())->toArray();
  249. $membership_discount_rate = config("site.membership_discount_rate") / 100;
  250. $available_services = [];
  251. foreach ($items as &$item) {
  252. $item["membership_discount_price"] = fixed2Float($item["real_price"] * $membership_discount_rate);
  253. if ($massager["store_id"] == $exist && $exist > 0) {
  254. $available_services[] = $item;
  255. }
  256. }
  257. $massager["services"] = $available_services;
  258. return Massager::fmtMassager($massager);
  259. }
  260. /**
  261. * @param $massager_id
  262. * @param $page
  263. * @param $size
  264. * @return array
  265. * @throws \think\exception\DbException
  266. */
  267. public function fetchMassagerComment($massager_id, $page, $size)
  268. {
  269. $paginate = $this->commentModel->fetchByMassagerId($massager_id, $page, $size);
  270. $items = $paginate->items();
  271. return [
  272. array_map(function ($data) {
  273. $nickname = "匿名用户";
  274. $avatar = null;
  275. if ($data['user'] && 0 == $data['is_anonymity']) {
  276. $nickname = $data['user']['nickname'];
  277. $avatar = $data['user']['avatar'];
  278. }
  279. $data['user_nickname'] = $nickname;
  280. $data['user_avatar'] = $avatar;
  281. unset($data['user']);
  282. return $data;
  283. }, $items),
  284. $paginate->total()
  285. ];
  286. }
  287. public function getMassagerTentativeNowLevelConfiguration($m_id, $city_code, $intraday = null)
  288. {
  289. $configuration = [
  290. [0, 50],
  291. [300, 60],
  292. [500, 65],
  293. [700, 70]
  294. ];
  295. $level_index = 1;
  296. $profit_rate = 50;
  297. $next_configuration = 288;
  298. $sumPerformanceByNowMonth = $this->orderModel->sumPerformanceByIntraday($m_id, $city_code, $intraday);
  299. if ($sumPerformanceByNowMonth > $configuration[0][0]) {
  300. for ($index = 0; $index < count($configuration); $index++) {
  301. $item = $configuration[$index];
  302. if ($sumPerformanceByNowMonth > $item[0]) {
  303. $level_index += 1;
  304. $profit_rate = $item[1];
  305. $next_configuration = isset($configuration[$index + 1]) ? $configuration[$index + 1][0] : null;
  306. }
  307. }
  308. $level_index -= 1;
  309. }
  310. return [
  311. "level" => $level_index,
  312. "profit_rate" => $profit_rate,
  313. "next_configuration" => $next_configuration,
  314. "sum_performance_by_now_month" => $sumPerformanceByNowMonth,
  315. ];
  316. }
  317. public function getMassagerNowLevelConfiguration($m_id, $city_code, $y = null, $m = null)
  318. {
  319. $configuration = array_map(function ($data) {
  320. $performance = explode("|", $data["月业绩/元"]);
  321. $duration = explode("|", $data["在岗时长/小时"]);
  322. $reorder = explode("|", $data["加钟比例(%)"]);
  323. $praise = explode("|", $data["好评率(%)"]);
  324. $chargeback = explode("|", $data["退单率(%)"]);
  325. return [
  326. [
  327. "condition" => isset($performance[0]) && is_numeric($performance[0]) ? (float)$performance[0] : 6500,
  328. "rate" => isset($performance[1]) && is_numeric($performance[1]) ? (float)$performance[1] : 55
  329. ],
  330. [
  331. "condition" => isset($duration[0]) && is_numeric($duration[0]) ? (float)$duration[0] : 100,
  332. "rate" => isset($duration[1]) && is_numeric($duration[1]) ? (float)$duration[1] : 1
  333. ],
  334. [
  335. "condition" => isset($reorder[0]) && is_numeric($reorder[0]) ? (float)$reorder[0] : 30,
  336. "rate" => isset($reorder[1]) && is_numeric($reorder[1]) ? (float)$reorder[1] : 1
  337. ],
  338. [
  339. "condition" => isset($praise[0]) && is_numeric($praise[0]) ? (float)$praise[0] : 30,
  340. "rate" => isset($praise[1]) && is_numeric($praise[1]) ? (float)$praise[1] : 1
  341. ],
  342. [
  343. "condition" => isset($chargeback[0]) && is_numeric($chargeback[0]) ? (float)$chargeback[0] : 30,
  344. "rate" => isset($chargeback[1]) && is_numeric($chargeback[1]) ? (float)$chargeback[1] : -1
  345. ]
  346. ];
  347. }, [
  348. config("site.massager_level_1"),
  349. config("site.massager_level_2"),
  350. config("site.massager_level_3"),
  351. config("site.massager_level_4"),
  352. config("site.massager_level_5"),
  353. ]);
  354. $sumPerformanceByNowMonth = $this->orderModel->sumPerformanceByNowMonth($m_id, $city_code, $y, $m);
  355. $sumDurationByNowMonth = $this->workModel->sumByNowMonth($m_id, $y, $m);
  356. $reorderRateByNowMonth = $this->orderModel->reorderRateByNowMonth($m_id, $city_code, $y, $m);
  357. $praiseRateByNowMonth = $this->orderModel->countByPraiseRate($m_id, $city_code, $y, $m);
  358. $chargebackRateByNowMonth = $this->orderModel->chargebackRate($m_id, $city_code, $y, $m);
  359. $profit_rate = config("site.massager_basics_deposit_rate") ?? 50;
  360. $level_index = 0;
  361. $next_configuration = null;
  362. for ($i = 0; $i < count($configuration); $i++) {
  363. $now_configuration = $configuration[$i];
  364. if ($sumPerformanceByNowMonth >= $now_configuration[0]["condition"]) {
  365. $level_index += 1;
  366. $next_configuration = isset($configuration[$i + 1]) ? $configuration[$i + 1] : null;
  367. $profit_rate = $now_configuration[0]["rate"];
  368. $profit_rate += ($sumDurationByNowMonth >= $now_configuration[1]["condition"] ? $now_configuration[1]["rate"] : 0);
  369. $profit_rate += ($reorderRateByNowMonth >= $now_configuration[2]["condition"] ? $now_configuration[2]["rate"] : 0);
  370. $profit_rate += ($praiseRateByNowMonth >= $now_configuration[3]["condition"] ? $now_configuration[3]["rate"] : 0);
  371. $profit_rate += ($chargebackRateByNowMonth >= $now_configuration[4]["condition"] ? $now_configuration[4]["rate"] : 0);
  372. } else {
  373. break;
  374. }
  375. }
  376. return [
  377. "level" => $level_index,
  378. "profit_rate" => $profit_rate,
  379. "next_configuration" => $next_configuration,
  380. "sum_performance_by_now_month" => $sumPerformanceByNowMonth,
  381. ];
  382. }
  383. public function getProfitRate($m_id, $city_code, $y = null, $m = null, $intraday = null): float
  384. {
  385. $massager = $this->massagerModel->findById($m_id);
  386. if (!$massager)
  387. return 0;
  388. if ($massager["fixed_profit_rate"] > 0)
  389. return $massager["fixed_profit_rate"];
  390. // DOTO: 临时替换
  391. $level_info = $this->getMassagerTentativeNowLevelConfiguration($m_id, $city_code, $intraday);
  392. // $level_info = $this->getMassagerNowLevelConfiguration($m_id, $city_code, $y, $m);
  393. return $level_info["profit_rate"];
  394. }
  395. public function interiorScoreLevel($score)
  396. {
  397. $config = config("site.massager_review_level");
  398. $keys = array_keys($config);
  399. $level_desc = "不及格";
  400. foreach ($keys as $key) {
  401. if ($score >= (float)$key) {
  402. $level_desc = $config[$key];
  403. } else {
  404. break;
  405. }
  406. }
  407. return $level_desc;
  408. }
  409. public function updateInteriorScore($m_id, $year, $month)
  410. {
  411. $config = config("site.massager_review_score");
  412. $fmt_config = [
  413. "duration" => explode("|", $config["时长/天"] ?? "6|2"),
  414. "praise" => explode("|", $config["评论/好评"] ?? "1|2"),
  415. "negative" => explode("|", $config["评论/差评"] ?? "1|-10"),
  416. "performance" => explode("|", $config["业绩/天"] ?? "500|2"),
  417. "reorder" => explode("|", $config["项目加钟"] ?? "1|2")
  418. ];
  419. $byDuration = $this->interiorScoreByDuration($m_id, $year, $month, $fmt_config["duration"]);
  420. $byPraise = $this->interiorScoreByPraise($m_id, $year, $month, $fmt_config["praise"]);
  421. $byNegative = $this->interiorScoreByNegative($m_id, $year, $month, $fmt_config["negative"]);
  422. $byPerformance = $this->interiorScoreByPerformance($m_id, $year, $month, $fmt_config["performance"]);
  423. $byReorder = $this->interiorScoreByReorder($m_id, $year, $month, $fmt_config["reorder"]);
  424. $total_score = fixed2Float(
  425. $byDuration["score"]
  426. + $byPraise["score"]
  427. + $byNegative["score"]
  428. + $byPerformance["score"]
  429. + $byReorder["score"]
  430. );
  431. if (date("Y") == $year && date("m") == $month) {
  432. $this->massagerModel->update([
  433. "interior_score" => $total_score
  434. ], ["id" => $m_id]);
  435. }
  436. return [
  437. "total_interior_score" => $total_score,
  438. "level_desc" => $this->interiorScoreLevel($total_score),
  439. "items" => [
  440. "byDuration" => $byDuration,
  441. "byPraise" => $byPraise,
  442. "byNegative" => $byNegative,
  443. "byPerformance" => $byPerformance,
  444. "byReorder" => $byReorder
  445. ]
  446. ];
  447. }
  448. public function interiorScoreByDuration($m_id, $year, $month, $config)
  449. {
  450. $last_day = date("t", strtotime("$year-$month-01 00:00:00"));
  451. $starttime = strtotime("$year-$month-01 00:00:00");
  452. $endtime = strtotime("$year-$month-$last_day 23:59:59");
  453. $works = (new Work())
  454. ->where("massager_id", $m_id)
  455. ->where("clock_in_time", ">=", $starttime)
  456. ->where("clock_off_time", "<=", $endtime)
  457. ->select();
  458. $clocks_times = [];
  459. foreach ($works as $work) {
  460. $times = splittingDate($work["clock_in_time"], $work["clock_off_time"]);
  461. $clocks_times = array_merge($clocks_times, $times);
  462. }
  463. $clocks_times = array_merge(array_unique($clocks_times));
  464. $every_days = [];
  465. $DAY_TIME = 24 * 60 * 60;
  466. $start_time_copy = $starttime;
  467. while ($start_time_copy <= $endtime) {
  468. array_push($every_days, date("Y-m-d", $start_time_copy));
  469. $start_time_copy += $DAY_TIME;
  470. }
  471. $score = 0;
  472. $intersections = [];
  473. foreach ($every_days as $now_day) {
  474. $now_day_times = splittingDate(strtotime("$now_day 00:00:00"), strtotime("$now_day 23:59:59"));
  475. $intersection = array_intersect($now_day_times, $clocks_times);
  476. $meet = count($intersection) >= (fixed2Float((($config[0] ?? 6) * 60) / 10) + 1);
  477. if ($meet) {
  478. $intersections[] = [
  479. "date" => $now_day,
  480. "count" => count($intersection),
  481. ];
  482. $score += $config[1] ?? 2;
  483. }
  484. }
  485. return [
  486. "score" => fixed2Float($score),
  487. "details" => $intersections
  488. ];
  489. }
  490. public function interiorScoreByPraise($m_id, $year, $month, $config)
  491. {
  492. $negative = $this->commentModel->fetchPraiseCommentByMassager($m_id, $year, $month);
  493. $count = count($negative);
  494. $item_score = $config[1] / $config[0];
  495. return ["score" => fixed2Float($item_score * $count), "details" => $negative];
  496. }
  497. public function interiorScoreByNegative($m_id, $year, $month, $config)
  498. {
  499. $negative = $this->commentModel->fetchNegativeCommentByMassager($m_id, $year, $month);
  500. $count = count($negative);
  501. $item_score = $config[1] / $config[0];
  502. return ["score" => fixed2Float($item_score * $count), "details" => $negative];
  503. }
  504. public function interiorScoreByPerformance($m_id, $year, $month, $config)
  505. {
  506. $all_orders = $this->orderModel->fetchByMassager($m_id, $year, $month);
  507. $fmt = [];
  508. foreach ($all_orders as $order) {
  509. $key = date("Y-m-d", $order["pay_time"]);
  510. if (isset($fmt[$key])) {
  511. $fmt[$key] += $order["total_real_amount"];
  512. } else {
  513. $fmt[$key] = $order["total_real_amount"];
  514. }
  515. }
  516. $score = 0;
  517. $details = [];
  518. foreach ($fmt as $key => $value) {
  519. if ($value >= $config[0]) {
  520. $details[] = [
  521. "date" => $key,
  522. "count" => $value,
  523. ];
  524. $score += $config[1];
  525. }
  526. }
  527. return ["score" => fixed2Float($score), "details" => $details];
  528. }
  529. public function interiorScoreByReorder($m_id, $year, $month, $config)
  530. {
  531. $all_orders = $this->orderModel->fetchReorderByMassager($m_id, $year, $month);
  532. $count = count($all_orders);
  533. $item_score = $config[1] / $config[0];
  534. return ["score" => fixed2Float($item_score * $count), "details" => $all_orders];
  535. }
  536. public function fetchSelfDynamics($m_id, $page = 1, $size = 10)
  537. {
  538. $paginate = (new Dynamic())
  539. ->where("massager_id", $m_id)
  540. ->with(["comments", "likes"])
  541. ->page($page)
  542. ->paginate($size);
  543. return [
  544. $paginate->items(),
  545. $paginate->total()
  546. ];
  547. }
  548. public function fetchFansNumberAndPraiseNumber($m_id)
  549. {
  550. return [
  551. "fansNumber" => Collect::where("massager_id", $m_id)->count(),
  552. "praiseNumber" => Like::where("massager_id", $m_id)->count()
  553. ];
  554. }
  555. }