UserServiceImpl.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. package com.kym.service.impl;
  2. import cn.dev33.satoken.stp.StpUtil;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  5. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  6. import com.github.pagehelper.PageHelper;
  7. import com.github.yulichang.base.MPJBaseServiceImpl;
  8. import com.kym.common.R;
  9. import com.kym.common.config.WxConfig;
  10. import com.kym.common.constant.ResponseEnum;
  11. import com.kym.common.enums.WxApi;
  12. import com.kym.common.exception.BusinessException;
  13. import com.kym.common.utils.CommUtil;
  14. import com.kym.common.utils.HttpUtil;
  15. import com.kym.common.utils.IDGenerator;
  16. import com.kym.entity.*;
  17. import com.kym.entity.common.PageBean;
  18. import com.kym.entity.queryParams.CommonQueryParam;
  19. import com.kym.entity.queryParams.WxLoginParams;
  20. import com.kym.entity.vo.CustomUserVo;
  21. import com.kym.entity.vo.UserVo;
  22. import com.kym.entity.wechat.WxPhoneNum;
  23. import com.kym.mapper.MpRelationMapper;
  24. import com.kym.mapper.UserMapper;
  25. import com.kym.service.AccountService;
  26. import com.kym.service.CarsService;
  27. import com.kym.service.RefundLogService;
  28. import com.kym.service.UserService;
  29. import com.kym.service.cache.KymCache;
  30. import jakarta.annotation.PostConstruct;
  31. import lombok.SneakyThrows;
  32. import org.slf4j.Logger;
  33. import org.slf4j.LoggerFactory;
  34. import org.springframework.beans.BeanUtils;
  35. import org.springframework.context.annotation.Lazy;
  36. import org.springframework.stereotype.Service;
  37. import org.springframework.transaction.annotation.Transactional;
  38. import java.time.LocalDate;
  39. import java.time.LocalDateTime;
  40. import java.time.LocalTime;
  41. import java.time.temporal.TemporalAdjusters;
  42. import java.util.Arrays;
  43. import java.util.List;
  44. import java.util.Map;
  45. import java.util.stream.Collectors;
  46. import static java.util.Map.of;
  47. /**
  48. * <p>
  49. * 用户表 服务实现类
  50. * </p>
  51. *
  52. * @author skyline
  53. * @since 2023-06-27
  54. */
  55. @Service
  56. public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implements UserService {
  57. private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
  58. private final WxConfig wxConfig;
  59. private final AccountService accountService;
  60. private final RefundLogService refundLogService;
  61. private final CarsService carsService;
  62. private final MpRelationMapper mpRelationMapper;
  63. public UserServiceImpl(WxConfig wxConfig, @Lazy AccountService accountService, RefundLogService refundLogService,
  64. CarsService carsService, MpRelationMapper mpRelationMapper) {
  65. this.wxConfig = wxConfig;
  66. this.accountService = accountService;
  67. this.refundLogService = refundLogService;
  68. this.carsService = carsService;
  69. this.mpRelationMapper = mpRelationMapper;
  70. }
  71. @PostConstruct
  72. private void init() {
  73. // 初始化缓存
  74. QueryWrapper<User> wrapper = new QueryWrapper<User>().select("id", "station_id");
  75. List<User> users = baseMapper.selectList(wrapper);
  76. var map = users.stream()
  77. .filter(u -> CommUtil.isNotEmptyAndNull(u.getStationId()))
  78. .collect(Collectors.toMap(User::getId, User::getStationId));
  79. KymCache.INSTANCE.putUserId2StationId(map);
  80. }
  81. @Transactional(rollbackFor = Exception.class)
  82. @SneakyThrows
  83. @Override
  84. public R<?> wxLogin(WxLoginParams params) {
  85. // 微信登录
  86. var json = HttpUtil.getJson(WxApi.WX_MP_LOGIN.getApi(), Map.of(
  87. "appid", wxConfig.getAppid(),
  88. "secret", wxConfig.getSecret(),
  89. "js_code", params.getCode()
  90. ));
  91. Integer errorCode = json.getInteger("errcode");
  92. if (errorCode == null) {
  93. // 业务逻辑处理
  94. var openid = json.getString("openid");
  95. var unionid = json.getString("unionid") == null ? "" : json.getString("unionid");
  96. // 判断有没有,没有就新增
  97. var user = baseMapper.selectOne(new QueryWrapper<User>().eq("openid", openid));
  98. if (user != null) {
  99. // 登录逻辑
  100. if (CommUtil.isNotEmptyAndNull(unionid) && CommUtil.isEmptyOrNull(user.getUnionid())) {
  101. user.setUnionid(unionid);
  102. }
  103. return handleLogin(user);
  104. } else {
  105. if (CommUtil.isEmptyOrNull(params.getPhoneCode())) {
  106. throw new BusinessException(ResponseEnum.WX_MP_LOGIN_ERROR);
  107. }
  108. // 注册逻辑
  109. // 新增用户
  110. var newUser = new User();
  111. newUser.setOpenid(openid);
  112. newUser.setUnionid(unionid);
  113. // 手机号解密:先获取access_token,再请求手机号信息
  114. // access_token获取
  115. var accessTokenJson = HttpUtil.getJson(WxApi.WX_GET_ACCESS_TOKEN.getApi(), Map.of("appid", wxConfig.getAppid(), "secret", wxConfig.getSecret()));
  116. var accessToken = accessTokenJson.getString("access_token");
  117. var wxPhoneNum = HttpUtil.post(WxApi.WX_MP_GET_PHONE.getApi().replace("ACCESS_TOKEN", accessToken), Map.of("code", params.getPhoneCode()), WxPhoneNum.class);
  118. var mobilePhone = wxPhoneNum.getPhone_info().getPurePhoneNumber();
  119. newUser.setMobilePhone(mobilePhone);
  120. newUser.setUsername(mobilePhone);
  121. newUser.setAvatar(params.getAvatar());
  122. newUser.setNickname(params.getNickname());
  123. // 用户归属站点:扫码设备注册的直接确定,自主注册的在首次消费时自动归属
  124. if (CommUtil.isNotEmptyAndNull(params.getShortId())) {
  125. newUser.setStationId(KymCache.INSTANCE.gesStationIdByShortId(params.getShortId()));
  126. }
  127. baseMapper.insert(newUser);
  128. long userId = newUser.getId();
  129. if (CommUtil.isNotEmptyAndNull(newUser.getStationId())) {
  130. KymCache.INSTANCE.putUserId2StationId(Map.of(userId, newUser.getStationId()));
  131. }
  132. // 创建用户账户
  133. var account = new Account();
  134. account.setUserId(userId);
  135. accountService.save(account);
  136. // 登录逻辑
  137. return handleLogin(newUser);
  138. }
  139. } else {
  140. // 抛出异常
  141. LOGGER.error("微信登录异常,错误码{},异常信息{}", errorCode, json.getString("errmsg"));
  142. throw new BusinessException(ResponseEnum.WX_MP_LOGIN_ERROR);
  143. }
  144. }
  145. /**
  146. * 获取当前登录用户信息
  147. *
  148. * @return
  149. */
  150. @Override
  151. public UserVo getMe() {
  152. var userId = StpUtil.getLoginIdAsLong();
  153. var userVo = baseMapper.getMe(userId);
  154. var cars = carsService.lambdaQuery()
  155. .eq(Cars::getUserId, userId)
  156. .eq(Cars::getIsDefault, 1)
  157. .list();
  158. var car = cars.stream().findFirst().orElse(null);
  159. if (cars.size() > 1) {
  160. // 清理历史脏数据:仅保留第一个默认车辆,其余清除默认标记
  161. var staleDefaults = cars.stream().skip(1).peek(c -> c.setIsDefault(false)).toList();
  162. carsService.updateBatchById(staleDefaults);
  163. }
  164. if (car != null) {
  165. userVo.setDefaultPlateNo(car.getPlateNo());
  166. userVo.setVin(car.getVin());
  167. }
  168. return userVo;
  169. }
  170. /**
  171. * 登录逻辑处理
  172. *
  173. * @param user
  174. * @return
  175. */
  176. private R<?> handleLogin(User user) {
  177. StpUtil.login(user.getId());
  178. // 用户名存入session,统一日志读取使用
  179. StpUtil.getSession().set("openid", user.getOpenid());
  180. StpUtil.getSession().set("unionid", user.getUnionid());
  181. StpUtil.getSession().set("userId", user.getId());
  182. StpUtil.getSession().set("username", user.getUsername());
  183. StpUtil.getSession().set("mobilePhone", user.getMobilePhone());
  184. if (user.getStationId() != null) {
  185. StpUtil.getSession().set("stationId", user.getStationId());
  186. }
  187. user.setLastLoginTime(LocalDateTime.now());
  188. updateById(user);
  189. LOGGER.info("用户:{}/{}登录成功,tokenName:{},tokenValue:{}", user.getMobilePhone(), user.getId(), StpUtil.getTokenName(), StpUtil.getTokenValue());
  190. updateUnionid();
  191. return R.success(of("userId", user.getId(), "satoken", StpUtil.getTokenValue()));
  192. }
  193. protected void updateUnionid() {
  194. var unionid = StpUtil.getSession().getString("unionid");
  195. if (CommUtil.isNotEmptyAndNull(unionid)) {
  196. // 匹配公众号用户
  197. var wrapper = new LambdaUpdateWrapper<MpRelation>().eq(MpRelation::getUnionid, unionid).set(MpRelation::getUserId, StpUtil.getLoginIdAsLong()).set(MpRelation::getOpenid, StpUtil.getSession().getString("openid"));
  198. mpRelationMapper.update(null, wrapper);
  199. }
  200. }
  201. @Transactional(rollbackFor = Exception.class)
  202. @Override
  203. public void updateUser(UserVo userVo) {
  204. userVo.setId(StpUtil.getSession().getLong("userId"));
  205. var user = new User();
  206. BeanUtils.copyProperties(userVo, user);
  207. if (!CommUtil.isEmptyOrNull(user.getAvatar()) || !CommUtil.isEmptyOrNull(user.getMobilePhone()) || !CommUtil.isEmptyOrNull(user.getNickname())) {
  208. baseMapper.updateById(user);
  209. }
  210. // 更新车牌信息
  211. if (userVo.getDefaultPlateNo() != null) {
  212. var car = new Cars();
  213. car.setUserId(userVo.getId());
  214. car.setPlateNo(userVo.defaultPlateNo);
  215. car.setIsDefault(true);
  216. car.setIsDelete(false);
  217. if (userVo.getVin() != null) {
  218. car.setVin(userVo.getVin());
  219. }
  220. // 将用户名下其他车辆设为非默认
  221. var cars = carsService.lambdaQuery()
  222. .eq(Cars::getUserId, userVo.getId())
  223. .and(w -> w.eq(Cars::getIsDelete, false).or().isNull(Cars::getIsDelete))
  224. .list();
  225. cars.stream()
  226. .filter(c -> !userVo.getDefaultPlateNo().equals(c.getPlateNo()))
  227. .forEach(s -> s.setIsDefault(false));
  228. carsService.updateBatchById(cars);
  229. var wrapper = new QueryWrapper<Cars>();
  230. wrapper.eq("plate_no", userVo.getDefaultPlateNo());
  231. carsService.saveOrUpdate(car, wrapper);
  232. }
  233. }
  234. @Override
  235. public PageBean<UserVo> listUserVo(Integer pageNum, Integer pageSize) {
  236. PageHelper.startPage(pageNum, pageSize);
  237. var userVoList = list().stream().map(user -> {
  238. var userVo = new UserVo();
  239. BeanUtils.copyProperties(user, userVo);
  240. return userVo;
  241. }).collect(Collectors.toList());
  242. return new PageBean<>(userVoList);
  243. }
  244. /**
  245. * 分页查询用户列表
  246. *
  247. * @param params
  248. * @return
  249. */
  250. @Override
  251. public PageBean<CustomUserVo> listCustomUser(CommonQueryParam params) {
  252. // 站点数据权限
  253. var adminStationIds = KymCache.INSTANCE.getAdminUserStationIds(StpUtil.getLoginIdAsLong());
  254. if (CommUtil.isEmptyOrNull(params.getStationId()) &&
  255. CommUtil.isNotEmptyAndNull(KymCache.INSTANCE.getAdminUserStationIds(StpUtil.getLoginIdAsLong()))) {
  256. params.setStationId(adminStationIds.get(0));
  257. }
  258. List<Long> userIds = lambdaQuery()
  259. .eq(CommUtil.isNotEmptyAndNull(params.getMobilePhone()), User::getMobilePhone, params.getMobilePhone())
  260. .eq(CommUtil.isNotEmptyAndNull(params.getStationId()), User::getStationId, params.getStationId())
  261. .eq(CommUtil.isNotEmptyAndNull(params.getStatus()), User::getStatus, params.getStatus())
  262. .list().stream().map(User::getId)
  263. .toList();
  264. if (CommUtil.isEmptyOrNull(userIds)) {
  265. return new PageBean<>();
  266. }
  267. PageHelper.startPage(params.getPageNum(), params.getPageSize());
  268. var result = baseMapper.listUser(userIds);
  269. var page = new PageBean<>(result);
  270. // 用户余额,退款次数,退款金额
  271. var account = accountService.lambdaQuery().in(Account::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
  272. var user2Balance = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getBalance)));
  273. var user2rechargeBalance = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getRechargeBalance)));
  274. var user2GrantsBalance= account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getGrantsBalance)));
  275. var user2FrozenAmount = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getFrozenAmount)));
  276. var refund = refundLogService.lambdaQuery().in(RefundLog::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
  277. // refund按照用户维度计算退款次数和退款总金额
  278. var user2RefundAmount = refund.stream().collect(Collectors.groupingBy(RefundLog::getUserId, Collectors.summingInt(RefundLog::getRefund)));
  279. var user2RefundDiscountAmount = refund.stream().collect(Collectors.groupingBy(RefundLog::getUserId, Collectors.summingInt(RefundLog::getDiscountAmount)));
  280. var user2RefundTimes = refund.stream().collect(Collectors.groupingBy(RefundLog::getUserId, Collectors.counting()));
  281. // 将用户余额,退款次数,退款金额放入result中
  282. var res = result.stream().peek(vo -> {
  283. vo.setBalance(user2Balance.getOrDefault(vo.getUserId(), 0));
  284. vo.setRechargeBalance(user2rechargeBalance.getOrDefault(vo.getUserId(), 0));
  285. vo.setGrantsBalance(user2GrantsBalance.getOrDefault(vo.getUserId(), 0));
  286. vo.setFrozenAmount(user2FrozenAmount.getOrDefault(vo.getUserId(), 0));
  287. vo.setRefundTimes(user2RefundTimes.getOrDefault(vo.getUserId(), 0L));
  288. vo.setRefundAmount(user2RefundAmount.getOrDefault(vo.getUserId(), 0));
  289. vo.setRefundDiscountAmount(user2RefundDiscountAmount.getOrDefault(vo.getUserId(), 0));
  290. vo.setStationName(KymCache.INSTANCE.getStationNameById(vo.getStationId()));
  291. }).toList();
  292. page.setList(res);
  293. return page;
  294. }
  295. /**
  296. * 统计指定日期注册用户数量
  297. *
  298. * @param statDay
  299. * @return
  300. */
  301. @Override
  302. public Map<String, Integer> countDailyRegister(LocalDate statDay, String... stationId) {
  303. LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  304. wrapper.ge(User::getCreateTime, LocalDateTime.of(statDay, LocalTime.MIN));
  305. wrapper.lt(User::getCreateTime, LocalDateTime.of(statDay, LocalTime.MAX));
  306. if (stationId.length > 0) {
  307. wrapper.in(User::getStationId, Arrays.asList(stationId));
  308. }
  309. // 按照站点分组统计订单数量
  310. return list(wrapper).stream().collect(Collectors.groupingBy(u -> u.getStationId() != null ? u.getStationId() : "未分配", Collectors.summingInt(o -> 1)));
  311. }
  312. /**
  313. * 统计指定日期当月注册用户数量
  314. *
  315. * @param statDay
  316. * @return
  317. */
  318. @Override
  319. public Map<String, Integer> countMonthRegister(LocalDate statDay) {
  320. var startTime = statDay.with(TemporalAdjusters.firstDayOfMonth()).atTime(LocalTime.MIN);
  321. var endTime = statDay.with(TemporalAdjusters.lastDayOfMonth()).atTime(LocalTime.MAX);
  322. LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  323. wrapper.ge(User::getCreateTime, startTime);
  324. wrapper.lt(User::getCreateTime, endTime);
  325. return list(wrapper).stream().collect(Collectors.groupingBy(u -> u.getStationId() != null ? u.getStationId() : "未分配", Collectors.summingInt(o -> 1)));
  326. }
  327. }