| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863 |
- package com.xuekairui.user.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.xuekairui.common.BusinessException;
- import com.xuekairui.common.ErrorCode;
- import com.xuekairui.common.ValidateUtil;
- import com.xuekairui.common.event.ActivityEventRedisService;
- import com.xuekairui.user.dto.*;
- import com.xuekairui.user.entity.User;
- import com.xuekairui.user.entity.LoginLog;
- import com.xuekairui.user.enums.LicenseStatus;
- import com.xuekairui.user.enums.MembershipLevel;
- import com.xuekairui.user.enums.OperationSource;
- import com.xuekairui.user.enums.OperationType;
- import com.xuekairui.user.enums.OperatorRole;
- import com.xuekairui.user.event.UserRegisteredEvent;
- import com.xuekairui.user.mapper.UserMapper;
- import com.xuekairui.user.util.JwtUtil;
- import com.xuekairui.user.util.PasswordUtil;
- import io.jsonwebtoken.Claims;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.slf4j.MDC;
- import org.springframework.context.ApplicationEventPublisher;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.springframework.transaction.support.TransactionSynchronization;
- import org.springframework.transaction.support.TransactionSynchronizationManager;
- import java.time.LocalDateTime;
- import java.util.Map;
- /**
- * 认证服务
- */
- @Slf4j
- @Service
- @RequiredArgsConstructor
- public class AuthService {
- private final UserMapper userMapper;
- private final VerificationCodeService verificationCodeService;
- private final WechatService wechatService;
- private final JwtUtil jwtUtil;
- private final ApplicationEventPublisher eventPublisher;
- private final PasswordUtil passwordUtil;
- private final OperationAuditLogService auditLogService;
- private final MembershipService membershipService;
- private final AdminAuthService adminAuthService;
- private final com.xuekairui.user.security.TokenVersionCache tokenVersionCache;
- private final WechatMiniProgramService wechatMiniProgramService;
- private final ExternalUserSyncService externalUserSyncService;
- private final CertificateSyncService certificateSyncService;
- private final BusinessLicenseService businessLicenseService;
- private final ShopResourceSyncService shopResourceSyncService;
- private final com.xuekairui.user.mapper.PlatformAccountMapper platformAccountMapper;
- private final com.xuekairui.user.mapper.CrawlerUsageLogMapper crawlerUsageLogMapper;
- private final com.xuekairui.user.mapper.SearchRecordMapper searchRecordMapper;
- private final com.xuekairui.user.mapper.PurchaseIntentMapper purchaseIntentMapper;
- private final ActivityEventRedisService activityEventRedisService;
- private final com.xuekairui.user.mapper.LoginLogMapper loginLogMapper;
- /**
- * 发送验证码
- */
- public void sendVerificationCode(SendCodeRequest request, String ip) {
- verificationCodeService.sendCode(request.getPhone(), request.getScene(), ip);
- }
- /**
- * 验证码登录/注册
- * 如果手机号已注册则登录,否则自动注册
- */
- @Transactional
- public LoginResponse smsLogin(SmsLoginRequest request, String ip) {
- // 1. 验证验证码(兼容 scene=login 和 scene=register,统一用 login key)
- verificationCodeService.verifyCode(request.getPhone(), request.getCode());
- // 2. 查找或创建用户
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- boolean isNewUser = false;
- if (user == null) {
- isNewUser = true;
- // 自动注册新用户
- user = User.builder()
- .phone(request.getPhone())
- .nickname("用户" + request.getPhone().substring(7))
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("SMS")
- .status(1)
- .build();
- userMapper.insert(user);
- // 注册即写入 PLUS 会员记录
- membershipService.initPlusMembership(user.getId());
- // 发布用户注册事件(邀请模块监听处理邀请码,赠送会员奖励)
- eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
- }
- // 3. 更新登录信息
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- String loginSource = request.getLoginSource() != null && !request.getLoginSource().isBlank()
- ? request.getLoginSource() : "PC";
- user.setLoginSource(loginSource);
- userMapper.updateById(user);
- // 4. 生成Token(小程序不递增tokenVersion,不踢PC)
- boolean incrementVersion = !"MINIAPP".equals(loginSource);
- LoginResponse response = buildLoginResponseInternal(user, incrementVersion, loginSource, false);
- response.setUserId(user.getId());
- response.setNewUser(isNewUser);
- // 记录审计日志
- if (isNewUser) {
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_REGISTER, "USER:" + user.getId(), user.getId());
- }
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
- // 登录时检查入驻信息,未同步成功则补推一次到第三方
- syncShopResourceOnLogin(user.getId());
- return response;
- }
- /**
- * 微信扫码登录
- */
- @Transactional
- public LoginResponse wechatLogin(WechatLoginRequest request, String ip) {
- // 1. 获取微信用户信息
- WechatService.WechatUserInfo wechatInfo = wechatService.getUserInfo(request.getCode());
- // 2. 通过UnionID查找用户
- User user = null;
- if (wechatInfo.getUnionId() != null) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, wechatInfo.getUnionId()));
- }
- // 3. 如果用户存在且已绑定手机号,直接登录
- if (user != null && user.getPhone() != null) {
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("PC");
- userMapper.updateById(user);
- syncShopResourceOnLogin(user.getId());
- return buildLoginResponse(user);
- }
- // 4. 如果用户存在但未绑定手机号,或新用户,返回需要绑定手机号
- if (user == null) {
- // 创建新用户(未绑定手机号)
- user = User.builder()
- .wechatOpenId(wechatInfo.getOpenId())
- .wechatUnionId(wechatInfo.getUnionId())
- .nickname(wechatInfo.getNickname())
- .avatar(wechatInfo.getHeadImgUrl())
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("WECHAT")
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- } else {
- // 更新微信信息
- user.setWechatOpenId(wechatInfo.getOpenId());
- user.setWechatUnionId(wechatInfo.getUnionId());
- if (user.getNickname() == null) {
- user.setNickname(wechatInfo.getNickname());
- }
- if (user.getAvatar() == null) {
- user.setAvatar(wechatInfo.getHeadImgUrl());
- }
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- userMapper.updateById(user);
- }
- // 5. 生成临时Token,要求绑定手机号
- String tempToken = jwtUtil.generateTempToken(wechatInfo.getOpenId(), wechatInfo.getUnionId());
- // 确保 MDC 中有 uid(微信登录临时Token场景不走 buildLoginResponse)
- MDC.put("uid", String.valueOf(user.getId()));
- return LoginResponse.builder()
- .needBindPhone(true)
- .tempToken(tempToken)
- .userInfo(convertToUserInfoResponse(user))
- .build();
- }
- /**
- * 微信绑定手机号
- */
- @Transactional
- public LoginResponse wechatBindPhone(WechatBindRequest request, String ip) {
- // 1. 验证临时Token
- Claims claims = jwtUtil.parseToken(request.getTempToken());
- String tokenType = claims.get("type", String.class);
- if (!"temp".equals(tokenType)) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 验证验证码
- verificationCodeService.verifyCode(request.getPhone(), request.getCode());
- // 3. 检查手机号是否已被其他用户绑定
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- User wechatUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatOpenId, openId));
- if (wechatUser == null) {
- throw new BusinessException(ErrorCode.WECHAT_LOGIN_FAILED);
- }
- if (existUser != null && !existUser.getId().equals(wechatUser.getId())) {
- // 手机号已被其他账号绑定,合并账号
- existUser.setWechatOpenId(openId);
- existUser.setWechatUnionId(unionId);
- existUser.setLastLoginTime(LocalDateTime.now());
- existUser.setLastLoginIp(ip);
- existUser.setLoginSource("PC");
- userMapper.updateById(existUser);
- // 禁用旧的微信用户(不物理删除,仅改状态)
- wechatUser.setStatus(0);
- userMapper.updateById(wechatUser);
- wechatUser = existUser;
- } else if (existUser == null) {
- // 绑定手机号到微信用户(首次完成注册)
- wechatUser.setPhone(request.getPhone());
- wechatUser.setLoginSource("PC");
- wechatUser.setLastLoginTime(LocalDateTime.now());
- wechatUser.setLastLoginIp(ip);
- userMapper.updateById(wechatUser);
- // 发布用户注册事件,邀请模块可据此建立邀请关系并发放奖励
- eventPublisher.publishEvent(new UserRegisteredEvent(this, wechatUser.getId(), request.getInviteCode()));
- // 普通会员(PLUS)免费开放,注册即拥有,无需额外发放
- }
- // 4. 生成正式Token
- syncShopResourceOnLogin(wechatUser.getId());
- return buildLoginResponse(wechatUser);
- }
- /**
- * 外部系统换取 Token(登录 + 信息同步,兼容旧版一并传入用户资料和入驻信息)
- * <p>查找或自动注册用户。根据 loginSource 决定过期策略:MINIAPP=30天不踢PC / 其他=1小时。
- */
- @Transactional
- public ExternalTokenResponse externalMiniAppToken(ExternalTokenRequest request, String ip) {
- // 登录来源:不传默认 MINIAPP(向后兼容)
- String loginSource = request.getLoginSource() != null && !request.getLoginSource().isBlank()
- ? request.getLoginSource() : "MINIAPP";
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- boolean isNewUser = false;
- if (user == null) {
- isNewUser = true;
- // 优先级:nickname > username > 手机号生成
- String nickname = (request.getNickname() != null && !request.getNickname().isBlank())
- ? request.getNickname()
- : (request.getUsername() != null && !request.getUsername().isBlank())
- ? request.getUsername()
- : "用户" + request.getPhone().substring(request.getPhone().length() - 4);
- user = User.builder()
- .phone(request.getPhone())
- .nickname(nickname)
- .userName(request.getUsername())
- .wechatMiniOpenId(request.getOpenId())
- .wechatUnionId(request.getUnionId())
- .pharmacyName(request.getPharmacyName())
- .province(request.getProvince())
- .city(request.getCity())
- .district(request.getDistrict())
- .pharmacyAddress(request.getPharmacyAddress())
- .contactPerson(request.getContactPerson())
- .contactPhone(request.getContactPhone())
- .businessLicenseNo(request.getBusinessLicenseNo())
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource(loginSource)
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- if (request.getInviteCode() != null) {
- eventPublisher.publishEvent(
- new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- }
- } else {
- // 已存在用户:更新第三方传入的信息(非空字段才覆盖)
- if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setUserName(request.getUsername());
- }
- if (request.getNickname() != null && !request.getNickname().isBlank()) {
- user.setNickname(request.getNickname());
- } else if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setNickname(request.getUsername());
- }
- if (request.getPharmacyName() != null && !request.getPharmacyName().isBlank()) {
- user.setPharmacyName(request.getPharmacyName());
- }
- if (request.getProvince() != null && !request.getProvince().isBlank()) {
- user.setProvince(request.getProvince());
- }
- if (request.getCity() != null && !request.getCity().isBlank()) {
- user.setCity(request.getCity());
- }
- if (request.getDistrict() != null && !request.getDistrict().isBlank()) {
- user.setDistrict(request.getDistrict());
- }
- if (request.getPharmacyAddress() != null && !request.getPharmacyAddress().isBlank()) {
- user.setPharmacyAddress(request.getPharmacyAddress());
- }
- if (request.getContactPerson() != null && !request.getContactPerson().isBlank()) {
- user.setContactPerson(request.getContactPerson());
- }
- if (request.getContactPhone() != null && !request.getContactPhone().isBlank()) {
- user.setContactPhone(request.getContactPhone());
- }
- if (request.getBusinessLicenseNo() != null && !request.getBusinessLicenseNo().isBlank()) {
- user.setBusinessLicenseNo(request.getBusinessLicenseNo());
- }
- if (request.getOpenId() != null && !request.getOpenId().isBlank()) {
- user.setWechatMiniOpenId(request.getOpenId());
- }
- if (request.getUnionId() != null && !request.getUnionId().isBlank()) {
- user.setWechatUnionId(request.getUnionId());
- }
- }
- user.setLoginSource(loginSource);
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- userMapper.updateById(user);
- // 同步入驻信息到 t_business_license(仅当传入入驻相关字段时)
- syncBusinessLicenseFromTokenRequest(request, user.getId());
- boolean skipRefresh = request.getSkipRefreshToken() != null && request.getSkipRefreshToken();
- boolean incrementVersion = !"MINIAPP".equals(loginSource);
- LoginResponse response = buildLoginResponseInternal(user, incrementVersion, loginSource, skipRefresh);
- response.setNewUser(isNewUser);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- isNewUser ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
- "USER:" + user.getId(), user.getId());
- return ExternalTokenResponse.builder()
- .userId(response.getUserId())
- .accessToken(response.getAccessToken())
- .refreshToken(response.getRefreshToken())
- .expiresIn(response.getExpiresIn())
- .newUser(isNewUser)
- .role(response.getRole())
- .build();
- }
- public LoginResponse heartbeat(Long userId, String role, String loginSource) {
- String roleCode = role != null ? role : "USER";
- Long currentVersion = tokenVersionCache.getVersion(userId, false);
- if (currentVersion == null) {
- User user = userMapper.selectById(userId);
- currentVersion = user != null && user.getTokenVersion() != null
- ? user.getTokenVersion() : 0L;
- if (user != null) {
- tokenVersionCache.putVersion(userId, false, currentVersion);
- }
- }
- String newAccessToken = jwtUtil.generateAccessToken(
- userId, roleCode, loginSource, currentVersion);
- // 心跳不覆盖已有的 ACTIVE refreshToken,避免 PC/小程序互相踢 refreshToken
- User user = userMapper.selectById(userId);
- String newRefreshToken = null;
- if (user != null && (user.getRefreshTokenHash() == null || !"ACTIVE".equals(user.getRefreshTokenStatus()))) {
- newRefreshToken = jwtUtil.generateRefreshToken(userId, loginSource);
- }
- User updateUser = new User();
- updateUser.setId(userId);
- if (newRefreshToken != null) {
- updateUser.setRefreshTokenHash(JwtUtil.hashToken(newRefreshToken));
- updateUser.setRefreshTokenStatus("ACTIVE");
- }
- userMapper.updateById(updateUser);
- return LoginResponse.builder()
- .userId(userId)
- .accessToken(newAccessToken)
- .refreshToken(newRefreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .needBindPhone(false)
- .role(roleCode)
- .build();
- }
- /**
- * 获取用户信息
- */
- public UserInfoResponse getUserInfo(Long userId) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- return convertToUserInfoResponse(user);
- }
- /**
- * 获取聚合用户信息(小程序"我的"页面)— 新增,不影响 /api/auth/user/info
- * <p>返回 user + gate(门禁)+ quota(配额)+ platformBindings(平台绑定)。
- */
- public java.util.Map<String, Object> getUserMe(Long userId) {
- UserInfoResponse userInfo = getUserInfo(userId);
- UserGateStatus gate = buildGateStatus(userId);
- java.util.Map<String, Object> quota = buildQuotaSummary(userId);
- java.util.List<?> platforms = getPlatformBindings(userId);
- java.util.Map<String, Object> result = new java.util.LinkedHashMap<>(toMap(userInfo));
- result.put("gate", gate);
- result.put("quota", quota);
- result.put("platformBindings", platforms);
- return result;
- }
- /**
- * 首页概览统计 — 新增
- */
- public DashboardResponse getDashboard(Long userId) {
- // 今日查询次数
- int todayQueryCount = crawlerUsageLogMapper.sumTodayUsage(userId);
- // 累计查询次数
- int totalQueryCount = crawlerUsageLogMapper.sumTotalUsage(userId);
- // 最近查询摘要(最多 3 条)
- java.util.List<DashboardResponse.RecentQueryItem> recent =
- searchRecordMapper.selectRecentByUserId(userId, 3)
- .stream().map(r -> DashboardResponse.RecentQueryItem.builder()
- .drugName(r.getDrugName())
- .searchedAt(r.getSearchedAt() != null ? r.getSearchedAt().toString() : null)
- .lowestPrice(r.getQuoteCount() > 0 ? "有报价" : null)
- .build())
- .toList();
- return DashboardResponse.builder()
- .todayQueryCount(todayQueryCount)
- .todayPurchaseIntentCount(
- purchaseIntentMapper.countTodayByUserId(userId, java.time.LocalDate.now()))
- .totalQueryCount(totalQueryCount)
- .recentQueries(recent)
- .build();
- }
- /**
- * 登出 — 新增,清除 refresh token
- */
- @Transactional
- public void logout(Long userId) {
- User user = new User();
- user.setId(userId);
- user.setRefreshTokenHash(null);
- user.setRefreshTokenStatus(null);
- userMapper.updateById(user);
- log.info("用户 {} 已登出", userId);
- }
- /**
- * 独立注册(小程序填写药店信息后调用) — 新增
- */
- @Transactional
- public LoginResponse register(RegisterRequest request, String ip) {
- // 1. 验证临时 Token
- io.jsonwebtoken.Claims claims = jwtUtil.parseToken(request.getTempToken());
- if (!"temp".equals(claims.get("type", String.class))) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 查找小程序用户(由 miniappUnifiedLogin 创建的未完成注册的用户)
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, openId));
- if (user == null) {
- // fallback: try unionId
- if (unionId != null) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, unionId));
- }
- }
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND, "请先通过小程序授权登录");
- }
- // 3. 补全药店信息
- user.setPharmacyName(request.getPharmacyName());
- user.setProvince(request.getProvince());
- user.setCity(request.getCity());
- user.setDistrict(request.getDistrict());
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- userMapper.updateById(user);
- // 4. 发布注册事件(处理邀请码)
- if (request.getInviteCode() != null) {
- eventPublisher.publishEvent(
- new UserRegisteredEvent(this, user.getId(), request.getInviteCode()));
- }
- // 5. 签发正式 Token
- LoginResponse response = buildMiniAppLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_REGISTER, "USER:" + user.getId(), user.getId());
- return response;
- }
- // ====================================
- // 聚合查询辅助方法
- // ====================================
- /**
- * 第三方信息同步(独立于登录,通过手机号定位用户,更新用户信息 + 入驻信息)
- */
- @Transactional
- public void syncExternalInfo(SyncInfoRequest request) {
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 更新用户信息字段(非空覆盖)
- if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setUserName(request.getUsername());
- }
- if (request.getNickname() != null && !request.getNickname().isBlank()) {
- user.setNickname(request.getNickname());
- } else if (request.getUsername() != null && !request.getUsername().isBlank()) {
- user.setNickname(request.getUsername());
- }
- if (request.getPharmacyName() != null && !request.getPharmacyName().isBlank()) {
- user.setPharmacyName(request.getPharmacyName());
- }
- if (request.getProvince() != null && !request.getProvince().isBlank()) {
- user.setProvince(request.getProvince());
- }
- if (request.getCity() != null && !request.getCity().isBlank()) {
- user.setCity(request.getCity());
- }
- if (request.getDistrict() != null && !request.getDistrict().isBlank()) {
- user.setDistrict(request.getDistrict());
- }
- if (request.getPharmacyAddress() != null && !request.getPharmacyAddress().isBlank()) {
- user.setPharmacyAddress(request.getPharmacyAddress());
- }
- if (request.getContactPerson() != null && !request.getContactPerson().isBlank()) {
- user.setContactPerson(request.getContactPerson());
- }
- if (request.getContactPhone() != null && !request.getContactPhone().isBlank()) {
- user.setContactPhone(request.getContactPhone());
- }
- if (request.getBusinessLicenseNo() != null && !request.getBusinessLicenseNo().isBlank()) {
- user.setBusinessLicenseNo(request.getBusinessLicenseNo());
- }
- if (request.getOpenId() != null && !request.getOpenId().isBlank()) {
- user.setWechatMiniOpenId(request.getOpenId());
- }
- if (request.getUnionId() != null && !request.getUnionId().isBlank()) {
- user.setWechatUnionId(request.getUnionId());
- }
- userMapper.updateById(user);
- // 同步入驻信息到 t_business_license
- syncBusinessLicense(request, user.getId());
- auditLogService.logSuccess(
- 0L, OperatorRole.SYSTEM,
- com.xuekairui.user.enums.OperationType.LICENSE_SYNC_THIRD,
- "SYNC_INFO:" + request.getPhone(),
- user.getId(),
- null,
- Map.of("phone", request.getPhone(), "userId", user.getId(),
- "status", request.getStatus(), "storeName", request.getStoreName()),
- "第三方同步用户信息:phone=" + request.getPhone(),
- com.xuekairui.user.enums.OperationSource.SYSTEM_SYNC);
- log.info("第三方信息同步完成: phone={}, userId={}", request.getPhone(), user.getId());
- }
- /**
- * 第三方同步用户店铺信息(无鉴权,按手机号自动创建/更新用户,仅返回同步确认)
- * <p>对应接口:POST /api/sync_shop_resource/report_user
- */
- @Transactional
- public SyncShopReportResponse reportUser(SyncShopReportRequest request) {
- String phone = request.getPhone();
- if (phone == null || phone.isBlank()) {
- throw new BusinessException(ErrorCode.PARAM_ERROR, "手机号不能为空");
- }
- // 1. 查找或创建用户
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
- boolean isNewUser = false;
- if (user == null) {
- isNewUser = true;
- user = User.builder()
- .phone(phone)
- .nickname("用户" + phone.substring(phone.length() - 4))
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("MINIAPP")
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- }
- // 2. 映射第三方参数 → 更新用户信息(非空覆盖)
- updateUserFromShopReport(user, request);
- userMapper.updateById(user);
- // 3. 同步入驻信息
- syncBusinessLicenseFromShopReport(request, user.getId());
- log.info("第三方店铺同步完成: phone={}, userId={}, newUser={}", phone, user.getId(), isNewUser);
- return SyncShopReportResponse.builder()
- .userId(String.valueOf(user.getId()))
- .newUser(isNewUser)
- .build();
- }
- /** 第三方 shop_type → 内部枚举字符串,兼容数字 "1"/"2"/"3" 和英文 */
- private static String parseShopType(String shopType) {
- if (shopType == null || shopType.isBlank()) {
- return null;
- }
- return switch (shopType) {
- case "1", "SINGLE" -> "SINGLE";
- case "2", "CHAIN" -> "CHAIN";
- case "3", "CLINIC", "COMMUNITY_HEALTH" -> "CLINIC";
- default -> shopType; // 未知值原样保留
- };
- }
- /** 映射第三方 status → int */
- private static int mapStatus(String status) {
- if (status == null || status.isBlank()) {
- return 0;
- }
- try {
- return Integer.parseInt(status);
- } catch (NumberFormatException e) {
- return 0;
- }
- }
- /** 更新用户信息字段 */
- private void updateUserFromShopReport(User user, SyncShopReportRequest req) {
- if (isNotBlank(req.getContactName())) {
- user.setNickname(req.getContactName());
- }
- if (isNotBlank(req.getContactShop())) {
- user.setPharmacyName(req.getContactShop());
- }
- if (isNotBlank(req.getContactProvince())) {
- user.setProvince(req.getContactProvince());
- }
- if (isNotBlank(req.getContactCity())) {
- user.setCity(req.getContactCity());
- }
- if (isNotBlank(req.getContactArea())) {
- user.setDistrict(req.getContactArea());
- }
- if (isNotBlank(req.getContactAddr())) {
- user.setPharmacyAddress(req.getContactAddr());
- }
- if (isNotBlank(req.getContactPhone())) {
- user.setContactPhone(req.getContactPhone());
- }
- }
- /** 同步入驻信息到 t_business_license */
- private void syncBusinessLicenseFromShopReport(SyncShopReportRequest req, Long userId) {
- String storeName = isNotBlank(req.getContactShop()) ? req.getContactShop() : null;
- String terminalType = parseShopType(req.getShopType());
- int status = mapStatus(req.getStatus());
- boolean hasLicenseField = isNotBlank(storeName)
- || terminalType != null
- || isNotBlank(req.getBusinessLicenseImage())
- || isNotBlank(req.getDrugBusinessLicenseImage())
- || isNotBlank(req.getTwoMedicalDeviceRegistration())
- || isNotBlank(req.getThreeMedicalDeviceBusinessLicense());
- if (!hasLicenseField) {
- return;
- }
- String reviewStatus;
- if (status == 0) {
- reviewStatus = LicenseStatus.PENDING.name();
- } else if (status == 1) {
- reviewStatus = LicenseStatus.APPROVED.name();
- } else {
- reviewStatus = LicenseStatus.REJECTED.name();
- }
- com.xuekairui.user.entity.BusinessLicense existing =
- businessLicenseService.getByUserId(userId);
- if (existing == null) {
- com.xuekairui.user.entity.BusinessLicense license =
- com.xuekairui.user.entity.BusinessLicense.builder()
- .userId(userId)
- .storeName(storeName)
- .terminalType(terminalType)
- .province(req.getContactProvince())
- .city(req.getContactCity())
- .district(req.getContactArea())
- .storeAddress(req.getContactAddr())
- .contactPerson(req.getContactName())
- .contactPhone(req.getContactPhone())
- .licenseImageUrl(req.getBusinessLicenseImage())
- .drugLicenseUrl(req.getDrugBusinessLicenseImage())
- .medicalDeviceClass2Url(req.getTwoMedicalDeviceRegistration())
- .medicalDeviceClass3Url(req.getThreeMedicalDeviceBusinessLicense())
- .reviewStatus(reviewStatus)
- .showVerifiedBadge(false)
- .syncSource("MINIAPP_SYNC")
- .remark(req.getRemarks())
- .rejectReason(LicenseStatus.REJECTED.name().equals(reviewStatus)
- ? req.getRemarks() : null)
- .build();
- businessLicenseService.insert(license);
- log.info("第三方店铺同步创建入驻记录: userId={}, licenseId={}, status={}",
- userId, license.getId(), reviewStatus);
- if (LicenseStatus.APPROVED.name().equals(reviewStatus)) {
- grantLicenseApprovedReward(userId, license.getId());
- }
- } else {
- // 已存在记录:更新入驻字段,原已审核通过的记录重置为PENDING重新审核
- boolean upgradedToApproved = false;
- boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
- if (!wasApproved) {
- // 本地未审核通过:允许第三方直接覆盖状态
- existing.setReviewStatus(reviewStatus);
- upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
- } else {
- // 本地已审核通过,第三方再次同步时更新字段并重置为PENDING重新审核
- // rewardGranted保持不变,确保不重复奖励
- existing.setReviewStatus(LicenseStatus.PENDING.name());
- existing.setShowVerifiedBadge(false);
- log.info("第三方店铺同步,原已审核通过,更新入驻字段并重置为PENDING待重新审核: licenseId={}, userId={}",
- existing.getId(), userId);
- }
- // 更新第三方传入的入驻字段(非空即覆盖)
- applyThirdPartyLicenseFields(existing, req, storeName, terminalType);
- // 拒绝时,remarks 同时保存为 rejectReason
- if (LicenseStatus.REJECTED.name().equals(existing.getReviewStatus()) && isNotBlank(req.getRemarks())) {
- existing.setRejectReason(req.getRemarks());
- }
- businessLicenseService.updateById(existing);
- log.info("第三方店铺同步已存在记录: licenseId={}, userId={}, wasApproved={}, newStatus={}, upgradedToApproved={}",
- existing.getId(), userId, wasApproved, existing.getReviewStatus(), upgradedToApproved);
- if (upgradedToApproved) {
- grantLicenseApprovedReward(userId, existing.getId());
- }
- }
- }
- /**
- * 将第三方传入的入驻字段覆盖到本地记录(非空即覆盖)
- */
- private void applyThirdPartyLicenseFields(
- com.xuekairui.user.entity.BusinessLicense existing,
- SyncShopReportRequest req, String storeName, String terminalType) {
- if (storeName != null) {
- existing.setStoreName(storeName);
- }
- if (terminalType != null) {
- existing.setTerminalType(terminalType);
- }
- if (isNotBlank(req.getContactProvince())) {
- existing.setProvince(req.getContactProvince());
- }
- if (isNotBlank(req.getContactCity())) {
- existing.setCity(req.getContactCity());
- }
- if (isNotBlank(req.getContactArea())) {
- existing.setDistrict(req.getContactArea());
- }
- if (isNotBlank(req.getContactAddr())) {
- existing.setStoreAddress(req.getContactAddr());
- }
- if (isNotBlank(req.getContactName())) {
- existing.setContactPerson(req.getContactName());
- }
- if (isNotBlank(req.getContactPhone())) {
- existing.setContactPhone(req.getContactPhone());
- }
- if (isNotBlank(req.getBusinessLicenseImage())) {
- existing.setLicenseImageUrl(req.getBusinessLicenseImage());
- }
- if (isNotBlank(req.getDrugBusinessLicenseImage())) {
- existing.setDrugLicenseUrl(req.getDrugBusinessLicenseImage());
- }
- if (isNotBlank(req.getTwoMedicalDeviceRegistration())) {
- existing.setMedicalDeviceClass2Url(req.getTwoMedicalDeviceRegistration());
- }
- if (isNotBlank(req.getThreeMedicalDeviceBusinessLicense())) {
- existing.setMedicalDeviceClass3Url(req.getThreeMedicalDeviceBusinessLicense());
- }
- if (isNotBlank(req.getRemarks())) {
- existing.setRemark(req.getRemarks());
- }
- }
- /**
- * 第三方登录接口同步入驻信息(适配 ExternalTokenRequest → SyncInfoRequest)
- */
- private void syncBusinessLicenseFromTokenRequest(ExternalTokenRequest req, Long userId) {
- SyncInfoRequest syncReq = new SyncInfoRequest();
- syncReq.setStoreName(req.getStoreName());
- syncReq.setPharmacyName(req.getPharmacyName());
- syncReq.setTerminalType(req.getTerminalType());
- syncReq.setBusinessLicenseUrl(req.getBusinessLicenseUrl());
- syncReq.setDrugLicenseUrl(req.getDrugLicenseUrl());
- syncReq.setMedicalDeviceClass2Url(req.getMedicalDeviceClass2Url());
- syncReq.setMedicalDeviceClass3Url(req.getMedicalDeviceClass3Url());
- syncReq.setProvince(req.getProvince());
- syncReq.setCity(req.getCity());
- syncReq.setDistrict(req.getDistrict());
- syncReq.setPharmacyAddress(req.getPharmacyAddress());
- syncReq.setContactPerson(req.getContactPerson());
- syncReq.setContactPhone(req.getContactPhone());
- syncReq.setBusinessLicenseNo(req.getBusinessLicenseNo());
- syncReq.setStatus(req.getStatus());
- syncReq.setRemarks(req.getRemarks());
- syncBusinessLicense(syncReq, userId);
- }
- private void syncBusinessLicense(SyncInfoRequest request, Long userId) {
- // 先更新用户基本信息(非空即覆盖),确保入驻信息同步时用户信息也一并保存
- User user = userMapper.selectById(userId);
- if (user != null) {
- applySyncInfoUserFields(user, request);
- userMapper.updateById(user);
- }
- String storeName = (request.getStoreName() != null && !request.getStoreName().isBlank())
- ? request.getStoreName() : request.getPharmacyName();
- // 第三方终端类型(可能是数字字符串1/2/3),统一转换
- String terminalType = parseShopType(request.getTerminalType());
- boolean hasLicenseField = isNotBlank(storeName)
- || terminalType != null
- || isNotBlank(request.getBusinessLicenseUrl())
- || isNotBlank(request.getDrugLicenseUrl())
- || isNotBlank(request.getMedicalDeviceClass2Url())
- || isNotBlank(request.getMedicalDeviceClass3Url());
- if (!hasLicenseField) {
- return;
- }
- com.xuekairui.user.entity.BusinessLicense existing =
- businessLicenseService.getByUserId(userId);
- // 审核状态:0-审核中→PENDING / 1-审核成功→APPROVED / 2-审核失败→REJECTED(不传默认0)
- Integer status = request.getStatus();
- String reviewStatus;
- if (status == null || status == 0) {
- reviewStatus = LicenseStatus.PENDING.name();
- } else if (status == 1) {
- reviewStatus = LicenseStatus.APPROVED.name();
- } else {
- reviewStatus = LicenseStatus.REJECTED.name();
- }
- if (existing == null) {
- com.xuekairui.user.entity.BusinessLicense license =
- com.xuekairui.user.entity.BusinessLicense.builder()
- .userId(userId)
- .storeName(storeName)
- .terminalType(terminalType)
- .province(request.getProvince())
- .city(request.getCity())
- .district(request.getDistrict())
- .storeAddress(request.getPharmacyAddress())
- .contactPerson(request.getContactPerson())
- .contactPhone(request.getContactPhone())
- .licenseImageUrl(request.getBusinessLicenseUrl())
- .drugLicenseUrl(request.getDrugLicenseUrl())
- .creditCode(request.getBusinessLicenseNo())
- .medicalDeviceClass2Url(request.getMedicalDeviceClass2Url())
- .medicalDeviceClass3Url(request.getMedicalDeviceClass3Url())
- .reviewStatus(reviewStatus)
- .showVerifiedBadge(false)
- .syncSource("MINIAPP_SYNC")
- .remark(request.getRemarks())
- .rejectReason(LicenseStatus.REJECTED.name().equals(reviewStatus)
- ? request.getRemarks() : null)
- .build();
- businessLicenseService.insert(license);
- log.info("第三方用户{}同步入驻信息,创建新记录: licenseId={}, status={}",
- userId, license.getId(), reviewStatus);
- // 第三方已审核通过:赠送30天高级会员 + 推送Redis事件
- if (LicenseStatus.APPROVED.name().equals(reviewStatus)) {
- grantLicenseApprovedReward(userId, license.getId());
- }
- } else {
- // 已存在记录:更新入驻字段,原已审核通过的记录重置为PENDING重新审核
- boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
- boolean upgradedToApproved = false;
- if (!wasApproved) {
- // 本地未审核通过:允许第三方直接覆盖状态
- existing.setReviewStatus(reviewStatus);
- upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
- } else {
- // 本地已审核通过,第三方再次同步时更新字段并重置为PENDING重新审核
- // rewardGranted保持不变,确保不重复奖励
- existing.setReviewStatus(LicenseStatus.PENDING.name());
- existing.setShowVerifiedBadge(false);
- log.info("第三方用户{}重新同步入驻信息,原已审核通过,重置为PENDING待重新审核: licenseId={}",
- userId, existing.getId());
- }
- // 更新第三方传入的入驻字段(非空即覆盖)
- applySyncInfoLicenseFields(existing, request, storeName);
- // 拒绝时,remarks 同时保存为 rejectReason
- if (LicenseStatus.REJECTED.name().equals(existing.getReviewStatus()) && isNotBlank(request.getRemarks())) {
- existing.setRejectReason(request.getRemarks());
- }
- businessLicenseService.updateById(existing);
- log.info("第三方用户{}重新同步入驻信息: licenseId={}, wasApproved={}, newStatus={}",
- userId, existing.getId(), wasApproved, existing.getReviewStatus());
- // 从非APPROVED升级到APPROVED:赠送30天高级会员 + 推送Redis事件
- if (upgradedToApproved) {
- grantLicenseApprovedReward(userId, existing.getId());
- }
- }
- }
- private boolean isNotBlank(String s) {
- return s != null && !s.isBlank();
- }
- /**
- * 将 SyncInfoRequest 中的入驻字段覆盖到本地记录(非空即覆盖)
- */
- private void applySyncInfoLicenseFields(
- com.xuekairui.user.entity.BusinessLicense existing,
- SyncInfoRequest req, String storeName) {
- if (storeName != null) {
- existing.setStoreName(storeName);
- }
- if (isNotBlank(req.getTerminalType())) {
- existing.setTerminalType(req.getTerminalType());
- }
- if (isNotBlank(req.getProvince())) {
- existing.setProvince(req.getProvince());
- }
- if (isNotBlank(req.getCity())) {
- existing.setCity(req.getCity());
- }
- if (isNotBlank(req.getDistrict())) {
- existing.setDistrict(req.getDistrict());
- }
- if (isNotBlank(req.getPharmacyAddress())) {
- existing.setStoreAddress(req.getPharmacyAddress());
- }
- if (isNotBlank(req.getContactPerson())) {
- existing.setContactPerson(req.getContactPerson());
- }
- if (isNotBlank(req.getContactPhone())) {
- existing.setContactPhone(req.getContactPhone());
- }
- if (isNotBlank(req.getBusinessLicenseUrl())) {
- existing.setLicenseImageUrl(req.getBusinessLicenseUrl());
- }
- if (isNotBlank(req.getDrugLicenseUrl())) {
- existing.setDrugLicenseUrl(req.getDrugLicenseUrl());
- }
- if (isNotBlank(req.getMedicalDeviceClass2Url())) {
- existing.setMedicalDeviceClass2Url(req.getMedicalDeviceClass2Url());
- }
- if (isNotBlank(req.getMedicalDeviceClass3Url())) {
- existing.setMedicalDeviceClass3Url(req.getMedicalDeviceClass3Url());
- }
- if (isNotBlank(req.getBusinessLicenseNo())) {
- existing.setCreditCode(req.getBusinessLicenseNo());
- }
- if (isNotBlank(req.getRemarks())) {
- existing.setRemark(req.getRemarks());
- }
- }
- /**
- * 将 SyncInfoRequest 中的用户字段覆盖到本地 User 记录(非空即覆盖)
- */
- private void applySyncInfoUserFields(User user, SyncInfoRequest req) {
- if (isNotBlank(req.getUsername())) {
- user.setUserName(req.getUsername());
- }
- if (isNotBlank(req.getNickname())) {
- user.setNickname(req.getNickname());
- } else if (isNotBlank(req.getUsername())) {
- user.setNickname(req.getUsername());
- }
- if (isNotBlank(req.getPharmacyName())) {
- user.setPharmacyName(req.getPharmacyName());
- }
- if (isNotBlank(req.getProvince())) {
- user.setProvince(req.getProvince());
- }
- if (isNotBlank(req.getCity())) {
- user.setCity(req.getCity());
- }
- if (isNotBlank(req.getDistrict())) {
- user.setDistrict(req.getDistrict());
- }
- if (isNotBlank(req.getPharmacyAddress())) {
- user.setPharmacyAddress(req.getPharmacyAddress());
- }
- if (isNotBlank(req.getContactPerson())) {
- user.setContactPerson(req.getContactPerson());
- }
- if (isNotBlank(req.getContactPhone())) {
- user.setContactPhone(req.getContactPhone());
- }
- if (isNotBlank(req.getBusinessLicenseNo())) {
- user.setBusinessLicenseNo(req.getBusinessLicenseNo());
- }
- if (isNotBlank(req.getOpenId())) {
- user.setWechatMiniOpenId(req.getOpenId());
- }
- if (isNotBlank(req.getUnionId())) {
- user.setWechatUnionId(req.getUnionId());
- }
- }
- /**
- * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
- * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
- * <p>幂等性检查:通过 DB 原子 CAS(UPDATE WHERE reward_granted = 0)保证同一入驻记录
- * 全局仅奖励一次,杜绝并发场景下的竞态条件。
- */
- private void grantLicenseApprovedReward(Long userId, Long licenseId) {
- // 原子标记奖励已发放(CAS:仅当 reward_granted=0 时更新为1),
- // 杜绝并发场景下 rewardGranted 检查与更新的竞态条件,确保全局仅发放一次
- int claimed = businessLicenseService.markRewardGrantedAtomic(licenseId);
- if (claimed == 0) {
- log.info("用户{}的入驻记录奖励已被标记,跳过重复赠送: licenseId={}", userId, licenseId);
- return;
- }
- membershipService.grantOrExtendMembership(
- userId,
- MembershipLevel.PRO,
- 30,
- "LICENSE",
- "入驻信息审核通过赠送30天高级会员");
- log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
- // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
- // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
- TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
- @Override
- public void afterCommit() {
- try {
- activityEventRedisService.pushLicenseApproved(userId, licenseId, "BUSINESS_LICENSE");
- } catch (Exception e) {
- log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
- }
- }
- });
- }
- private UserGateStatus buildGateStatus(Long userId) {
- // 执照审核状态
- com.xuekairui.user.entity.BusinessLicense license =
- businessLicenseService.getByUserId(userId);
- String licenseStatus = license != null ? license.getReviewStatus() : LicenseStatus.NOT_SUBMITTED.name();
- // 已绑定平台数
- Long boundCount = platformAccountMapper.selectCount(
- new LambdaQueryWrapper<com.xuekairui.user.entity.PlatformAccount>()
- .eq(com.xuekairui.user.entity.PlatformAccount::getUserId, userId)
- .eq(com.xuekairui.user.entity.PlatformAccount::getEnabled, true));
- boolean canQuery = LicenseStatus.APPROVED.name().equals(licenseStatus) && boundCount >= 1;
- java.util.List<String> messages = new java.util.ArrayList<>();
- if (!LicenseStatus.APPROVED.name().equals(licenseStatus)) {
- messages.add("请先提交入驻信息并通过审核");
- }
- if (boundCount == 0) {
- messages.add("请至少绑定1个B2B平台账号");
- }
- return UserGateStatus.builder()
- .canQueryOnDesktop(canQuery)
- .licenseStatus(licenseStatus)
- .platformBoundCount(boundCount.intValue())
- .gateMessages(messages)
- .build();
- }
- private java.util.Map<String, Object> buildQuotaSummary(Long userId) {
- // 今日已用 + 每日上限
- int dailyUsed = crawlerUsageLogMapper.sumTodayUsage(userId);
- java.util.Map<String, Object> quota = new java.util.LinkedHashMap<>();
- quota.put("dailyQueryUsed", dailyUsed);
- quota.put("dailyQueryLimit", 20); // TODO: 从会员等级动态获取
- return quota;
- }
- private java.util.List<java.util.Map<String, Object>> getPlatformBindings(Long userId) {
- java.util.List<com.xuekairui.user.entity.PlatformAccount> accounts =
- platformAccountMapper.selectList(
- new LambdaQueryWrapper<com.xuekairui.user.entity.PlatformAccount>()
- .eq(com.xuekairui.user.entity.PlatformAccount::getUserId, userId));
- return accounts.stream().map(a -> {
- java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();
- m.put("platformCode", a.getPlatformCode());
- m.put("platformName", a.getPlatformName());
- m.put("status", Boolean.TRUE.equals(a.getEnabled()) ? "bound" : "disabled");
- m.put("lastVerifyTime", a.getLastVerifyTime());
- return m;
- }).toList();
- }
- private java.util.Map<String, Object> toMap(UserInfoResponse info) {
- // 简单反射转 Map,避免引入额外依赖
- java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
- map.put("id", info.getId());
- map.put("phone", info.getPhone());
- map.put("nickname", info.getNickname());
- map.put("avatar", info.getAvatar());
- map.put("wechatBound", info.getWechatBound());
- map.put("createTime", info.getCreateTime());
- map.put("loginSource", info.getLoginSource());
- map.put("pharmacyName", info.getPharmacyName());
- map.put("province", info.getProvince());
- map.put("city", info.getCity());
- map.put("district", info.getDistrict());
- map.put("role", info.getRole());
- map.put("membershipLevel", info.getMembershipLevel());
- map.put("membershipLevelName", info.getMembershipLevelName());
- map.put("inviterId", info.getInviterId());
- map.put("inviterNickname", info.getInviterNickname());
- map.put("hasBoundInviteCode", info.getHasBoundInviteCode());
- map.put("licenseStatus", info.getLicenseStatus());
- map.put("levelCode", info.getLevelCode());
- map.put("version", info.getVersion());
- return map;
- }
- /**
- * Refresh Token 旋转 + 重用检测
- * <p>
- * 每次刷新:旧 Refresh Token 标记为 USED,签发新的一对 Token。
- * 如果检测到已 USED 的 Refresh Token 被再次使用 → 说明被盗 → 踢出所有设备。
- */
- @Transactional
- public LoginResponse refreshToken(String refreshToken) {
- // 1. 验证JWT格式
- String tokenType = jwtUtil.getTokenType(refreshToken);
- if (!"refresh".equals(tokenType)) {
- throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
- }
- Long userId = jwtUtil.getUserId(refreshToken);
- // 只查 Refresh Token 校验所需的字段,减少DB开销
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>()
- .select(User::getId, User::getPhone,
- User::getTokenVersion, User::getRefreshTokenHash,
- User::getRefreshTokenStatus, User::getStatus)
- .eq(User::getId, userId));
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 2. 计算哈希,比对DB中的记录
- String tokenHash = JwtUtil.hashToken(refreshToken);
- String storedHash = user.getRefreshTokenHash();
- String storedStatus = user.getRefreshTokenStatus();
- // 3. 重用检测:Refresh Token 已被使用过 → 被盗!
- if (tokenHash.equals(storedHash) && "USED".equals(storedStatus)) {
- log.warn("用户 {} 的Refresh Token被重用(疑似被盗),踢出所有设备", userId);
- // 递增 tokenVersion,使所有旧 access token 立即失效
- long newVersion = (user.getTokenVersion() != null ? user.getTokenVersion() : 0L) + 1;
- user.setTokenVersion(newVersion);
- user.setRefreshTokenHash(null);
- user.setRefreshTokenStatus(null);
- userMapper.updateById(user);
- tokenVersionCache.putVersion(userId, false, newVersion);
- throw new BusinessException(ErrorCode.TOKEN_KICKED);
- }
- // 4. 校验:哈希不匹配 → 无效或过期的 Refresh Token
- if (!tokenHash.equals(storedHash) || !"ACTIVE".equals(storedStatus)) {
- throw new BusinessException(ErrorCode.REFRESH_TOKEN_INVALID);
- }
- // 5. 旋转:标记旧 Refresh Token 为 USED,签发新的一对
- user.setRefreshTokenStatus("USED");
- String roleCode = "USER";
- long currentVersion = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- String loginSource = jwtUtil.getLoginSource(refreshToken);
- String newAccessToken = jwtUtil.generateAccessToken(
- user.getId(), roleCode, loginSource, currentVersion);
- String newRefreshToken = jwtUtil.generateRefreshToken(user.getId(), loginSource);
- // 存储新 Refresh Token 哈希
- user.setRefreshTokenHash(JwtUtil.hashToken(newRefreshToken));
- user.setRefreshTokenStatus("ACTIVE");
- userMapper.updateById(user);
- log.debug("用户 {} Refresh Token 旋转成功", userId);
- return LoginResponse.builder()
- .userId(user.getId())
- .accessToken(newAccessToken)
- .refreshToken(newRefreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .userInfo(convertToUserInfoResponse(user))
- .needBindPhone(false)
- .newUser(false)
- .role(roleCode)
- .build();
- }
- /** PC 端构建登录响应:
- * 递增 tokenVersion,踢掉旧 PC,正常刷新 token */
- private LoginResponse buildLoginResponse(User user) {
- return buildLoginResponseInternal(user, true, "PC", false);
- }
- /** 小程序构建登录响应:不递增 tokenVersion,不踢 PC,正常刷新 token */
- private LoginResponse buildMiniAppLoginResponse(User user) {
- return buildLoginResponseInternal(user, false, "MINIAPP", false);
- }
- /**
- * 构建登录响应(内部通用方法)
- * @param skipRefreshToken 不生成/保存新 refreshToken(仅返回 accessToken)
- */
- private LoginResponse buildLoginResponseInternal(User user, boolean incrementVersion,
- String loginSource, boolean skipRefreshToken) {
- MDC.put("uid", String.valueOf(user.getId()));
- String roleCode = "USER";
- long version;
- if (incrementVersion) {
- long oldVersion = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- version = oldVersion + 1;
- user.setTokenVersion(version);
- tokenVersionCache.putVersion(user.getId(), false, version);
- } else {
- version = user.getTokenVersion() != null ? user.getTokenVersion() : 0L;
- }
- String accessToken = jwtUtil.generateAccessToken(
- user.getId(), "USER", loginSource, version);
- String refreshToken = null;
- if (!skipRefreshToken) {
- // 仅当无 ACTIVE 的 refreshToken 时才生成新的(首次登录),已有则复用
- // 避免 PC/小程序两端登录互相覆盖 refreshToken 导致另一端无法刷新
- if (user.getRefreshTokenHash() == null || !"ACTIVE".equals(user.getRefreshTokenStatus())) {
- refreshToken = jwtUtil.generateRefreshToken(user.getId(), loginSource);
- user.setRefreshTokenHash(JwtUtil.hashToken(refreshToken));
- user.setRefreshTokenStatus("ACTIVE");
- }
- }
- userMapper.updateById(user);
- // 记录登录日志(用于区分不同端最后登录时间)
- recordLoginLog(user.getId(), user.getLoginSource(), user.getLastLoginIp());
- return LoginResponse.builder()
- .userId(user.getId())
- .accessToken(accessToken)
- .refreshToken(refreshToken)
- .expiresIn(jwtUtil.getExpirationInSeconds(roleCode, loginSource))
- .userInfo(convertToUserInfoResponse(user))
- .needBindPhone(false)
- .newUser(false)
- .role(roleCode)
- .build();
- }
- /**
- * 登录时检查入驻信息并同步到第三方(智价云药店版)
- * <p>
- * 仅在我方系统自行登录(短信/密码/微信/小程序)时触发,第三方登录不走此逻辑。
- * 幂等策略:仅当入驻记录存在且 status != 1(未同步成功)时才补推一次,
- * 避免每次登录都重复调用第三方接口。
- * 同步在事务提交后异步执行,不阻塞登录响应,失败仅记录日志。
- */
- private void syncShopResourceOnLogin(Long userId) {
- if (userId == null) {
- return;
- }
- try {
- com.xuekairui.user.entity.BusinessLicense license =
- businessLicenseService.getByUserId(userId);
- if (license == null) {
- return;
- }
- // status=1 表示已同步成功,跳过;0=未同步,2=同步失败,需补推
- if (license.getStatus() != null && license.getStatus() == 1) {
- return;
- }
- User user = userMapper.selectById(userId);
- if (user == null) {
- return;
- }
- String phone = user.getPhone();
- String userName = user.getNickname() != null
- ? user.getNickname() : String.valueOf(userId);
- String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
- String targetIdentifier = String.format("PHONE:%s|USER:%d|LICENSE:%d",
- phone != null ? phone : "UNKNOWN", userId, license.getId());
- TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
- @Override
- public void afterCommit() {
- try {
- ShopResourceSyncService.SyncResult result =
- shopResourceSyncService.syncShopResource(user, license);
- if (result != null && result.getUserId() != null) {
- license.setExternalId(result.getUserId());
- license.setStatus(1);
- businessLicenseService.updateById(license);
- log.info("登录时同步入驻信息到第三方成功: userId={}, externalUserId={}",
- userId, result.getUserId());
- auditLogService.logSuccess(
- 0L,
- OperatorRole.SYSTEM,
- OperationType.LICENSE_SYNC_THIRD,
- targetIdentifier,
- license.getId(),
- null,
- Map.of("userId", userId, "userName", userName, "storeName", storeName,
- "phone", phone != null ? phone : "UNKNOWN",
- "trigger", "LOGIN",
- "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
- String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方成功,第三方用户ID:%s",
- userName, phone != null ? phone : "UNKNOWN", storeName, result.getUserId()),
- OperationSource.SYSTEM_SYNC);
- } else {
- license.setStatus(2);
- businessLicenseService.updateById(license);
- log.warn("登录时同步入驻信息到第三方未成功(未返回user_id): userId={}", userId);
- auditLogService.logSuccess(
- 0L,
- OperatorRole.SYSTEM,
- OperationType.LICENSE_SYNC_THIRD,
- targetIdentifier,
- license.getId(),
- null,
- Map.of("userId", userId, "userName", userName, "storeName", storeName,
- "phone", phone != null ? phone : "UNKNOWN",
- "trigger", "LOGIN", "result", "no_user_id"),
- String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方未成功:第三方未返回用户ID",
- userName, phone != null ? phone : "UNKNOWN", storeName),
- OperationSource.SYSTEM_SYNC);
- }
- } catch (Exception e) {
- log.warn("登录时同步入驻信息到第三方失败(不阻断登录): userId={}, error={}",
- userId, e.getMessage());
- auditLogService.logSuccess(
- 0L,
- OperatorRole.SYSTEM,
- OperationType.LICENSE_SYNC_THIRD,
- targetIdentifier,
- license.getId(),
- null,
- Map.of("userId", userId, "userName", userName, "storeName", storeName,
- "phone", phone != null ? phone : "UNKNOWN",
- "trigger", "LOGIN", "error", e.getMessage()),
- String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方失败:%s",
- userName, phone != null ? phone : "UNKNOWN", storeName, e.getMessage()),
- OperationSource.SYSTEM_SYNC);
- }
- }
- });
- } catch (Exception e) {
- log.warn("登录时检查入驻信息失败(不阻断登录): userId={}, error={}", userId, e.getMessage());
- }
- }
- /**
- * 转换用户信息响应
- */
- private UserInfoResponse convertToUserInfoResponse(User user) {
- UserLevelResponse levelInfo = null;
- if (user != null && user.getId() != null) {
- try {
- levelInfo = membershipService.getUserLevel(user.getId());
- } catch (Exception e) {
- log.warn("获取用户等级信息失败: userId={}, error={}", user.getId(), e.getMessage());
- }
- }
- // 营业执照审核状态
- String licenseStatus = LicenseStatus.NOT_SUBMITTED.name();
- try {
- assert user != null;
- com.xuekairui.user.entity.BusinessLicense license =
- businessLicenseService.getByUserId(user.getId());
- if (license != null && license.getReviewStatus() != null) {
- licenseStatus = license.getReviewStatus();
- }
- } catch (Exception e) {
- log.warn("查询营业执照状态失败: userId={}, error={}", user.getId(), e.getMessage());
- }
- // 邀请人信息
- Long inviterId = user.getInviterId();
- String inviterNickname = null;
- if (inviterId != null) {
- User inviter = userMapper.selectById(inviterId);
- if (inviter != null) {
- inviterNickname = inviter.getWechatNickname() != null ? inviter.getWechatNickname()
- : inviter.getNickname() != null ? inviter.getNickname()
- : "一位用户";
- }
- }
- return UserInfoResponse.builder()
- .id(user.getId())
- .phone(ValidateUtil.maskPhone(user.getPhone()))
- .nickname(user.getNickname())
- .userName(user.getUserName())
- .avatar(user.getAvatar())
- .wechatBound(user.getWechatOpenId() != null)
- .levelInfo(levelInfo)
- .createTime(user.getCreateTime())
- .loginSource(user.getLoginSource() != null ? user.getLoginSource() : "UNKNOWN")
- .pharmacyName(user.getPharmacyName())
- .province(user.getProvince())
- .city(user.getCity())
- .district(user.getDistrict())
- .role("USER")
- .membershipLevel(levelInfo != null ? levelInfo.getLevelCode() : "PLUS")
- .membershipLevelName(levelInfo != null ? levelInfo.getLevelName() : "普通会员")
- .inviterId(inviterId)
- .inviterNickname(inviterNickname)
- .hasBoundInviteCode(inviterId != null)
- .licenseStatus(licenseStatus)
- .build();
- }
- /**
- * 密码登录
- * 支持手机号或用户名登录:phone 和 username 至少填一个
- */
- @Transactional
- public LoginResponse passwordLogin(PasswordLoginRequest request, String ip) {
- // 1. 校验:phone 和 username 至少填一个
- boolean hasPhone = request.getPhone() != null && !request.getPhone().isBlank();
- boolean hasUsername = request.getUsername() != null && !request.getUsername().isBlank();
- if (!hasPhone && !hasUsername) {
- throw new BusinessException(ErrorCode.PARAM_ERROR, "手机号和用户名至少填写一个");
- }
- // 2. 查找用户(优先手机号,其次用户名/昵称)
- User user;
- if (hasPhone) {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
- } else {
- user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getNickname, request.getUsername()));
- }
- if (user == null) {
- return adminAuthService.passwordLogin(
- hasPhone ? request.getPhone() : request.getUsername(),
- request.getPassword(), ip);
- }
- // 2. 检查是否设置了密码
- if (user.getPassword() == null) {
- throw new BusinessException(ErrorCode.PASSWORD_NOT_SET);
- }
- // 3. 验证密码
- if (!passwordUtil.matches(request.getPassword(), user.getPassword())) {
- throw new BusinessException(ErrorCode.PASSWORD_INCORRECT);
- }
- // 4. 检查用户状态
- if (user.getStatus() != null && user.getStatus() == 0) {
- throw new BusinessException(ErrorCode.USER_DISABLED);
- }
- // 5. 更新登录信息
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("PC");
- userMapper.updateById(user);
- // 6. 生成Token
- LoginResponse response = buildLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
- syncShopResourceOnLogin(user.getId());
- return response;
- }
- /**
- * 设置/修改密码
- */
- @Transactional
- public void setPassword(Long userId, SetPasswordRequest request) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 如果已有密码,需要验证旧密码
- if (user.getPassword() != null && request.getOldPassword() != null) {
- if (!passwordUtil.matches(request.getOldPassword(), user.getPassword())) {
- throw new BusinessException(ErrorCode.OLD_PASSWORD_INCORRECT);
- }
- } else if (user.getPassword() != null) {
- throw new BusinessException(ErrorCode.OLD_PASSWORD_REQUIRED);
- }
- // 加密并更新密码
- user.setPassword(passwordUtil.encode(request.getNewPassword()));
- userMapper.updateById(user);
- log.info("用户{}设置/修改密码成功", userId);
- auditLogService.logSuccess(userId, user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_UPDATE, "PHONE:" + userId, userId);
- }
- /**
- * 修改手机号
- */
- @Transactional
- public void changePhone(Long userId, ChangePhoneRequest request) {
- User user = userMapper.selectById(userId);
- if (user == null) {
- throw new BusinessException(ErrorCode.USER_NOT_FOUND);
- }
- // 1. 验证验证码(使用新手机号和change场景)
- verificationCodeService.verifyCode(request.getNewPhone(), request.getCode());
- // 2. 检查新手机号是否已被其他用户占用
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, request.getNewPhone()));
- if (existUser != null && !existUser.getId().equals(userId)) {
- throw new BusinessException(ErrorCode.PHONE_ALREADY_EXISTS);
- }
- // 3. 更新手机号
- String oldPhone = user.getPhone();
- user.setPhone(request.getNewPhone());
- userMapper.updateById(user);
- log.info("用户{}修改手机号成功,旧手机号:{},新手机号:{}", userId, oldPhone, request.getNewPhone());
- auditLogService.logSuccess(userId, user.getNickname(), OperatorRole.USER.getCode(),
- OperationType.USER_UPDATE, "PHONE:" + userId, userId);
- }
- // ====================================
- // 小程序统一登录(处理四种用户身份场景)
- // ====================================
- /**
- * 小程序统一登录
- * <p>
- * 自动处理"小程序老/新用户 × 当前系统老/新用户"四种场景:
- * <ol>
- * <li>外部老 + 本地老 → 直接登录</li>
- * <li>外部老 + 本地新 → 本地注册 + 异步同步证书</li>
- * <li>外部新 + 本地老 → 异步注册外部 + 本地直接登录</li>
- * <li>外部新 + 本地新 → 两端都注册</li>
- * </ol>
- * <p>
- * 关键原则:外部系统操作异步执行,不阻塞登录响应。
- */
- @Transactional
- public LoginResponse miniappUnifiedLogin(MiniProgramLoginRequest request, String ip) {
- // 1. code2Session
- WechatMiniProgramService.MiniProgramSessionInfo session =
- wechatMiniProgramService.code2Session(request.getJsCode());
- // 2. 获取手机号(如果提供了 phoneCode)
- String phone = null;
- if (request.getPhoneCode() != null && !request.getPhoneCode().isBlank()) {
- try {
- phone = wechatMiniProgramService.getPhoneNumber(request.getPhoneCode());
- } catch (Exception e) {
- log.warn("获取小程序手机号失败(不阻断登录): {}", e.getMessage());
- }
- }
- // 3. 并行检查双系统
- User user = findUserByWechatIdentity(session.getUnionId(), session.getOpenId());
- boolean existsExternal = checkExternalUserSafe(session.getUnionId());
- boolean isNewLocal = (user == null);
- log.info("小程序统一登录: unionId={}, isNewLocal={}, existsExternal={}",
- session.getUnionId(), isNewLocal, existsExternal);
- // 4. 场景路由
- if (!isNewLocal && existsExternal) {
- // 场景1: 两端都有 → 更新微信信息,直接登录
- updateWechatMiniInfo(user, session.getOpenId(), session.getUnionId(), phone, ip);
- } else if (isNewLocal && existsExternal) {
- // 场景2: 外部老 + 本地新 → 本地注册 + 异步同步证书
- user = createUserFromMiniProgram(session, phone, request.getInviteCode(), ip);
- final Long newUserId = user.getId();
- final String syncUnionId = session.getUnionId();
- try {
- certificateSyncService.syncCertificatesFromExternal(newUserId, syncUnionId);
- } catch (Exception e) {
- log.warn("触发证书同步失败(不阻断登录): userId={}", newUserId, e);
- }
- } else if (!isNewLocal) {
- // 场景3: 外部新 + 本地老 → 更新微信信息,异步注册外部
- updateWechatMiniInfo(user, session.getOpenId(), session.getUnionId(), phone, ip);
- asyncRegisterInExternal(user, session.getOpenId());
- } else {
- // 场景4: 两端都新 → 本地注册 + 异步注册外部
- user = createUserFromMiniProgram(session, phone, request.getInviteCode(), ip);
- asyncRegisterInExternal(user, session.getOpenId());
- }
- // 5. 没有手机号 → 返回需要绑定
- if (user.getPhone() == null) {
- String tempToken = jwtUtil.generateTempToken(session.getOpenId(), session.getUnionId());
- MDC.put("uid", String.valueOf(user.getId()));
- return LoginResponse.builder()
- .needBindPhone(true)
- .tempToken(tempToken)
- .userInfo(convertToUserInfoResponse(user))
- .build();
- }
- // 6. 更新登录信息并签发 Token
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- userMapper.updateById(user);
- LoginResponse response = buildMiniAppLoginResponse(user);
- auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
- isNewLocal ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
- "USER:" + user.getId(), user.getId());
- syncShopResourceOnLogin(user.getId());
- return response;
- }
- /**
- * 小程序绑定手机号(needBindPhone=true 时调用)
- */
- @Transactional
- public LoginResponse miniappBindPhone(MiniProgramPhoneRequest request, String ip) {
- // 1. 验证临时 Token
- Claims claims = jwtUtil.parseToken(request.getTempToken());
- if (!"temp".equals(claims.get("type", String.class))) {
- throw new BusinessException(ErrorCode.TOKEN_INVALID);
- }
- String openId = claims.get("openId", String.class);
- String unionId = claims.get("unionId", String.class);
- // 2. 获取手机号
- String phone = wechatMiniProgramService.getPhoneNumber(request.getPhoneCode());
- // 3. 查找小程序用户
- User wechatUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, openId));
- if (wechatUser == null) {
- throw new BusinessException(ErrorCode.WECHAT_LOGIN_FAILED);
- }
- // 4. 检查手机号是否已被其他用户绑定
- User existUser = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
- if (existUser != null && !existUser.getId().equals(wechatUser.getId())) {
- // 手机号已被其他账号绑定 → 合并账号
- existUser.setWechatMiniOpenId(openId);
- if (unionId != null) {
- existUser.setWechatUnionId(unionId);
- }
- existUser.setLoginSource("MINIAPP");
- existUser.setLastLoginTime(LocalDateTime.now());
- existUser.setLastLoginIp(ip);
- userMapper.updateById(existUser);
- // 禁用旧的小程序用户(不物理删除,仅改状态)
- wechatUser.setStatus(0);
- userMapper.updateById(wechatUser);
- wechatUser = existUser;
- } else if (existUser == null) {
- // 绑定手机号 + 补全微信身份标识(完成注册)
- wechatUser.setPhone(phone);
- if (unionId != null && wechatUser.getWechatUnionId() == null) {
- wechatUser.setWechatUnionId(unionId);
- }
- wechatUser.setLoginSource("MINIAPP");
- wechatUser.setLastLoginTime(LocalDateTime.now());
- wechatUser.setLastLoginIp(ip);
- userMapper.updateById(wechatUser);
- eventPublisher.publishEvent(new UserRegisteredEvent(
- this, wechatUser.getId(), request.getInviteCode()));
- }
- syncShopResourceOnLogin(wechatUser.getId());
- return buildMiniAppLoginResponse(wechatUser);
- }
- // ====================================
- // 小程序登录辅助方法
- // ====================================
- /** 通过微信身份标识查找用户(unionId 优先,fallback miniOpenId) */
- private User findUserByWechatIdentity(String unionId, String miniOpenId) {
- if (unionId != null) {
- User user = userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatUnionId, unionId));
- if (user != null) {
- return user;
- }
- }
- if (miniOpenId != null) {
- return userMapper.selectOne(
- new LambdaQueryWrapper<User>().eq(User::getWechatMiniOpenId, miniOpenId));
- }
- return null;
- }
- /** 安全检查外部用户(不可用时降级为"外部新用户",不阻塞流程) */
- private boolean checkExternalUserSafe(String unionId) {
- try {
- return externalUserSyncService.userExists(unionId);
- } catch (Exception e) {
- log.warn("检查外部用户失败(降级为外部新用户): unionId={}", unionId, e);
- return false;
- }
- }
- /** 从小程序注册新用户 */
- private User createUserFromMiniProgram(
- WechatMiniProgramService.MiniProgramSessionInfo session,
- String phone, String inviteCode, String ip) {
- User user = User.builder()
- .wechatMiniOpenId(session.getOpenId())
- .wechatUnionId(session.getUnionId())
- .phone(phone)
- .nickname("药店用户")
- .membershipLevel(MembershipLevel.PLUS)
- .loginSource("MINIAPP")
- .lastLoginTime(LocalDateTime.now())
- .lastLoginIp(ip)
- .status(1)
- .build();
- userMapper.insert(user);
- membershipService.initPlusMembership(user.getId());
- if (inviteCode != null) {
- eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getId(), inviteCode));
- }
- log.info("小程序新用户注册: userId={}, unionId={}", user.getId(), session.getUnionId());
- return user;
- }
- /** 更新已有用户的微信小程序信息(始终写入 DB 保存登录时间戳和身份信息) */
- private void updateWechatMiniInfo(User user, String miniOpenId, String unionId,
- String phone, String ip) {
- if (miniOpenId != null && !miniOpenId.equals(user.getWechatMiniOpenId())) {
- user.setWechatMiniOpenId(miniOpenId);
- }
- if (unionId != null && !unionId.equals(user.getWechatUnionId())) {
- user.setWechatUnionId(unionId);
- }
- if (phone != null && user.getPhone() == null) {
- user.setPhone(phone);
- }
- user.setLastLoginTime(LocalDateTime.now());
- user.setLastLoginIp(ip);
- user.setLoginSource("MINIAPP");
- userMapper.updateById(user);
- }
- /** 异步在外部系统注册用户(不阻塞主流程) */
- private void asyncRegisterInExternal(User user, String miniProgramOpenId) {
- try {
- ExternalUserSyncService.ExternalUserInfo extInfo =
- new ExternalUserSyncService.ExternalUserInfo();
- extInfo.setMiniProgramOpenId(miniProgramOpenId);
- extInfo.setUnionId(user.getWechatUnionId());
- extInfo.setPhone(user.getPhone());
- extInfo.setNickname(user.getNickname());
- extInfo.setAvatar(user.getAvatar());
- String extUserId = externalUserSyncService.registerUser(extInfo);
- if (extUserId != null) {
- user.setExternalUserId(extUserId);
- user.setExternalSyncTime(LocalDateTime.now());
- userMapper.updateById(user);
- log.info("外部系统注册成功: userId={}, extUserId={}", user.getId(), extUserId);
- }
- } catch (Exception e) {
- log.warn("外部系统注册失败(不阻断主流程): userId={}", user.getId(), e);
- }
- }
- /**
- * 记录用户登录日志
- */
- private void recordLoginLog(Long userId, String loginSource, String ip) {
- if (userId == null || loginSource == null) {
- return;
- }
- try {
- LoginLog logEntry = LoginLog.builder()
- .userId(userId)
- .loginSource(loginSource)
- .loginIp(ip)
- .loginTime(LocalDateTime.now())
- .build();
- loginLogMapper.insert(logEntry);
- } catch (Exception e) {
- log.warn("记录登录日志失败(不阻断登录): userId={}, source={}", userId, loginSource, e);
- }
- }
- }
|