EventController.php 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. <?php
  2. namespace app\modules\client\controllers\v1;
  3. use app\models\Event;
  4. use app\models\EventForm;
  5. use app\models\EventFormDetail;
  6. use app\models\EventSetting;
  7. use app\models\EventSignLog;
  8. use app\models\EventSignStaff;
  9. use app\models\EventUser;
  10. use yii\data\Pagination;
  11. use app\modules\client\controllers\BaseController;
  12. use app\utils\QrCode;
  13. use app\utils\ShareQrcode;
  14. use app\utils\Alipay\alipaySdk\aop\request\AlipayOpenAppQrcodeCreateRequest;
  15. use Yii;
  16. /**
  17. * @file EventController
  18. * @editor Created by vscode
  19. * @author WPing丶
  20. * @date 2025/07/14
  21. * @time 09:20:05
  22. *
  23. * 备注:活动报名功能前端接口
  24. */
  25. class EventController extends BaseController
  26. {
  27. /**
  28. * 模块名:list
  29. * 代码描述:活动专区列表
  30. * 作者:WPing丶
  31. * 请求方式:GET
  32. * 创建时间:2025/07/14 09:45:13
  33. * @param int page
  34. * @param int limit
  35. */
  36. public function actionList()
  37. {
  38. $user_id = get_user_id();
  39. $store_id = get_store_id();
  40. $page = max(1, (int)get_params('page'));
  41. $limit = (int)get_params('limit', 10);
  42. $limit = ($limit <= 0) ? 10 : min($limit, 100);
  43. $now = time();
  44. // 活动分页列表
  45. $query = Event::find()
  46. ->where([
  47. 'is_delete' => 0,
  48. 'store_id' => $store_id,
  49. ])
  50. ->andWhere(['and', ['<=', 'start_time', $now], ['>', 'end_time', $now]])
  51. ->orderBy(['start_time' => SORT_DESC]);
  52. $count = (clone $query)->count(); // 避免污染
  53. $pagination = new Pagination([
  54. 'totalCount' => $count,
  55. 'pageSize' => $limit,
  56. 'page' => $page - 1,
  57. ]);
  58. $list = $query
  59. ->limit($pagination->limit)
  60. ->offset($pagination->offset)
  61. ->asArray()
  62. ->all();
  63. foreach ($list as &$item) {
  64. // 查询已报名人数
  65. $item['count'] = EventUser::find()
  66. ->where(['event_id' => $item['event_id'], 'is_delete' => 0])
  67. ->count();
  68. $event_user = EventUser::findOne(['user_id' => $user_id, 'is_delete' => 0, 'event_id' => $item['id']]);
  69. if (!$event_user) {
  70. $is_apply = -1;
  71. } else {
  72. $is_apply = $event_user->apply_status;
  73. }
  74. $item['is_apply'] = (int)$is_apply;
  75. }
  76. // 三种状态的统计
  77. $apply_num = $this->getUserEventCount($user_id, 0, 'start_time', '>', $now); // 申请中
  78. $underway_num = $this->getUserEventCount($user_id, 1, 'end_time', '>=', $now); // 进行中
  79. $finished_num = $this->getUserEventCount($user_id, 1, 'end_time', '<', $now); // 已结束
  80. return $this->asJson([
  81. 'code' => 0,
  82. 'msg' => 'success',
  83. 'data' => [
  84. 'list' => $list,
  85. 'row_count' => (int)$count,
  86. 'page_count' => (int)$pagination->getPageCount(),
  87. 'apply_num' => (int)$apply_num,
  88. 'underway_num' => (int)$underway_num,
  89. 'finished_num' => (int)$finished_num,
  90. ],
  91. ]);
  92. }
  93. /**
  94. * 模块名:getUserEventCount
  95. * 代码描述:获取某用户某一状态的报名活动数量
  96. * 作者:WPing丶
  97. * 请求方式:GET
  98. * 创建时间:2025/07/14 10:49:38
  99. *
  100. * @param int $user_id 用户ID
  101. * @param int $apply_status 报名状态:0=申请中,1=已通过
  102. * @param string $time_field 时间字段名(start_time 或 end_time)
  103. * @param string $operator 比较操作符:>、>=、<
  104. * @param int $timestamp 当前时间
  105. * @return int
  106. */
  107. private function getUserEventCount($user_id, $apply_status, $time_field, $operator, $timestamp)
  108. {
  109. return EventUser::find()->alias('eu')
  110. ->select('eu.id') // 提高 count 性能
  111. ->leftJoin(['e' => Event::tableName()], 'e.id = eu.event_id')
  112. ->where([
  113. 'eu.user_id' => $user_id,
  114. 'eu.is_delete' => 0,
  115. 'e.is_delete' => 0,
  116. 'eu.apply_status' => $apply_status,
  117. ])
  118. ->andWhere([$operator, "e.{$time_field}", $timestamp])
  119. ->count();
  120. }
  121. /**
  122. * 模块名:event-apply-form
  123. * 代码描述:报名自定义表单
  124. * 作者:WPing丶
  125. * 请求方式:GET
  126. * 创建时间:2025/07/14 11:31:46
  127. * @param int id 活动ID
  128. */
  129. public function actionEventApplyForm()
  130. {
  131. $user_id = get_user_id();
  132. $store_id = get_store_id();
  133. $id = (int)get_params('id');
  134. if (!$id) {
  135. return $this->asJson(['code' => 1, 'msg' => '活动ID不能为空']);
  136. }
  137. // 查询活动是否存在
  138. $event = Event::find()
  139. ->where([
  140. 'id' => $id,
  141. 'store_id' => $store_id,
  142. 'is_delete' => 0
  143. ])
  144. ->asArray()
  145. ->one();
  146. if (!$event) {
  147. return $this->asJson(['code' => 1, 'msg' => '活动不存在']);
  148. }
  149. // 查询用户是否已报名
  150. $eventUser = EventUser::find()
  151. ->where([
  152. 'user_id' => $user_id,
  153. 'event_id' => $id,
  154. 'is_delete' => 0
  155. ])
  156. ->orderBy('id DESC') // 保证拿到最新一条
  157. ->asArray()
  158. ->one();
  159. // 情况 1:已报名,审核中或通过审核admin
  160. if ($eventUser && $eventUser['apply_status'] == 0) {
  161. return $this->asJson(['code' => 1, 'msg' => '您已提交报名,正在审核中', 'eventUser' => $eventUser]);
  162. }
  163. if ($eventUser && $eventUser['apply_status'] == 1) {
  164. return $this->asJson(['code' => 1, 'msg' => '您已提交报名,审核成功']);
  165. }
  166. // 查询所有表单结构,按 form_type 分组返回
  167. $forms = EventForm::find()
  168. ->where([
  169. 'event_id' => $id,
  170. 'is_delete' => 0
  171. ])
  172. ->orderBy('sort ASC')
  173. ->asArray()
  174. ->all();
  175. // 构造分组结构:1=企业,2=机构
  176. $form_group = [];
  177. foreach ($forms as $form) {
  178. $form_group[$form['form_type']][] = $form;
  179. }
  180. // 情况 2:首次报名(没有任何报名记录)
  181. if (!$eventUser) {
  182. return $this->asJson([
  183. 'code' => 0,
  184. 'msg' => 'success',
  185. 'data' => [
  186. // 'event' => $event,
  187. 'form' => $form_group,
  188. ],
  189. 'setting' => $this->getSetting()
  190. ]);
  191. }
  192. // 情况 3:报名被拒绝 → 需要回填
  193. if ($eventUser['apply_status'] == 2) {
  194. // 回填数据查询
  195. $details = EventFormDetail::find()
  196. ->where([
  197. 'event_id' => $id,
  198. 'event_user_id' => $eventUser['id'],
  199. 'user_id' => $user_id,
  200. 'is_delete' => 0
  201. ])
  202. ->asArray()
  203. ->all();
  204. // 组装回填数据映射(按 form_type + key)
  205. $detail_map = [];
  206. foreach ($details as $item) {
  207. $detail_map[$item['form_type']][$item['key']] = $item['value'];
  208. }
  209. // 将回填数据合并到 form 结构中
  210. foreach ($form_group as $form_type => &$items) {
  211. foreach ($items as &$field) {
  212. $field['value'] = $detail_map[$form_type][$field['name']] ?? '';
  213. }
  214. }
  215. return $this->asJson([
  216. 'code' => 0,
  217. 'msg' => 'success',
  218. 'data' => [
  219. 'form' => $form_group, // 每个字段中已附带 value
  220. 'event_user' => $eventUser,
  221. 'reason_refusal' => $eventUser['reason_refusal'],
  222. ],
  223. 'setting' => $this->getSetting()
  224. ]);
  225. }
  226. // 其他未定义情况,兜底处理
  227. return $this->asJson(['code' => 1, 'msg' => '无法处理的报名状态']);
  228. }
  229. /**
  230. * 模块名:event-apply-form-post
  231. * 代码描述: 提交活动报名表单
  232. * 作者:WPing丶
  233. * 请求方式: POST
  234. * 创建时间:2025/07/14 15:58:20
  235. * @param int id 活动ID
  236. * @param int form_type 表单类型(1=企业,2=机构)
  237. * @param array form_data 用户填写的表单(字段名为中文名称)
  238. * @param string company_code 统一社会信息代码
  239. * @param string company_name 公司名称
  240. * @param string company_address 公司地址
  241. * @param string real_name 姓名
  242. * @param string phone 电话
  243. * @param string identity_card 身份证
  244. */
  245. public function actionEventApplyFormPost()
  246. {
  247. $user_id = get_user_id();
  248. $store_id = get_store_id();
  249. $id = (int)post_params('id');
  250. $form_type = (int)post_params('form_type', 1);
  251. $form_data = post_params('form_data', []);
  252. $company_code = post_params('company_code');
  253. $company_name = post_params('company_name');
  254. $company_address = post_params('company_address');
  255. $real_name = post_params('real_name');
  256. $phone = post_params('phone');
  257. $identity_card = post_params('identity_card');
  258. if (!$id || !$form_type || empty($form_data || !$company_code || !$company_name || $company_address || $real_name || $phone || $identity_card)) {
  259. return $this->asJson(['code' => 1, 'msg' => '参数不完整']);
  260. }
  261. // 检查活动是否存在
  262. $event = Event::findOne([
  263. 'id' => $id,
  264. 'store_id' => $store_id,
  265. 'is_delete' => 0
  266. ]);
  267. if (!$event) {
  268. return $this->asJson(['code' => 1, 'msg' => '活动不存在']);
  269. }
  270. if ($event->end_time < time()) {
  271. return $this->asJson(['code' => 1, 'msg' => '活动报名已结束无法报名']);
  272. }
  273. // 检查是否已报名
  274. $exist = EventUser::findOne([
  275. 'event_id' => $id,
  276. 'user_id' => $user_id,
  277. 'is_delete' => 0,
  278. 'apply_status' => [0, 1]
  279. ]);
  280. if ($exist) {
  281. return $this->asJson(['code' => 1, 'msg' => '您已报名,请勿重复提交']);
  282. }
  283. $phone_exist = EventUser::findOne([
  284. 'event_id' => $id,
  285. 'phone' => $phone,
  286. 'is_delete' => 0,
  287. 'apply_status' => [0, 1]
  288. ]);
  289. if ($phone_exist) {
  290. return $this->asJson(['code' => 1, 'msg' => '此手机号已报名,请勿重复提交']);
  291. }
  292. // 检查限制人数 attendance = 0 不限制
  293. if ($event->attendance > 0) {
  294. $attendance = EventUser::find()
  295. ->where([
  296. 'event_id' => $id,
  297. 'is_delete' => 0,
  298. 'apply_status' => 1,
  299. 'company_code' => $company_code
  300. ])->count();
  301. if ($attendance >= $event->attendance) {
  302. return $this->asJson(['code' => 1, 'msg' => '【' . $company_name . '】报名已达到限制人数,请与活动主办方联系。']);
  303. }
  304. }
  305. // 开启事务
  306. $transaction = Yii::$app->db->beginTransaction();
  307. try {
  308. // 写入 event_user 表
  309. $eventUser = EventUser::findOne([
  310. 'event_id' => $id,
  311. 'user_id' => $user_id,
  312. 'is_delete' => 0,
  313. 'apply_status' => 2,
  314. 'store_id' => $store_id
  315. ]);
  316. if (!$eventUser) {
  317. $eventUser = new EventUser();
  318. $eventUser->store_id = $store_id;
  319. $eventUser->event_id = $id;
  320. $eventUser->user_id = $user_id;
  321. }
  322. $eventUser->company_code = $company_code;
  323. $eventUser->company_name = $company_name;
  324. $eventUser->company_address = $company_address;
  325. $eventUser->real_name = $real_name;
  326. $eventUser->phone = $phone;
  327. $eventUser->identity_card = $identity_card;
  328. $eventUser->apply_status = 0;
  329. $eventUser->created_at = time();
  330. $eventUser->updated_at = time();
  331. $eventUser->is_delete = 0;
  332. $eventUser->hotel = '{"title":"","data":[]}';
  333. $eventUser->restaurant = '{"title":"","data":[]}';
  334. $eventUser->itinerary = '{"title":"","data":[]}';
  335. $eventUser->form_type = $form_type;
  336. if (!$eventUser->save()) {
  337. throw new \Exception('保存报名主表失败');
  338. }
  339. // 查询字段定义
  340. $formList = EventForm::find()
  341. ->where([
  342. 'event_id' => $id,
  343. 'form_type' => $form_type,
  344. 'is_delete' => 0
  345. ])
  346. ->asArray()
  347. ->all();
  348. $formNameMap = []; // name => type
  349. foreach ($formList as $item) {
  350. $formNameMap[$item['name']] = $item['type'] ?? 'text';
  351. }
  352. // 写入表单明细
  353. EventFormDetail::updateAll(['is_delete' => 1], ['event_user_id' => $eventUser->id]);
  354. foreach ($form_data as $name => $value) {
  355. if (!isset($formNameMap[$name])) {
  356. continue;
  357. }
  358. $detail = new EventFormDetail();
  359. $detail->store_id = $store_id;
  360. $detail->event_user_id = $eventUser->id;
  361. $detail->event_id = $id;
  362. $detail->user_id = $user_id;
  363. $detail->form_type = $form_type;
  364. $detail->key = $name;
  365. $detail->value = $value;
  366. $detail->type = $formNameMap[$name];
  367. $detail->is_delete = 0;
  368. if (!$detail->save()) {
  369. throw new \Exception("保存表单字段【{$name}】失败");
  370. }
  371. }
  372. $transaction->commit();
  373. return $this->asJson(['code' => 0, 'msg' => '报名成功,等待审核']);
  374. } catch (\Exception $e) {
  375. $transaction->rollBack();
  376. return $this->asJson(['code' => 1, 'msg' => '报名失败:' . $e->getMessage()]);
  377. }
  378. }
  379. /**
  380. * 模块名:user-event-list
  381. * 代码描述: 我的报名
  382. * 作者:WPing丶
  383. * 请求方式:GET
  384. * 创建时间:2025/07/14 18:09:26
  385. * @param int page
  386. * @param int limit
  387. * @param string status 活动状态:all 全部 | audit 审核中 | underway 进行中 | done 已结束
  388. * @param string lng 经度
  389. * @param string lat 纬度
  390. */
  391. public function actionUserEventList()
  392. {
  393. $user_id = get_user_id();
  394. $store_id = get_store_id();
  395. $status = get_params('status', 'all'); // all | audit | underway | done
  396. $page = (int)get_params('page', 1);
  397. $limit = (int)get_params('limit', 10);
  398. $limit = ($limit <= 0) ? 10 : min($limit, 100);
  399. //用户当前的经纬度,如果未传经纬度,说明用户拒绝定位,则后续酒店、饭店、行程信息处理时距离返回空即可
  400. $lng = get_params('lng');
  401. $lat = get_params('lat');
  402. $query = EventUser::find()->alias('eu')
  403. ->leftJoin(['e' => Event::tableName()], 'e.id = eu.event_id')
  404. ->where([
  405. 'eu.user_id' => $user_id,
  406. 'eu.is_delete' => 0,
  407. 'e.is_delete' => 0,
  408. 'e.store_id' => $store_id,
  409. ]);
  410. // 状态筛选
  411. $now = time();
  412. if ($status === 'audit') {
  413. $query->andWhere(['eu.apply_status' => [0, 2]]);
  414. $query->andWhere(['>', 'e.start_time', $now]);
  415. } elseif ($status === 'underway') {
  416. $query->andWhere(['eu.apply_status' => 1]);
  417. $query->andWhere(['>=', 'e.end_time', $now]);
  418. } elseif ($status === 'done') {
  419. $query->andWhere(['eu.apply_status' => 1]);
  420. $query->andWhere(['<', 'e.end_time', $now]);
  421. }
  422. $count = $query->count();
  423. $pagination = new Pagination([
  424. 'totalCount' => $count,
  425. 'pageSize' => $limit,
  426. 'page' => $page - 1
  427. ]);
  428. $list = $query->select([
  429. 'eu.id',
  430. 'e.id AS event_id',
  431. 'e.name',
  432. 'e.start_time',
  433. 'e.end_time',
  434. 'e.cover_pic',
  435. 'eu.reason_refusal',
  436. 'eu.apply_status',
  437. 'eu.hotel',
  438. 'eu.restaurant',
  439. 'eu.itinerary',
  440. ])
  441. ->orderBy('e.start_time DESC')
  442. ->limit($pagination->limit)
  443. ->offset($pagination->offset)
  444. ->asArray()
  445. ->all();
  446. // 补充 status_text 和报名人数
  447. foreach ($list as &$item) {
  448. $apply_status = (int)$item['apply_status'];
  449. $start = (int)$item['start_time'];
  450. $end = (int)$item['end_time'];
  451. if ($apply_status === 0) {
  452. $item['status_text'] = '报名中·待审核';
  453. } elseif ($apply_status === 1 && $end >= $now) {
  454. $item['status_text'] = '进行中';
  455. } elseif ($apply_status === 1 && $end < $now) {
  456. $item['status_text'] = '已结束';
  457. } elseif ($apply_status === 2) {
  458. $item['status_text'] = '报名中·已驳回';
  459. } else {
  460. $item['status_text'] = '未知状态';
  461. }
  462. //酒店、饭店、行程数组处理
  463. //code...
  464. $has_location = $lat && $lng;
  465. // 计算距离(单位:米)
  466. $calcDistance = function ($lat1, $lng1, $lat2, $lng2) {
  467. $earthRadius = 6371000; // 米
  468. $lat1 = deg2rad($lat1);
  469. $lng1 = deg2rad($lng1);
  470. $lat2 = deg2rad($lat2);
  471. $lng2 = deg2rad($lng2);
  472. $dlat = $lat2 - $lat1;
  473. $dlng = $lng2 - $lng1;
  474. $a = sin($dlat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dlng / 2) ** 2;
  475. $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
  476. return round($earthRadius * $c, 2);
  477. };
  478. // 格式化距离显示
  479. $formatDistance = function ($distance) {
  480. if ($distance < 1000) {
  481. return (int)$distance . 'm';
  482. }
  483. return round($distance / 1000, 1) . 'km';
  484. };
  485. // 转换 json + 加入距离
  486. $parseJsonWithDistance = function ($json_str, $user_lat, $user_lng) use ($has_location, $calcDistance, $formatDistance) {
  487. $result = json_decode($json_str, true);
  488. if (!is_array($result) || !isset($result['data'])) {
  489. return null;
  490. }
  491. foreach ($result['data'] as &$item) {
  492. if (!$has_location || !isset($item['lat']) || !isset($item['lng'])) {
  493. $item['distance'] = null;
  494. continue;
  495. }
  496. $distance_val = $calcDistance((float)$user_lat, (float)$user_lng, (float)$item['lat'], (float)$item['lng']);
  497. $item['distance'] = $formatDistance($distance_val);
  498. }
  499. return $result;
  500. };
  501. // 应用处理
  502. $item['hotel'] = $parseJsonWithDistance($item['hotel'], $lat, $lng);
  503. $item['restaurant'] = $parseJsonWithDistance($item['restaurant'], $lat, $lng);
  504. $item['itinerary'] = $parseJsonWithDistance($item['itinerary'], $lat, $lng);
  505. // 查询已报名人数
  506. $item['count'] = EventUser::find()
  507. ->where(['event_id' => $item['event_id'], 'is_delete' => 0])
  508. ->count();
  509. }
  510. return $this->asJson([
  511. 'code' => 0,
  512. 'msg' => 'success',
  513. 'data' => [
  514. 'list' => $list,
  515. 'row_count' => $count,
  516. 'page_count' => $pagination->pageCount
  517. ],
  518. 'setting' => $this->getSetting()
  519. ]);
  520. }
  521. /**
  522. * 模块名:get-event-user-qr
  523. * 代码描述:签到二维码
  524. * 作者:WPing丶
  525. * 请求方式:GET
  526. * 创建时间:2025/07/14 19:14:25
  527. * @param int id
  528. */
  529. public function actionGetEventUserQr()
  530. {
  531. $user_id = get_user_id();
  532. $store_id = get_store_id();
  533. $id = (int)get_params('id');
  534. if (!$id) {
  535. return $this->asJson(['code' => 1, 'msg' => '缺少参数:id']);
  536. }
  537. // 查询报名记录
  538. $eventUser = EventUser::find()
  539. ->where([
  540. 'id' => $id,
  541. 'user_id' => $user_id,
  542. 'store_id' => $store_id,
  543. 'is_delete' => 0,
  544. 'apply_status' => 1, // 只允许通过审核的用户查看二维码
  545. ])
  546. ->asArray()
  547. ->one();
  548. if (!$eventUser) {
  549. return $this->asJson(['code' => 1, 'msg' => '报名记录不存在或未审核通过']);
  550. }
  551. return $this->asJson([
  552. 'code' => 0,
  553. 'msg' => 'success',
  554. 'data' => [
  555. 'real_name' => $eventUser['real_name'] ?? '',
  556. 'phone' => $eventUser['phone'] ?? '',
  557. 'identity_card' => $eventUser['identity_card'] ?? '',
  558. ]
  559. ]);
  560. }
  561. /**
  562. * 模块名:getSetting
  563. * 代码描述:获取设置项
  564. * 作者:WPing丶
  565. * 请求方式:private
  566. * 创建时间:2025/07/16 10:45:11
  567. */
  568. private function getSetting()
  569. {
  570. $store_id = get_store_id();
  571. $setting = EventSetting::findOne(['store_id' => $store_id, 'is_delete' => 0]);
  572. return [
  573. 'banner' => json_decode($setting->banner, true)
  574. ];
  575. }
  576. /**
  577. * 模块名:do-sign-by-staff
  578. * 代码描述:确认签到
  579. * 作者:WPing丶
  580. * 请求方式:POST
  581. * 创建时间:2025/07/16 11:28:38
  582. * @param int user_id
  583. * @param int event_id
  584. */
  585. public function actionDoSignByStaff()
  586. {
  587. $store_id = get_store_id();
  588. $staff_id = get_user_id(); // 当前登录用户(签到员)
  589. $user_id = (int)post_params('user_id'); // 被签到用户
  590. $event_id = (int)post_params('event_id');
  591. if (!$user_id || !$event_id) {
  592. return $this->asJson(['code' => 1, 'msg' => '参数错误']);
  593. }
  594. // 检查当前用户是否是有效的签到员
  595. $staff = EventSignStaff::findOne(['user_id' => $staff_id, 'is_delete' => 0]);
  596. if (!$staff) {
  597. return $this->asJson(['code' => 1, 'msg' => '无权限']);
  598. }
  599. // 查找报名记录
  600. $eventUser = EventUser::findOne([
  601. 'user_id' => $user_id,
  602. 'event_id' => $event_id,
  603. 'is_delete' => 0,
  604. 'apply_status' => 1
  605. ]);
  606. if (!$eventUser) {
  607. return $this->asJson(['code' => 1, 'msg' => '报名记录不存在或未通过审核']);
  608. }
  609. // 检查是否已签到
  610. if ($eventUser->sign_time > 0) {
  611. return $this->asJson(['code' => 1, 'msg' => '用户已签到']);
  612. }
  613. $transaction = Yii::$app->db->beginTransaction();
  614. try {
  615. $time = time();
  616. // 写入签到日志表
  617. $log = new EventSignLog();
  618. $log->store_id = $store_id;
  619. $log->is_delete = 0;
  620. $log->staff_id = $staff->id;
  621. $log->user_id = $user_id;
  622. $log->event_id = $event_id;
  623. $log->event_user_id = $eventUser->id;
  624. if (!$log->save()) {
  625. throw new \Exception('签到记录保存失败');
  626. }
  627. // 更新报名表中的签到时间
  628. $eventUser->sign_time = $time;
  629. if (!$eventUser->save()) {
  630. throw new \Exception('签到时间更新失败');
  631. }
  632. $transaction->commit();
  633. return $this->asJson(['code' => 0, 'msg' => '签到成功']);
  634. } catch (\Exception $e) {
  635. $transaction->rollBack();
  636. return $this->asJson(['code' => 1, 'msg' => '签到失败:' . $e->getMessage()]);
  637. }
  638. }
  639. /**
  640. * 模块名:my-sign-record
  641. * 代码描述:签到员记录
  642. * 作者:WPing丶
  643. * 请求方式:GET
  644. * 创建时间:2025/07/16 14:17:38
  645. * @param int page
  646. * @param int limit
  647. */
  648. public function actionMySignRecord()
  649. {
  650. $store_id = get_store_id();
  651. $staff_id = EventSignStaff::findOne(['user_id' => get_user_id(), 'is_delete' => 0])->id;
  652. $page = (int)get_params('page', 1);
  653. $limit = (int)get_params('limit', 10);
  654. if (!$staff_id) {
  655. return $this->asJson(['code' => 1, 'msg' => '当前用户不是签到员']);
  656. }
  657. $query = EventSignLog::find()->alias('l')
  658. ->leftJoin(['u' => EventUser::tableName()], 'u.id = l.event_user_id')
  659. ->leftJoin(['e' => Event::tableName()], 'e.id = l.event_id')
  660. ->where([
  661. 'l.store_id' => $store_id,
  662. 'l.is_delete' => 0,
  663. 'l.staff_id' => $staff_id
  664. ]);
  665. $count = $query->count();
  666. $pagination = new Pagination([
  667. 'totalCount' => $count,
  668. 'pageSize' => $limit,
  669. 'page' => $page - 1
  670. ]);
  671. $list = $query->select([
  672. 'u.sign_time',
  673. 'u.real_name',
  674. 'u.phone',
  675. 'e.name AS event_name'
  676. ])
  677. ->orderBy('l.created_at DESC')
  678. ->offset($pagination->offset)
  679. ->limit($pagination->limit)
  680. ->asArray()
  681. ->all();
  682. return $this->asJson([
  683. 'code' => 0,
  684. 'msg' => 'success',
  685. 'data' => [
  686. 'list' => $list,
  687. 'row_count' => $count,
  688. 'page_count' => $pagination->pageCount,
  689. ]
  690. ]);
  691. }
  692. /**
  693. * 模块名: sign-preview
  694. * 代码描述: 签到员预览页
  695. * 请求方式: GET
  696. * @param int id event_user主键ID
  697. */
  698. public function actionSignPreview()
  699. {
  700. $store_id = get_store_id();
  701. $id = (int)get_params('id');
  702. if (!$id) {
  703. return $this->asJson(['code' => 1, 'msg' => '缺少报名记录ID']);
  704. }
  705. $staff_id = get_user_id(); // 当前登录用户(签到员)
  706. // 检查当前用户是否是有效的签到员
  707. $staff = EventSignStaff::findOne(['user_id' => $staff_id, 'is_delete' => 0]);
  708. if (!$staff) {
  709. return $this->asJson(['code' => 1, 'msg' => '无权限']);
  710. }
  711. // 联查 event_user 和 event 表(主表 event_user)
  712. $info = EventUser::find()
  713. ->alias('eu')
  714. ->leftJoin(['e' => Event::tableName()], 'e.id = eu.event_id')
  715. ->where([
  716. 'eu.id' => $id,
  717. 'eu.is_delete' => 0,
  718. 'eu.store_id' => $store_id,
  719. 'e.is_delete' => 0,
  720. ])
  721. ->select([
  722. 'eu.id',
  723. 'eu.real_name',
  724. 'eu.phone',
  725. 'eu.identity_card',
  726. 'eu.company_code',
  727. 'eu.company_name',
  728. 'eu.company_address',
  729. 'eu.event_id',
  730. 'e.name',
  731. 'e.address',
  732. 'e.start_time',
  733. 'e.end_time'
  734. ])
  735. ->asArray()
  736. ->one();
  737. if (!$info) {
  738. return $this->asJson(['code' => 1, 'msg' => '记录不存在,id=【' . $id . '】']);
  739. }
  740. // 查询自定义表单字段(event_form_detail)
  741. $formDetail = EventFormDetail::find()
  742. ->where([
  743. 'event_user_id' => $id,
  744. 'is_delete' => 0
  745. ])
  746. ->select(['key', 'value'])
  747. ->orderBy('id ASC')
  748. ->asArray()
  749. ->all();
  750. return $this->asJson([
  751. 'code' => 0,
  752. 'msg' => 'success',
  753. 'data' => [
  754. 'info' => $info,
  755. 'form_detail' => $formDetail
  756. ]
  757. ]);
  758. }
  759. /**
  760. * 模块名:get-event-sign-qrcode
  761. * 代码描述:获取签到二维码
  762. * 作者:WPing丶
  763. * 请求方式:GET
  764. * 创建时间:2025/07/16 16:28:11
  765. * @param int event_id
  766. */
  767. public function actionGetEventSignQrcode()
  768. {
  769. $event_id = (int)get_params('event_id');
  770. $user_id = get_user_id();
  771. if (!$event_id) {
  772. return $this->asJson(['code' => 1, 'msg' => '参数缺失']);
  773. }
  774. // 查询报名记录
  775. $eventUser = EventUser::find()
  776. ->where([
  777. 'event_id' => $event_id,
  778. 'is_delete' => 0,
  779. 'user_id' => $user_id,
  780. 'apply_status' => 1, // 只允许通过审核的用户查看二维码
  781. ])
  782. ->asArray()
  783. ->one();
  784. if (!$eventUser) {
  785. return $this->asJson(['code' => 1, 'msg' => '报名记录不存在或未审核通过']);
  786. }
  787. $event_user_id = $eventUser['id'];
  788. $page = "eventPlanning/verifyCodeResult/verifyCodeResult";
  789. $scene = "event_user_id:{$event_user_id}";
  790. $file_name = [
  791. 'url_path' => ''
  792. ];
  793. if (is_wechat_platform()) {
  794. // 微信平台小程序码
  795. $file_name = ShareQrcode::wxQrcode($page, $scene);
  796. if ($file_name['code'] != 0) {
  797. return $this->asJson($file_name);
  798. }
  799. }
  800. if (is_alipay_platform()) {
  801. // 支付宝小程序码
  802. $request = new AlipayOpenAppQrcodeCreateRequest();
  803. $param = [
  804. 'url_param' => $page,
  805. 'query_param' => $scene,
  806. 'describe' => "签到预览页二维码"
  807. ];
  808. $aliConfigQr = $this->aliConfigQr($request, $param);
  809. if ($aliConfigQr->code === "10000") {
  810. $file_name['url_path'] = $aliConfigQr->qr_code_url_circle_blue;
  811. }
  812. }
  813. if (is_h5() || is_app_platform()) {
  814. $fileName = md5($page . $scene . get_store_id());
  815. $dir = \Yii::$app->runtimePath . '/image/wx_qrcode';
  816. if (!is_dir($dir)) {
  817. mkdir($dir, 0777, true);
  818. }
  819. $save_path = $dir . '/' . $fileName . '.jpg';
  820. $url = \Yii::$app->request->hostInfo . '/h5/#/' . $page . '?scene=' . urlencode($scene);
  821. QrCode::image($url, 600, false, 'L', 'JPEG', 0, ['255,255,255', '0,0,0'], 1, false, $save_path);
  822. $file_name['url_path'] = str_replace('http://', 'https://', \Yii::$app->request->hostInfo . '/runtime/image/wx_qrcode/' . $fileName . '.jpg');
  823. }
  824. return $this->asJson([
  825. 'code' => 0,
  826. 'msg' => 'success',
  827. 'data' => [
  828. 'qr_code' => $file_name['url_path'],
  829. 'real_name' => $eventUser['real_name'] ?? '',
  830. 'phone' => $eventUser['phone'] ?? '',
  831. 'identity_card' => $eventUser['identity_card'] ?? '',
  832. ]
  833. ]);
  834. }
  835. /**
  836. * 模块名:get-event-detail
  837. * 代码描述:获取报名详情
  838. * 作者:WPing丶
  839. * 请求方式:GET
  840. * 创建时间:2025/07/21 11:55:40
  841. * @param int event_user_id
  842. * @param int event_id
  843. * @param int lng 经度
  844. * @param int lat 纬度
  845. */
  846. public function actionGetEventDetail()
  847. {
  848. $event_user_id = (int)get_params('event_user_id');
  849. $event_id = (int)get_params('event_id');
  850. $user_id = get_user_id();
  851. //用户当前的经纬度,如果未传经纬度,说明用户拒绝定位,则后续酒店、饭店、行程信息处理时距离返回空即可
  852. $lng = get_params('lng');
  853. $lat = get_params('lat');
  854. $store_id = get_store_id();
  855. if (!$event_user_id && !$event_id) {
  856. return $this->asJson(['code' => 1, 'msg' => '参数缺失']);
  857. }
  858. // 查询报名记录
  859. $query = EventUser::find()
  860. ->where([
  861. 'store_id' => $store_id,
  862. 'is_delete' => 0,
  863. 'apply_status' => 1, // 只允许通过审核的用户查看二维码
  864. ]);
  865. if ($event_user_id) {
  866. $query->andWhere(['id' => $event_user_id]);
  867. } elseif ($event_id) {
  868. $query->andWhere(['event_id' => $event_id, 'user_id' => $user_id]);
  869. }
  870. $eventUser = $query
  871. ->asArray()
  872. ->one();
  873. if (!$eventUser) {
  874. return $this->asJson(['code' => 1, 'msg' => '报名记录不存在或未审核通过']);
  875. }
  876. $data = [];
  877. foreach ($eventUser as $k => $item) {
  878. if ($k == 'hotel' || $k == 'restaurant' || $k == 'itinerary') {
  879. /* begin 2025/07/24 16:00:21 WPing丶 */
  880. //酒店、饭店、行程数组处理
  881. //code...
  882. $has_location = $lat && $lng;
  883. // 计算距离(单位:米)
  884. $calcDistance = function ($lat1, $lng1, $lat2, $lng2) {
  885. $earthRadius = 6371000; // 米
  886. $lat1 = deg2rad($lat1);
  887. $lng1 = deg2rad($lng1);
  888. $lat2 = deg2rad($lat2);
  889. $lng2 = deg2rad($lng2);
  890. $dlat = $lat2 - $lat1;
  891. $dlng = $lng2 - $lng1;
  892. $a = sin($dlat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dlng / 2) ** 2;
  893. $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
  894. return round($earthRadius * $c, 2);
  895. };
  896. // 格式化距离显示
  897. $formatDistance = function ($distance) {
  898. if ($distance < 1000) {
  899. return (int)$distance . 'm';
  900. }
  901. return round($distance / 1000, 1) . 'km';
  902. };
  903. // 转换 json + 加入距离
  904. $parseJsonWithDistance = function ($json_str, $user_lat, $user_lng) use ($has_location, $calcDistance, $formatDistance) {
  905. $result = json_decode($json_str, true);
  906. if (!is_array($result) || !isset($result['data'])) {
  907. return null;
  908. }
  909. foreach ($result['data'] as &$item) {
  910. if (!$has_location || !isset($item['lat']) || !isset($item['lng'])) {
  911. $item['distance'] = null;
  912. continue;
  913. }
  914. $distance_val = $calcDistance((float)$user_lat, (float)$user_lng, (float)$item['lat'], (float)$item['lng']);
  915. $item['distance'] = $formatDistance($distance_val);
  916. }
  917. return $result;
  918. };
  919. // $arr = json_decode($item, true);
  920. // 应用处理
  921. $arr = $parseJsonWithDistance($item, $lat, $lng);
  922. /* end */
  923. if (count($arr['data']) == 0) {
  924. $data[$k] = [];
  925. } else {
  926. $data[$k] = $arr;
  927. }
  928. } else {
  929. $data[$k] = $item;
  930. }
  931. }
  932. $setting = EventSetting::findOne(['store_id' => $store_id, 'is_delete' => 0]);
  933. return $this->asJson([
  934. 'code' => 0,
  935. 'msg' => 'success',
  936. 'data' => $data,
  937. 'banner' => json_decode($setting->banner, true)
  938. ]);
  939. }
  940. /**
  941. * 模块名:event-msg-list
  942. * 代码描述:活动信息列表页
  943. * 作者:WPing丶
  944. * 请求方式:GET
  945. * 创建时间:2025/07/24 14:21:14
  946. * @param int event_id 活动ID
  947. * @param string type 信息类型:sign=签到;hotel=住宿信息;restaurant=用餐信息;itinerary=行程信息;
  948. */
  949. public function actionEventMsgList()
  950. {
  951. $user_id = get_user_id();
  952. $store_id = get_store_id();
  953. $event_id = get_params('event_id');
  954. $list = EventUser::find()->alias('eu')
  955. ->leftJoin(['e' => Event::tableName()], 'eu.event_id = e.id')
  956. ->where([
  957. 'eu.user_id' => $user_id,
  958. 'eu.store_id' => $store_id,
  959. 'eu.is_delete' => 0,
  960. 'e.is_delete' => 0,
  961. 'eu.apply_status' => 1,
  962. ])
  963. ->select('eu.id as event_user_id, e.name')
  964. ->asArray()
  965. ->all();
  966. return $this->asJson([
  967. 'code' => 0,
  968. 'msg' => 'success',
  969. 'data' => $list
  970. ]);
  971. }
  972. }