MemberService.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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_zg_vip',
  227. 'zg_vip_expired',
  228. 'is_zsb_vip',
  229. 'zsb_vip_expired',
  230. 'is_video_vip',
  231. 'video_vip_expired',
  232. 'entry_type',
  233. 'need_paper'
  234. ];
  235. // 只保留允许字段
  236. $data = array_intersect_key($data, array_flip($allowedFields));
  237. // 头像与证件图片处理
  238. foreach (['avatar', 'driving_license', 'drivers_license'] as $field) {
  239. if (!empty($data[$field])) {
  240. $data[$field] = get_image_path($data[$field]);
  241. }
  242. }
  243. $id = $data['id'] ?? 0;
  244. $mobile = trim($data['mobile'] ?? '');
  245. if ($mobile) {
  246. $checkId = $this->model->where(['mobile' => $mobile, 'mark' => 1])->value('id');
  247. if ($checkId && $checkId != $id) {
  248. return message('手机号已存在', false);
  249. }
  250. }
  251. // 密码加密
  252. if (!empty($data['password'])) {
  253. $data['password'] = get_password(trim($data['password']));
  254. }
  255. // VIP字段处理(可选)
  256. $vipFields = ['is_zg_vip', 'is_zsb_vip', 'is_video_vip'];
  257. $vipMapping = [
  258. 'is_zg_vip' => 'zg_vip_expired',
  259. 'is_zsb_vip' => 'zsb_vip_expired',
  260. 'is_video_vip' => 'video_vip_expired'
  261. ];
  262. foreach ($vipFields as $vipField) {
  263. if (isset($data[$vipField])) {
  264. $data[$vipField] = intval($data[$vipField]);
  265. $expireField = $vipMapping[$vipField];
  266. // 获取当前数据库状态
  267. $oldStatus = $this->model->where('id', $id)->value($vipField);
  268. $oldExpire = $this->model->where('id', $id)->value($expireField);
  269. // 调用公共方法计算新过期时间
  270. if($data[$expireField]<=date('Y-m-d H:i:s')){
  271. $data[$expireField] = $this->calcVipExpire($oldStatus, $data[$vipField], $oldExpire);
  272. }
  273. }
  274. }
  275. // 日志记录
  276. ActionLogModel::setRecord(
  277. session('userId'),
  278. [
  279. 'type' => 1,
  280. 'title' => $id ? '修改用户信息' : '新增用户',
  281. 'content' => json_encode($data, 256),
  282. 'module' => 'admin'
  283. ]
  284. );
  285. ActionLogModel::record();
  286. // 清理缓存
  287. RedisService::keyDel("caches:members:count*");
  288. try {
  289. if ($id) {
  290. DB::table('member')->where('id', $id)->update($data);
  291. } else {
  292. DB::table('member')->insert($data);
  293. }
  294. return message("操作成功", true, );
  295. } catch (\Exception $e) {
  296. return message("操作失败:" . $e->getMessage(), false);
  297. }
  298. }
  299. /**
  300. * 审核
  301. * @return array
  302. * @since 2020/11/11
  303. * @author laravel开发员
  304. */
  305. public function confirm()
  306. {
  307. // 请求参数
  308. $data = request()->all();
  309. $drivingLicense = isset($data['driving_license']) ? $data['driving_license'] : '';
  310. if ($drivingLicense) {
  311. $data['driving_license'] = get_image_path($drivingLicense);
  312. }
  313. $driversLicense = isset($data['drivers_license']) ? $data['drivers_license'] : '';
  314. if ($driversLicense) {
  315. $data['drivers_license'] = get_image_path($driversLicense);
  316. }
  317. // 设置日志
  318. $mobile = isset($data['mobile']) ? $data['mobile'] : '';
  319. ActionLogModel::setRecord(session('userId'), ['type' => 1, 'title' => "审核用户[{$mobile}]信息", 'content' => json_encode($data, 256), 'module' => 'admin']);
  320. ActionLogModel::record();
  321. RedisService::keyDel("caches:members:count*");
  322. return parent::edit($data); // TODO: Change the autogenerated stub
  323. }
  324. /**
  325. * 删除
  326. * @return array
  327. */
  328. public function delete()
  329. {
  330. // 设置日志标题
  331. ActionLogModel::setRecord(session('userId'), ['type' => 1, 'title' => "删除用户信息", 'content' => json_encode(request()->post(), 256), 'module' => 'admin']);
  332. ActionLogModel::record();
  333. $this->model->where('mark', 0)->where('update_time', '<=', time() - 7 * 86400)->delete();
  334. RedisService::keyDel("caches:members:count*");
  335. return parent::delete(); // TODO: Change the autogenerated stub
  336. }
  337. /**
  338. * 批量设置VIP
  339. * 前端传参示例:
  340. * {
  341. * ids: [1,2,3],
  342. * is_zg_vip: 1 // 可选字段: is_zg_vip / is_zsb_vip / is_video_vip
  343. * }
  344. * @return array
  345. */
  346. public function batchVip()
  347. {
  348. $params = request()->post();
  349. // 校验会员ID
  350. if (empty($params['ids']) || !is_array($params['ids'])) {
  351. return message("请选择要操作的会员", false);
  352. }
  353. $ids = $params['ids'];
  354. $vipMapping = [
  355. 'is_zg_vip' => 'zg_vip_expired',
  356. 'is_zsb_vip' => 'zsb_vip_expired',
  357. 'is_video_vip' => 'video_vip_expired'
  358. ];
  359. $updateData = ['update_time' => time()];
  360. $hasVipField = false;
  361. foreach ($vipMapping as $vipField => $expireField) {
  362. if (isset($params[$vipField])) {
  363. $status = intval($params[$vipField]);
  364. $updateData[$vipField] = $status;
  365. // 查出这些会员当前的VIP状态和过期时间
  366. $members = DB::table('member')
  367. ->whereIn('id', $ids)
  368. ->select('id', $vipField, $expireField)
  369. ->get()
  370. ->keyBy('id');
  371. foreach ($members as $memberId => $member) {
  372. $newExpire = $this->calcVipExpire(intval($member->$vipField), $status, $member->$expireField);
  373. DB::table('member')
  374. ->where('id', $memberId)
  375. ->update([
  376. $vipField => $status,
  377. $expireField => $newExpire,
  378. 'update_time' => time(),
  379. ]);
  380. }
  381. $hasVipField = true;
  382. }
  383. }
  384. if (!$hasVipField) {
  385. return message("请指定要操作的VIP类型", false);
  386. }
  387. return message("批量设置VIP成功", true);
  388. }
  389. public function getMemberStats(array $params)
  390. {
  391. $dateType = $params['dateType'] ?? 'day';
  392. $page = max(1, (int) ($params['page'] ?? 1));
  393. $limit = max(1, (int) ($params['limit'] ?? 10));
  394. // 分组日期格式
  395. switch ($dateType) {
  396. case 'month':
  397. $format = '%Y-%m';
  398. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  399. break;
  400. case 'year':
  401. $format = '%Y';
  402. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  403. break;
  404. case 'week':
  405. // 按周统计,格式 YYYY-WW
  406. $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)), '-', LPAD(WEEK(FROM_UNIXTIME(create_time), 1), 2, '0'))";
  407. break;
  408. case 'day':
  409. default:
  410. $format = '%Y-%m-%d';
  411. $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
  412. break;
  413. }
  414. // 统一转为时间戳
  415. $startTimestamp = !empty($params['start_time'])
  416. ? (is_numeric($params['start_time'])
  417. ? (int) $params['start_time']
  418. : strtotime($params['start_time']))
  419. : strtotime(date('2000-01-01 00:00:00')); // 默认起始时间
  420. $endTimestamp = !empty($params['end_time'])
  421. ? (is_numeric($params['end_time'])
  422. ? (int) $params['end_time']
  423. : strtotime($params['end_time']))
  424. : time(); // 默认当前时间
  425. if ($endTimestamp < $startTimestamp) {
  426. $endTimestamp = $startTimestamp;
  427. }
  428. // 基础查询
  429. $baseQuery = DB::table('member')
  430. ->selectRaw("{$groupRaw} as stat_date, COUNT(*) as reg_count")
  431. ->whereBetween('create_time', [$startTimestamp, $endTimestamp])
  432. ->groupBy('stat_date')
  433. ->orderBy('stat_date', 'desc');
  434. // 总数
  435. $total = DB::table(DB::raw("({$baseQuery->toSql()}) as t"))
  436. ->mergeBindings($baseQuery)
  437. ->count();
  438. // 分页数据
  439. $list = $baseQuery->forPage($page, $limit)->get();
  440. return [
  441. 'list' => $list,
  442. 'total' => $total,
  443. 'page' => $page,
  444. 'limit' => $limit,
  445. ];
  446. }
  447. /**
  448. * 根据旧状态和新状态计算VIP过期时间
  449. *
  450. * @param int|null $oldStatus 当前VIP状态 (0/1/2)
  451. * @param int $newStatus 新状态 (0/1/2)
  452. * @param string|null $oldExpire 当前过期时间
  453. * @return string|null 返回新的过期时间
  454. */
  455. public function calcVipExpire(?int $oldStatus, int $newStatus, ?string $oldExpire): ?string
  456. {
  457. if ($newStatus === 1) {
  458. // 从非VIP → VIP,才设置一年过期时间
  459. if ($oldStatus !== 1) {
  460. return date('Y-m-d H:i:s', time() + 365 * 24 * 3600);
  461. }
  462. // 已是VIP,保持原过期时间
  463. return $oldExpire;
  464. } else {
  465. // 0 或 2 → 清空过期时间
  466. return null;
  467. }
  468. }
  469. }