Appointment.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <?php
  2. namespace app\index\controller;
  3. use app\common\controller\Frontend;
  4. use app\common\library\Ems as EmsLib;
  5. use app\common\library\Sms as SmsLib;
  6. use app\common\model\AppointmentEmailLog;
  7. use app\common\model\AppointmentRecord;
  8. use app\common\model\AppointmentSmsLog;
  9. use think\facade\Cache;
  10. use think\facade\Db;
  11. use think\facade\Event;
  12. use think\facade\Validate;
  13. class Appointment extends Frontend
  14. {
  15. protected $noNeedLogin = ['submit'];
  16. protected $noNeedRight = ['*'];
  17. protected function frontendCurrentUrl(): string
  18. {
  19. $url = (string)$this->request->url(true);
  20. return function_exists('cms_lang_url') ? cms_lang_url($url) : $url;
  21. }
  22. protected function resolveNotifyChannel(string $lang): string
  23. {
  24. $lang = strtolower(trim($lang));
  25. return in_array($lang, ['cn', 'zh-cn', 'zh'], true) ? 'sms' : 'email';
  26. }
  27. protected function getTranslatedActivity(int $activityId, string $lang): array
  28. {
  29. $lang = strtolower(trim($lang));
  30. if ($lang === '') {
  31. $lang = 'cn';
  32. }
  33. $query = Db::name('cms_yuyue_data')->where('did', $activityId);
  34. $row = (clone $query)->where('lang', $lang)->find();
  35. if (!$row && $lang !== 'cn') {
  36. $row = (clone $query)->where('lang', 'cn')->find();
  37. }
  38. return is_array($row) ? $row : [];
  39. }
  40. protected function getTranslatedNotifyContent(array $activity, string $field, string $lang): string
  41. {
  42. return trim((string)($activity[$field] ?? ''));
  43. }
  44. protected function sendAppointmentEmail(string $email, string $subject, string $content): array
  45. {
  46. try {
  47. $config = get_addon_config('phpmailer');
  48. if (!$config) {
  49. return [false, 'NO_CONFIG', '未找到 phpmailer 插件配置'];
  50. }
  51. $mailer = new \addons\phpmailer\library\Mailer($config);
  52. $result = $mailer->to($email)->subject($subject)->message($content, true)->send();
  53. if ($result) {
  54. return [true, 'SUCCESS', '邮件发送成功'];
  55. }
  56. return [false, 'FAIL', $mailer->getError() ?: '邮件发送失败'];
  57. } catch (\Throwable $e) {
  58. return [false, 'EXCEPTION', $e->getMessage()];
  59. }
  60. }
  61. public function submit()
  62. {
  63. if (!$this->request->isPost()) {
  64. $this->error('请求方式错误');
  65. }
  66. $activityId = (int)$this->request->post('activity_id/d', 0);
  67. $mobile = trim((string)$this->request->post('mobile/s', ''));
  68. $email = trim((string)$this->request->post('email/s', ''));
  69. $name = trim((string)$this->request->post('name/s', ''));
  70. $remark = trim((string)$this->request->post('remark/s', ''));
  71. $sourcePage = trim((string)$this->request->post('source_page/s', $this->frontendCurrentUrl()));
  72. $lang = strtolower(trim((string)$this->request->post('lang/s', '')));
  73. if ($lang === '' && function_exists('cms_current_region_lang')) {
  74. $lang = strtolower(trim((string)cms_current_region_lang('cn')));
  75. }
  76. if ($lang === '') {
  77. $lang = 'cn';
  78. }
  79. $notifyChannel = $this->resolveNotifyChannel($lang);
  80. if ($activityId <= 0) {
  81. $this->error('预约活动信息缺失');
  82. }
  83. if ($notifyChannel === 'sms') {
  84. if (!$mobile || !Validate::regex($mobile, '^1\d{10}$')) {
  85. $this->error('手机号格式不正确');
  86. }
  87. } else {
  88. if (!$email || !Validate::is($email, 'email')) {
  89. $this->error('Email格式不正确');
  90. }
  91. }
  92. $now = time();
  93. $ip = $this->request->ip();
  94. $activity = Db::name('cms_yuyue')->where('id', $activityId)->find();
  95. if (!$activity) {
  96. $this->error('预约活动不存在或已下线');
  97. }
  98. $translatedActivity = $this->getTranslatedActivity($activityId, $lang);
  99. if ($translatedActivity) {
  100. $activity = array_merge($activity, $translatedActivity);
  101. }
  102. $activityTitle = trim((string)($activity['title'] ?? ''));
  103. if ($activityTitle === '') {
  104. $this->error('预约活动标题缺失');
  105. }
  106. $starttime = isset($activity['starttime']) ? (int)$activity['starttime'] : 0;
  107. $endtime = isset($activity['endtime']) ? (int)$activity['endtime'] : 0;
  108. if ($starttime > 0 && $now < $starttime) {
  109. $this->error('预约尚未开始');
  110. }
  111. if ($endtime > 0 && $now > $endtime) {
  112. $this->error('预约已结束');
  113. }
  114. $ipLimitWindowSeconds = 60;
  115. $ipLimitWindowMax = max(0, (int) config('site.sms_rpm', 5));
  116. $ipLimitDailyMax = max(0, (int) config('site.sms_drl', 10));
  117. $dayKey = date('Ymd', $now);
  118. $ipWindowKey = 'appointment_submit_ip_window_' . md5($ip);
  119. $ipDailyKey = 'appointment_submit_ip_daily_' . md5($ip . '_' . $dayKey);
  120. $ipWindowHits = (int) Cache::get($ipWindowKey, 0);
  121. if ($ipLimitWindowMax > 0 && $ipWindowHits >= $ipLimitWindowMax) {
  122. $this->error('提交过于频繁,请稍后再试');
  123. }
  124. $ipDailyHits = (int) Cache::get($ipDailyKey, 0);
  125. if ($ipLimitDailyMax > 0 && $ipDailyHits >= $ipLimitDailyMax) {
  126. $this->error('今日提交次数已达上限,请明天再试');
  127. }
  128. if ($ipLimitWindowMax > 0) {
  129. Cache::set($ipWindowKey, $ipWindowHits + 1, $ipLimitWindowSeconds);
  130. }
  131. if ($ipLimitDailyMax > 0) {
  132. Cache::set($ipDailyKey, $ipDailyHits + 1, 86400);
  133. }
  134. if ($notifyChannel === 'sms') {
  135. $exists = AppointmentRecord::where(['activity_id' => $activityId, 'mobile' => $mobile])->find();
  136. } else {
  137. $exists = AppointmentRecord::where(['activity_id' => $activityId, 'email' => $email])->find();
  138. }
  139. if ($exists) {
  140. $duplicateMsg = $notifyChannel === 'sms'
  141. ? '您已提交过该活动预约,请勿重复提交'
  142. : 'You have already submitted this reservation for the activity.';
  143. $this->success($duplicateMsg, null, [
  144. 'record_id' => $exists->id,
  145. 'duplicate' => 1,
  146. ]);
  147. }
  148. $submitNotifyTpl = $this->getTranslatedNotifyContent($activity, 'submit_sms_content', $lang);
  149. $autoNotify = !empty($activity['appointment_auto_sms']) ? 1 : 0;
  150. Db::startTrans();
  151. try {
  152. $record = new AppointmentRecord();
  153. $record->save([
  154. 'activity_id' => $activityId,
  155. 'activity_title' => $activityTitle,
  156. 'lang' => $lang,
  157. 'notify_channel' => $notifyChannel,
  158. 'mobile' => $mobile,
  159. 'email' => $email,
  160. 'name' => $name,
  161. 'remark' => $remark,
  162. 'status' => 1,
  163. 'submit_sms_status' => 0,
  164. 'submit_sms_time' => 0,
  165. 'batch_sms_status' => 0,
  166. 'batch_sms_time' => 0,
  167. 'source_page' => $sourcePage,
  168. 'ip' => $ip,
  169. 'admin_remark' => '',
  170. 'createtime' => $now,
  171. 'updatetime' => $now,
  172. ]);
  173. if ($notifyChannel === 'sms') {
  174. $submitContent = $submitNotifyTpl !== ''
  175. ? str_replace('{title}', $activityTitle, $submitNotifyTpl)
  176. : '您已成功预约【' . $activityTitle . '】,新品动态发布后我们将第一时间短信通知您。';
  177. $smsLog = new AppointmentSmsLog();
  178. $smsLog->save([
  179. 'record_id' => $record->id,
  180. 'activity_id' => $activityId,
  181. 'mobile' => $mobile,
  182. 'sms_type' => 'appointment_submit',
  183. 'template_code' => '',
  184. 'content' => $submitContent,
  185. 'status' => 0,
  186. 'response_code' => '',
  187. 'response_msg' => $autoNotify ? '待自动发送' : '当前活动未开启提交后自动短信',
  188. 'retry_count' => 0,
  189. 'send_time' => 0,
  190. 'createtime' => $now,
  191. 'updatetime' => $now,
  192. ]);
  193. if ($autoNotify) {
  194. if (Event::hasListener('sms_notice')) {
  195. $sendResult = SmsLib::notice($mobile, $submitContent, null);
  196. $smsLog->status = $sendResult ? 1 : 2;
  197. $smsLog->response_code = $sendResult ? 'SUCCESS' : 'FAIL';
  198. $smsLog->response_msg = $sendResult ? '提交后自动短信发送成功' : '提交后自动短信发送失败';
  199. $smsLog->send_time = $sendResult ? $now : 0;
  200. $smsLog->updatetime = $now;
  201. $smsLog->save();
  202. $record->submit_sms_status = $sendResult ? 1 : 2;
  203. $record->submit_sms_time = $sendResult ? $now : 0;
  204. $record->updatetime = $now;
  205. $record->save();
  206. } else {
  207. $smsLog->status = 2;
  208. $smsLog->response_code = 'NO_PLUGIN';
  209. $smsLog->response_msg = '未检测到短信通知插件监听 sms_notice';
  210. $smsLog->updatetime = $now;
  211. $smsLog->save();
  212. $record->submit_sms_status = 2;
  213. $record->updatetime = $now;
  214. $record->save();
  215. }
  216. }
  217. } else {
  218. $submitContent = $submitNotifyTpl !== ''
  219. ? str_replace('{title}', $activityTitle, $submitNotifyTpl)
  220. : 'You have successfully reserved [' . $activityTitle . ']. We will notify you by email when updates are available.';
  221. $subject = $activityTitle . ' Reservation Confirmation';
  222. $emailLog = new AppointmentEmailLog();
  223. $emailLog->save([
  224. 'record_id' => $record->id,
  225. 'activity_id' => $activityId,
  226. 'email' => $email,
  227. 'mail_type' => 'appointment_submit',
  228. 'subject' => $subject,
  229. 'content' => $submitContent,
  230. 'status' => 0,
  231. 'response_code' => '',
  232. 'response_msg' => $autoNotify ? '待自动发送' : '当前活动未开启提交后自动邮件',
  233. 'retry_count' => 0,
  234. 'send_time' => 0,
  235. 'createtime' => $now,
  236. 'updatetime' => $now,
  237. ]);
  238. if ($autoNotify) {
  239. [$sendResult, $responseCode, $responseMsg] = $this->sendAppointmentEmail($email, $subject, $submitContent);
  240. $emailLog->status = $sendResult ? 1 : 2;
  241. $emailLog->response_code = $responseCode;
  242. $emailLog->response_msg = $responseMsg ?: ($sendResult ? '提交后自动邮件发送成功' : '提交后自动邮件发送失败');
  243. $emailLog->send_time = $sendResult ? $now : 0;
  244. $emailLog->updatetime = $now;
  245. $emailLog->save();
  246. $record->submit_sms_status = $sendResult ? 1 : 2;
  247. $record->submit_sms_time = $sendResult ? $now : 0;
  248. $record->updatetime = $now;
  249. $record->save();
  250. }
  251. }
  252. Db::commit();
  253. } catch (\Throwable $e) {
  254. Db::rollback();
  255. $this->error('预约提交失败:' . $e->getMessage());
  256. }
  257. if ($notifyChannel === 'sms') {
  258. $msg = $autoNotify
  259. ? ($record->submit_sms_status == 1 ? '预约成功,短信已发送,请留意手机通知' : '预约成功,但自动短信发送未完成,请稍后关注后续通知')
  260. : '预约成功,我们会在合适时间统一发送短信通知';
  261. } else {
  262. $msg = $autoNotify
  263. ? ($record->submit_sms_status == 1 ? 'Reservation successful. A confirmation email has been sent.' : 'Reservation successful, but the automatic notification was not completed.')
  264. : 'Reservation successful. We will contact you later by email.';
  265. }
  266. $this->success($msg, null, [
  267. 'record_id' => $record->id,
  268. 'lang' => $lang,
  269. 'notify_channel' => $notifyChannel,
  270. 'submit_sms_status' => $record->submit_sms_status,
  271. 'batch_sms_status' => $record->batch_sms_status,
  272. 'appointment_auto_sms' => $autoNotify,
  273. ]);
  274. }
  275. }