MemberService.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2017~2021 LARAVEL研发中心
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://www.laravel.cn
  8. // +----------------------------------------------------------------------
  9. // | Author: laravel开发员 <laravel.qq.com>
  10. // +----------------------------------------------------------------------
  11. namespace App\Services\Common;
  12. use App\Models\ActionLogModel;
  13. use App\Models\MemberModel;
  14. use App\Services\BaseService;
  15. use App\Services\RedisService;
  16. use Illuminate\Support\Facades\Cache;
  17. use Illuminate\Support\Facades\DB;
  18. use InvalidArgumentException;
  19. /**
  20. * 用户(会员)管理-服务类
  21. * @author laravel开发员
  22. * @since 2020/11/11
  23. * Class MemberService
  24. * @package App\Services\Common
  25. */
  26. class MemberService extends BaseService
  27. {
  28. public static $instance = null;
  29. /**
  30. * 构造函数
  31. * @author laravel开发员
  32. * @since 2020/11/11
  33. * MemberService constructor.
  34. */
  35. public function __construct()
  36. {
  37. $this->model = new MemberModel();
  38. }
  39. /**
  40. * 静态入口
  41. */
  42. public static function make()
  43. {
  44. if (!self::$instance) {
  45. self::$instance = new static();
  46. }
  47. return self::$instance;
  48. }
  49. /**
  50. * 列表
  51. * @param $params
  52. * @param int $pageSize
  53. * @return array
  54. */
  55. public function getDataList($params, $pageSize = 15)
  56. {
  57. $where = ['a.mark' => 1];
  58. $status = isset($params['status']) ? $params['status'] : 0;
  59. if ($status > 0) {
  60. $where['a.status'] = $status;
  61. }
  62. $query = $this->model->from('member as a')
  63. ->where($where)
  64. ->where(function ($query) use ($params) {
  65. $keyword = isset($params['keyword']) ? $params['keyword'] : '';
  66. if ($keyword) {
  67. $query->where('a.nickname', 'like', "%{$keyword}%")->orWhere('a.realname', 'like', "%{$keyword}%");
  68. }
  69. })
  70. ->where(function ($query) use ($params) {
  71. $mobile = isset($params['mobile']) ? trim($params['mobile']) : '';
  72. if ($mobile) {
  73. $query->where('a.mobile', 'like', "%{$mobile}%");
  74. }
  75. })
  76. ->where(function ($query) use ($params) {
  77. $confirmStatus = isset($params['confirm_status']) ? $params['confirm_status'] : -1;
  78. if ($confirmStatus == 0) {
  79. $query->whereIn('a.confirm_status', [2, 3]);
  80. } else if ($confirmStatus > 0) {
  81. $query->where('a.confirm_status', $confirmStatus);
  82. }
  83. })
  84. ->select(['a.*']);
  85. $confirmStatus = isset($params['confirm_status']) ? $params['confirm_status'] : -1;
  86. if ($confirmStatus == 0) {
  87. $query->orderBy('a.confirm_status', 'asc')->orderBy('a.create_time', 'desc');
  88. } else {
  89. $query->orderBy('a.create_time', 'desc')->orderBy('a.id', 'desc');
  90. }
  91. $list = $query->paginate($pageSize > 0 ? $pageSize : 9999999);
  92. $list = $list ? $list->toArray() : [];
  93. return [
  94. 'pageSize' => $pageSize,
  95. 'total' => isset($list['total']) ? $list['total'] : 0,
  96. 'list' => isset($list['data']) ? $list['data'] : []
  97. ];
  98. }
  99. /**
  100. * 按日期统计注册用户数
  101. * @param $beginAt
  102. * @param $endAt
  103. * @param int $status
  104. * @return mixed
  105. */
  106. public function getRegisterCount($beginAt = 0, $endAt = 0, $status = 1)
  107. {
  108. $cacheKey = "caches:members:count_{$beginAt}_{$endAt}_{$status}";
  109. $data = RedisService::get($cacheKey);
  110. if ($data) {
  111. return $data;
  112. }
  113. $where = ['mark' => 1, 'status' => $status];
  114. if ($status == 2) {
  115. $where['status'] = 1;
  116. }
  117. $data = $this->model->where($where)->where(function ($query) use ($beginAt, $endAt) {
  118. if ($beginAt && $endAt) {
  119. $query->whereBetween('create_time', [strtotime($beginAt), strtotime($endAt)]);
  120. } else if ($beginAt) {
  121. $query->where('create_time', '>=', strtotime($beginAt));
  122. }
  123. })->count('id');
  124. if ($data) {
  125. RedisService::set($cacheKey, $data, rand(300, 600));
  126. }
  127. }
  128. public function countUsers($type = 'today', $start_time = null, $end_time = null)
  129. {
  130. // 生成缓存的唯一键,依据 type 和时间范围
  131. $cacheKey = "user_count_{$type}";
  132. if ($start_time && $end_time) {
  133. $cacheKey .= "_{$start_time}_{$end_time}";
  134. }
  135. // 检查缓存中是否已经存在该统计数据
  136. $count = Cache::get($cacheKey);
  137. if ($count === null) {
  138. // 如果缓存中没有数据,则执行数据库查询
  139. $query = DB::table('member');
  140. // 如果 type 是 'all',则不进行时间筛选,查询所有记录
  141. if ($type === 'all') {
  142. // 不添加时间过滤条件,查询所有用户数量
  143. $count = $query->count();
  144. } else {
  145. // 根据传入的时间范围进行过滤
  146. if ($start_time && $end_time) {
  147. $query->whereBetween('create_time', [$start_time, $end_time]);
  148. }
  149. // 根据不同的统计类型进行筛选
  150. switch ($type) {
  151. case 'today':
  152. $query->whereDate('create_time', '=', now()->toDateString()); // 今天
  153. break;
  154. case 'yesterday':
  155. $query->whereDate('create_time', '=', now()->subDay()->toDateString()); // 昨天
  156. break;
  157. case 'month':
  158. $query->whereMonth('create_time', '=', now()->month) // 当前月
  159. ->whereYear('create_time', '=', now()->year);
  160. break;
  161. case 'last_month':
  162. $query->whereMonth('create_time', '=', now()->subMonth()->month) // 上个月
  163. ->whereYear('create_time', '=', now()->subMonth()->year);
  164. break;
  165. case 'year':
  166. $query->whereYear('create_time', '=', now()->year); // 当前年
  167. break;
  168. default:
  169. throw new InvalidArgumentException("无效的类型");
  170. }
  171. // 执行查询并获取统计数据
  172. $count = $query->count();
  173. }
  174. // 将查询结果缓存,缓存时间为 10 分钟
  175. // Cache::put($cacheKey, $count, now()->addMinutes(10));
  176. }
  177. return $count;
  178. }
  179. /**
  180. * 用户选项
  181. * @return array
  182. */
  183. public function options()
  184. {
  185. // 获取参数
  186. $param = request()->all();
  187. // 用户ID
  188. $keyword = getter($param, "keyword");
  189. $parentId = getter($param, "parent_id");
  190. $userId = getter($param, "user_id");
  191. $datas = $this->model->where(function ($query) use ($parentId) {
  192. if ($parentId) {
  193. $query->where(['id' => $parentId, 'mark' => 1]);
  194. } else {
  195. $query->where(['status' => 1, 'mark' => 1]);
  196. }
  197. })
  198. ->where(function ($query) use ($userId) {
  199. if ($userId) {
  200. $query->whereNotIn('id', [$userId]);
  201. }
  202. })
  203. ->where(function ($query) use ($keyword) {
  204. if ($keyword) {
  205. $query->where('nickname', 'like', "%{$keyword}%")
  206. ->orWhere('mobile', 'like', "%{$keyword}%");
  207. }
  208. })
  209. ->select(['id', 'realname', 'mobile', 'code', 'nickname', 'status'])
  210. ->get();
  211. return $datas ? $datas->toArray() : [];
  212. }
  213. /**
  214. * 添加或编辑会员
  215. * @return array
  216. */
  217. public function edit()
  218. {
  219. $data = request()->all();
  220. // 允许更新的字段(对应前端 dialog)
  221. $allowedFields = [
  222. 'id',
  223. 'avatar',
  224. 'mobile',
  225. 'nickname',
  226. 'is_vip_1',
  227. 'vip_1_expired',
  228. 'is_vip_2',
  229. 'vip_2_expired',
  230. 'is_vip_3',
  231. 'vip_3_expired',
  232. 'is_vip_4',
  233. 'vip_4_expired',
  234. 'is_video_vip',
  235. 'video_vip_expired',
  236. 'entry_type',
  237. 'need_paper'
  238. ];
  239. // 只保留允许字段
  240. $data = array_intersect_key($data, array_flip($allowedFields));
  241. // 头像与证件图片处理
  242. foreach (['avatar', 'driving_license', 'drivers_license'] as $field) {
  243. if (!empty($data[$field])) {
  244. $data[$field] = get_image_path($data[$field]);
  245. }
  246. }
  247. $id = $data['id'] ?? 0;
  248. $mobile = trim($data['mobile'] ?? '');
  249. if ($mobile) {
  250. $checkId = $this->model->where(['mobile' => $mobile, 'mark' => 1])->value('id');
  251. if ($checkId && $checkId != $id) {
  252. return message('手机号已存在', false);
  253. }
  254. }
  255. // 密码加密
  256. if (!empty($data['password'])) {
  257. $data['password'] = get_password(trim($data['password']));
  258. }
  259. // VIP字段处理(可选)
  260. $vipFields = ['is_vip_1', 'is_vip_2', 'is_vip_3', 'is_vip_4', 'is_video_vip'];
  261. $vipMapping = [
  262. 'is_vip_1' => 'vip_1_expired',
  263. 'is_vip_2' => 'vip_2_expired',
  264. 'is_vip_3' => 'vip_3_expired',
  265. 'is_vip_4' => 'vip_4_expired',
  266. 'is_video_vip' => 'video_vip_expired'
  267. ];
  268. foreach ($vipFields as $vipField) {
  269. if (isset($data[$vipField])) {
  270. $data[$vipField] = intval($data[$vipField]);
  271. $expireField = $vipMapping[$vipField];
  272. // 获取当前数据库状态
  273. $oldStatus = $this->model->where('id', $id)->value($vipField);
  274. $oldExpire = $this->model->where('id', $id)->value($expireField);
  275. // 调用公共方法计算新过期时间
  276. if($data[$expireField]<=date('Y-m-d H:i:s')){
  277. $data[$expireField] = $this->calcVipExpire($oldStatus, $data[$vipField], $oldExpire);
  278. }
  279. }
  280. }
  281. // 日志记录
  282. ActionLogModel::setRecord(
  283. session('userId'),
  284. [
  285. 'type' => 1,
  286. 'title' => $id ? '修改用户信息' : '新增用户',
  287. 'content' => json_encode($data, 256),
  288. 'module' => 'admin'
  289. ]
  290. );
  291. ActionLogModel::record();
  292. // 清理缓存
  293. RedisService::keyDel("caches:members:count*");
  294. try {
  295. if ($id) {
  296. DB::table('member')->where('id', $id)->update($data);
  297. } else {
  298. DB::table('member')->insert($data);
  299. }
  300. return message("操作成功", true, );
  301. } catch (\Exception $e) {
  302. return message("操作失败:" . $e->getMessage(), false);
  303. }
  304. }
  305. /**
  306. * 审核
  307. * @return array
  308. * @since 2020/11/11
  309. * @author laravel开发员
  310. */
  311. public function confirm()
  312. {
  313. // 请求参数
  314. $data = request()->all();
  315. $drivingLicense = isset($data['driving_license']) ? $data['driving_license'] : '';
  316. if ($drivingLicense) {
  317. $data['driving_license'] = get_image_path($drivingLicense);
  318. }
  319. $driversLicense = isset($data['drivers_license']) ? $data['drivers_license'] : '';
  320. if ($driversLicense) {
  321. $data['drivers_license'] = get_image_path($driversLicense);
  322. }
  323. // 设置日志
  324. $mobile = isset($data['mobile']) ? $data['mobile'] : '';
  325. ActionLogModel::setRecord(session('userId'), ['type' => 1, 'title' => "审核用户[{$mobile}]信息", 'content' => json_encode($data, 256), 'module' => 'admin']);
  326. ActionLogModel::record();
  327. RedisService::keyDel("caches:members:count*");
  328. return parent::edit($data); // TODO: Change the autogenerated stub
  329. }
  330. /**
  331. * 删除
  332. * @return array
  333. */
  334. public function delete()
  335. {
  336. // 设置日志标题
  337. ActionLogModel::setRecord(session('userId'), ['type' => 1, 'title' => "删除用户信息", 'content' => json_encode(request()->post(), 256), 'module' => 'admin']);
  338. ActionLogModel::record();
  339. $this->model->where('mark', 0)->where('update_time', '<=', time() - 7 * 86400)->delete();
  340. RedisService::keyDel("caches:members:count*");
  341. return parent::delete(); // TODO: Change the autogenerated stub
  342. }
  343. /**
  344. * 批量设置VIP
  345. * 前端传参示例:
  346. * {
  347. * ids: [1,2,3],
  348. * is_zg_vip: 1 // 可选字段: is_zg_vip / is_zsb_vip / is_video_vip
  349. * }
  350. * @return array
  351. */
  352. public function batchVip()
  353. {
  354. $params = request()->post();
  355. // 校验会员ID
  356. if (empty($params['ids']) || !is_array($params['ids'])) {
  357. return message("请选择要操作的会员", false);
  358. }
  359. $ids = $params['ids'];
  360. $vipMapping = [
  361. 'is_vip_1' => 'vip_1_expired',
  362. 'is_vip_2' => 'vip_2_expired',
  363. 'is_vip_3' => 'vip_3_expired',
  364. 'is_vip_4' => 'vip_4_expired',
  365. 'is_video_vip' => 'video_vip_expired'
  366. ];
  367. $updateData = ['update_time' => time()];
  368. $hasVipField = false;
  369. foreach ($vipMapping as $vipField => $expireField) {
  370. if (isset($params[$vipField])) {
  371. $status = intval($params[$vipField]);
  372. $updateData[$vipField] = $status;
  373. // 查出这些会员当前的VIP状态和过期时间
  374. $members = DB::table('member')
  375. ->whereIn('id', $ids)
  376. ->select('id', $vipField, $expireField)
  377. ->get()
  378. ->keyBy('id');
  379. foreach ($members as $memberId => $member) {
  380. $newExpire = $this->calcVipExpire(intval($member->$vipField), $status, $member->$expireField);
  381. DB::table('member')
  382. ->where('id', $memberId)
  383. ->update([
  384. $vipField => $status,
  385. $expireField => $newExpire,
  386. 'update_time' => time(),
  387. ]);
  388. }
  389. $hasVipField = true;
  390. }
  391. }
  392. if (!$hasVipField) {
  393. return message("请指定要操作的VIP类型", false);
  394. }
  395. return message("批量设置VIP成功", true);
  396. }
  397. public function getMemberStats(array $params)
  398. {
  399. $dateType = $params['dateType'] ?? 'day';
  400. $page = max(1, (int) ($params['page'] ?? 1));
  401. $limit = max(1, (int) ($params['limit'] ?? 10));
  402. // 分组日期格式
  403. switch ($dateType) {
  404. case 'month':
  405. $format = '%Y-%m';
  406. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  407. break;
  408. case 'year':
  409. $format = '%Y';
  410. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  411. break;
  412. case 'week':
  413. // 按周统计,格式 YYYY-WW
  414. $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)), '-', LPAD(WEEK(FROM_UNIXTIME(create_time), 1), 2, '0'))";
  415. break;
  416. case 'day':
  417. default:
  418. $format = '%Y-%m-%d';
  419. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  420. break;
  421. }
  422. // 统一转为时间戳
  423. $startTimestamp = !empty($params['start_time'])
  424. ? (is_numeric($params['start_time'])
  425. ? (int) $params['start_time']
  426. : strtotime($params['start_time']))
  427. : strtotime(date('2000-01-01 00:00:00')); // 默认起始时间
  428. $endTimestamp = !empty($params['end_time'])
  429. ? (is_numeric($params['end_time'])
  430. ? (int) $params['end_time']
  431. : strtotime($params['end_time']))
  432. : time(); // 默认当前时间
  433. if ($endTimestamp < $startTimestamp) {
  434. $endTimestamp = $startTimestamp;
  435. }
  436. // 基础查询
  437. $baseQuery = DB::table('member')
  438. ->selectRaw("{$groupRaw} as stat_date, COUNT(*) as reg_count")
  439. ->whereBetween('create_time', [$startTimestamp, $endTimestamp])
  440. ->groupBy('stat_date')
  441. ->orderBy('stat_date', 'desc');
  442. // 总数
  443. $total = DB::table(DB::raw("({$baseQuery->toSql()}) as t"))
  444. ->mergeBindings($baseQuery)
  445. ->count();
  446. // 分页数据
  447. $list = $baseQuery->forPage($page, $limit)->get();
  448. return [
  449. 'list' => $list,
  450. 'total' => $total,
  451. 'page' => $page,
  452. 'limit' => $limit,
  453. ];
  454. }
  455. /**
  456. * 根据旧状态和新状态计算VIP过期时间
  457. *
  458. * @param int|null $oldStatus 当前VIP状态 (0/1/2)
  459. * @param int $newStatus 新状态 (0/1/2)
  460. * @param string|null $oldExpire 当前过期时间
  461. * @return string|null 返回新的过期时间
  462. */
  463. public function calcVipExpire(?int $oldStatus, int $newStatus, ?string $oldExpire): ?string
  464. {
  465. if ($newStatus === 1) {
  466. // 从非VIP → VIP,才设置一年过期时间
  467. if ($oldStatus !== 1) {
  468. return date('Y-m-d H:i:s', time() + 365 * 24 * 3600);
  469. }
  470. // 已是VIP,保持原过期时间
  471. return $oldExpire;
  472. } else {
  473. // 0 或 2 → 清空过期时间
  474. return null;
  475. }
  476. }
  477. }