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