diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index 4d66c7a..7571053 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -8,7 +8,7 @@ import cn.novalon.gym.manage.member.service.MemberService; import cn.novalon.gym.manage.member.service.WechatAuthService; import cn.novalon.gym.manage.member.service.WechatOfficialService; import cn.novalon.gym.manage.member.util.AesUtil; -import cn.novalon.gym.manage.member.util.AuthUtil; +import cn.novalon.gym.manage.sys.util.AuthUtil; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -42,7 +42,7 @@ public class MemberHandler { * 获取会员信息 * * GET /api/member/info - * Header: X-Member-Id: 123 + * header: { "Authorization": "Bearer xxx" } */ public Mono getMemberInfo(ServerRequest request) { @@ -51,18 +51,16 @@ public class MemberHandler { log.info("获取会员信息, memberId: {}", memberId); return memberService.getMemberInfo(memberId) - .flatMap(info -> { - return ServerResponse.ok() + .flatMap(info -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) - .bodyValue(info); - }); + .bodyValue(info)); } /** * 更新会员信息 * * PUT /api/member/info - * Header: X-Member-Id: 123 + * header: { "Authorization": "Bearer xxx" } * Body: { * "nickname": "新昵称", * "gender": 1, @@ -86,9 +84,8 @@ public class MemberHandler { /** * 绑定手机号(微信小程序) - * + * header: { "Authorization": "Bearer xxx" } * POST /api/member/phone/bind?code=PHONE_CODE - * Header: X-Member-Id: 123 */ public Mono bindPhone(ServerRequest request) { @@ -110,7 +107,6 @@ public class MemberHandler { * 查询服务号关注状态 * * GET /api/member/subscribe/status - * Header: X-Member-Id: 123 * */ public Mono checkSubscribeStatus(ServerRequest request) { @@ -131,6 +127,7 @@ public class MemberHandler { * 管理员更新手机号 * * POST /api/admin/member/123/phone + * header: { "Authorization": "Bearer xxx" } * Body: { "phone": "13800138000" } * */ @@ -180,6 +177,8 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0) throw new IllegalArgumentException("会员ID格式错误"); + log.info("前台查看会员信息, adminId: {}, memberId: {}", adminId, memberId); + // TODO 多表查询:会员信息、团课信息、会员卡信息 return ServerResponse.ok() @@ -202,6 +201,8 @@ public class MemberHandler { long memberId = NumberUtils.toLong(memberIdStr, 0L); if(memberId <= 0L) throw new IllegalArgumentException("会员ID格式错误"); + log.info("前台编辑会员信息, adminId: {}, memberId: {}", adminId, memberId); + // TODO 多表查询:会员信息、团课信息、会员卡信息 return ServerResponse.ok() @@ -224,6 +225,9 @@ public class MemberHandler { int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + log.info("前台搜索会员列表, adminId: {}, keyword: {}, filter: {}, pageNum: {}, pageSize: {}", + adminId, keyword, filter, pageNum, pageSize); + return memberService.searchMember(new SearchMemberDto(keyword, filter, pageNum, pageSize)) .map(member -> { // 解密手机号 @@ -258,6 +262,8 @@ public class MemberHandler { int pageNum = NumberUtils.toInt(request.queryParam("pageNum").orElse("1"), 1); int pageSize = NumberUtils.toInt(request.queryParam("pageSize").orElse("10"), 10); + log.info("前台查看会员列表, adminId: {}, pageNum: {}, pageSize: {}", adminId, pageNum, pageSize); + return memberService.findAll(pageNum, pageSize) .map(member -> { // 解密手机号 diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index cfd1517..d1b6c44 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -1,5 +1,9 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.SearchMemberDto; import cn.novalon.gym.manage.member.dto.UpdateMemberInfoDto; @@ -14,13 +18,10 @@ import cn.novalon.gym.manage.member.vo.MemberInfoVO; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +29,7 @@ import java.time.LocalDateTime; /** * 会员服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -48,28 +49,22 @@ public class MemberServiceImpl implements MemberService { public void init() { this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); } - - @Value("${wechat.aes.secret-key:}") - private String aesSecretKey; - - @Value("${wechat.aes.iv:}") - private String aesIv; @Override public Mono getMemberInfo(Long memberId) { return memberRepository.findById(memberId) - .map(this::buildMemberInfoResponse); + .map(this::buildMemberInfoResponse) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); } @Override public Mono updateMemberInfo(Long memberId, UpdateMemberInfoDto updateDto) { log.info("会员更新个人信息, memberId: {}", memberId); - + return memberRepository.findById(memberId) - .switchIfEmpty(Mono.defer(() -> { - log.error("会员不存在: memberId={}", memberId); - return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); - })) .flatMap(member -> { if (updateDto.getNickname() != null) { member.setNickname(updateDto.getNickname()); @@ -86,17 +81,20 @@ public class MemberServiceImpl implements MemberService { if (updateDto.getAddress() != null) { member.setAddress(updateDto.getAddress()); } - + return memberRepository.save(member); }) .doOnSuccess(memberSyncer::sync) .map(savedMember -> { log.info("会员信息更新成功, memberId: {}", savedMember.getId()); return buildMemberInfoResponse(savedMember); - }); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); } - // 会员信息响应 private MemberInfoVO buildMemberInfoResponse(Member member) { String phone = member.getPhone(); String maskedPhone = phone != null ? phone.replace(phone.substring(3, 7), "****") : null; @@ -116,34 +114,27 @@ public class MemberServiceImpl implements MemberService { @Override public Mono adminUpdatePhone(Long memberId, String phone) { log.info("管理端录入手机号, memberId: {}, phone: {}", memberId, phone); - + String encryptedPhone; try { - encryptedPhone = AesUtil.encrypt(phone, aesSecretKey, aesIv); + String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); + String iv = wechatProperties.getPhoneEncryption().getIv(); + encryptedPhone = AesUtil.encrypt(phone, secretKey, iv); log.info("手机号加密成功"); } catch (Exception e) { log.error("手机号加密失败", e); - return Mono.error(new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "手机号加密失败: " + e.getMessage() - )); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败: " + e.getMessage()); } - + return memberRepository.findByPhone(encryptedPhone) .flatMap(existingMember -> { if (existingMember.getId().equals(memberId)) { log.warn("手机号已是当前用户的: memberId={}", memberId); - return Mono.error(new ResponseStatusException( - HttpStatus.CONFLICT, - "重复绑定" - )); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "重复绑定"); } else { - log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", + log.warn("手机号已被其他用户绑定: memberId={}, existingMemberId={}", memberId, existingMember.getId()); - return Mono.error(new ResponseStatusException( - HttpStatus.CONFLICT, - "该手机号已被其他会员绑定" - )); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "该手机号已被其他会员绑定"); } }) .switchIfEmpty(Mono.defer(() -> { @@ -154,24 +145,27 @@ public class MemberServiceImpl implements MemberService { @Override public Flux searchMember(SearchMemberDto searchMemberDto) { + log.info("搜索会员, searchValue: {}, filter: {}, pageNum: {}, pageSize: {}", + searchMemberDto.getSearchValue(), + searchMemberDto.getFilter(), + searchMemberDto.getPageNum(), + searchMemberDto.getPageSize()); String searchValue = searchMemberDto.getSearchValue(); - // 1. 处理手机号加密 if(searchValue != null && searchValue.matches("^1[3-9]\\d{9}$")){ + log.debug("搜索值为手机号格式,进行加密处理"); String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); searchValue = AesUtil.encrypt(searchValue,secretKey,iv); } - // 2. 分页参数 Pageable pageable = PageRequest.of( searchMemberDto.getPageNum() - 1, searchMemberDto.getPageSize(), Sort.by(Sort.Direction.DESC, "update_at") ); - // 3. 调用 Repository 查询 return memberESRepository.findByMemberNoOrPhoneOrNicknameContainingAndGender( searchValue, searchValue, @@ -183,6 +177,8 @@ public class MemberServiceImpl implements MemberService { @Override public Flux findAll(Integer pageNum, Integer pageSize) { + log.info("查询所有会员列表, pageNum: {}, pageSize: {}", pageNum, pageSize); + Pageable pageable = PageRequest.of( pageNum - 1, pageSize @@ -191,13 +187,12 @@ public class MemberServiceImpl implements MemberService { return memberRepository.findAllBy(pageable); } - // 更新会员手机号 private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { member.setPhone(encryptedPhone); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .map(savedMember -> { @@ -205,9 +200,9 @@ public class MemberServiceImpl implements MemberService { return true; }); }) - .switchIfEmpty(Mono.defer(() -> { + .switchIfEmpty(Mono.error(() -> { log.error("会员不存在: memberId={}", memberId); - return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "会员不存在")); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } } diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java index 8b818f1..f760ae4 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.service.WechatApiService; import lombok.RequiredArgsConstructor; @@ -17,7 +19,7 @@ import java.util.Map; /** * 微信API服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -28,33 +30,22 @@ import java.util.Map; public class WechatApiServiceImpl implements WechatApiService { private final WechatProperties wechatProperties; - - /** - * WebClient实例 - 用于发送HTTP请求 - * 最大内存大小为10MB - */ + private final WebClient webClient = WebClient.builder() .baseUrl("https://api.weixin.qq.com") .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) .build(); - /** - * 小程序 - * - * @param code 小程序登录的code - * @return Mono> session_keyopenidunionid响应格式 - */ @Override public Mono> jsCode2Session(String code) { log.info("微信jsCode2Session API"); - log.info("信息 - AppID: {}, AppSecret {}", + log.info("信息 - AppID: {}, AppSecret {}", wechatProperties.getMiniapp().getAppId(), - wechatProperties.getMiniapp().getAppSecret() != null ? + wechatProperties.getMiniapp().getAppSecret() != null ? wechatProperties.getMiniapp().getAppSecret().substring(0, Math.min(4, wechatProperties.getMiniapp().getAppSecret().length())) + "***" : "null"); log.info(" - code: {}", code); - + return webClient.get() - // 构建URI .uri(uriBuilder -> uriBuilder .path("/sns/jscode2session") .queryParam("appid", wechatProperties.getMiniapp().getAppId()) @@ -62,44 +53,37 @@ public class WechatApiServiceImpl implements WechatApiService { .queryParam("js_code", code) .queryParam("grant_type", "authorization_code") .build()) - // 获取响应 .retrieve() .bodyToMono(String.class) - // 处理响应 .map(responseBody -> { log.info("微信API响应: {}", responseBody); - - // 解析JSON响应 + Map response; try { - // 使用Jackson解析JSON响应 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); response = mapper.readValue(responseBody, Map.class); } catch (Exception e) { log.error("微信API响应失败", e); - throw new RuntimeException("微信API响应失败: " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应失败: " + e.getMessage()); } - + Map result = new HashMap<>(); - - // 检查错误码 + if (response.containsKey("errcode")) { Integer errcode = (Integer) response.get("errcode"); String errmsg = (String) response.get("errmsg"); log.error("微信API失败, errcode: {}, errmsg: {}", errcode, errmsg); - throw new RuntimeException("微信API失败 [" + errcode + "]: " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API失败 [" + errcode + "]: " + errmsg); } - - // 获取session_keyopenidunionid + result.put("session_key", (String) response.get("session_key")); result.put("openid", (String) response.get("openid")); result.put("unionid", (String) response.get("unionid")); - - log.info("微信API响应成功, openid: {}, unionid: {}", + + log.info("微信API响应成功, openid: {}, unionid: {}", result.get("openid"), result.get("unionid")); return result; }) - // 异常处理 .onErrorResume(e -> { log.error("微信API响应异常 - URL: https://api.weixin.qq.com/sns/jscode2session"); log.error("异常: {}", e.getClass().getName()); @@ -107,26 +91,23 @@ public class WechatApiServiceImpl implements WechatApiService { if (e.getCause() != null) { log.error("异常原因: {}", e.getCause().getMessage()); } - return Mono.error(new RuntimeException("微信API响应异常 " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "微信API响应异常 " + e.getMessage()); }); } - /** - * 获取手机号 - * - * @param code 手机号或获取手机号的code - * @return Mono 手机号 - */ @Override public Mono getPhoneNumber(String code) { log.debug("微信getPhoneNumber API, code: {}", code); - + return getAccessToken("miniapp") .flatMap(accessToken -> { Map requestBody = new HashMap<>(); requestBody.put("code", code); - + return webClient.post() .uri(uriBuilder -> uriBuilder .path("/wxa/business/getuserphonenumber") @@ -137,43 +118,35 @@ public class WechatApiServiceImpl implements WechatApiService { .retrieve() .bodyToMono(Map.class) .map(response -> { - if (response.containsKey("errcode") && + if (response.containsKey("errcode") && (Integer) response.get("errcode") == 0) { - - Map phoneInfo = + + Map phoneInfo = (Map) response.get("phone_info"); String phoneNumber = (String) phoneInfo.get("purePhoneNumber"); - + log.info("获取手机号成功{}", phoneNumber); return phoneNumber; } else { String errmsg = (String) response.get("errmsg"); log.error("获取手机号失败 {}", errmsg); - throw new RuntimeException("获取手机号失败 " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + errmsg); } }); }) - // .onErrorResume(e -> { log.error("获取手机号失败", e); - return Mono.error(new RuntimeException("获取手机号失败 " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取手机号失败 " + e.getMessage()); }); } - /** - * 获取Access Token - * - * @param appType 应用类型 - * @return Mono access_token - */ @Override public Mono getAccessToken(String appType) { log.debug("获取access_token, appType: {}", appType); - - // TODO: 实现缓存逻辑 - // 使用 Caffeine 或 Redis 缓存 - // 目前直接调用微信API - + String appId, appSecret; if ("miniapp".equals(appType)) { appId = wechatProperties.getMiniapp().getAppId(); @@ -182,7 +155,7 @@ public class WechatApiServiceImpl implements WechatApiService { appId = wechatProperties.getMp().getAppId(); appSecret = wechatProperties.getMp().getAppSecret(); } - + return webClient.get() .uri(uriBuilder -> uriBuilder .path("/cgi-bin/token") @@ -197,46 +170,34 @@ public class WechatApiServiceImpl implements WechatApiService { String accessToken = (String) response.get("access_token"); Integer expiresIn = (Integer) response.get("expires_in"); log.info("获取access_token成功, expires_in: {}s", expiresIn); - - // TODO: 加入缓存 - // cache.put("wechat:token:" + appType, accessToken, expiresIn - 200, TimeUnit.SECONDS); - return accessToken; } else { String errmsg = (String) response.get("errmsg"); log.error("获取access_token失败: {}", errmsg); - throw new RuntimeException("获取access_token失败: " + errmsg); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "获取access_token失败: " + errmsg); } }); } - /** - * 验证微信消息签名 - * - * @param signature 微信消息签名 - * @param timestamp 创建时间戳 - * @param nonce 随机字符串 - * @return boolean true-签名有效false-签名无效 - */ @Override public boolean checkSignature(String signature, String timestamp, String nonce) { - log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", + log.debug("验证微信消息签名, signature: {}, timestamp: {}, nonce: {}", signature, timestamp, nonce); - + try { String token = wechatProperties.getMp().getToken(); - + String[] arr = new String[]{token, timestamp, nonce}; Arrays.sort(arr); - + StringBuilder content = new StringBuilder(); for (String s : arr) { content.append(s); } - + MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] digest = md.digest(content.toString().getBytes(StandardCharsets.UTF_8)); - + StringBuilder hexString = new StringBuilder(); for (byte b : digest) { String hex = Integer.toHexString(0xff & b); @@ -245,12 +206,12 @@ public class WechatApiServiceImpl implements WechatApiService { } hexString.append(hex); } - + String calculatedSignature = hexString.toString(); - + boolean isValid = calculatedSignature.equals(signature); log.debug("验证微信消息签名结果: {}", isValid ? "通过" : "失败"); - + return isValid; } catch (Exception e) { log.error("验证微信消息签名异常", e); diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java index 5d7904f..5e7f299 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java @@ -1,5 +1,9 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.exception.ConflictException; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.NotFoundException; +import cn.novalon.gym.manage.common.exception.SystemException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.WechatLoginDto; import cn.novalon.gym.manage.member.entity.Member; @@ -26,7 +30,7 @@ import java.util.List; /** * 微信认证服务实现 - * + * * @author 付嘉 * @date 2026-05-01 */ @@ -52,34 +56,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { } - /** - * 小程序登录 - 通过微信 code 完成登录 - * - * @param request 微信登录请求 - * @return Mono 微信登录响应 - */ @Override public Mono miniappLogin(WechatLoginDto request) { log.info("开始小程序登录"); - + return wechatApiService.jsCode2Session(request.getCode()) .flatMap(sessionData -> { String openid = sessionData.get("openid"); String unionId = sessionData.get("unionid"); String sessionKey = sessionData.get("session_key"); - + log.info("微信 API 返回: openid={}, unionid={}", openid, unionId); - + if (unionId != null && !unionId.isEmpty()) { return memberRepository.findByUnionId(unionId) .flatMap(member -> { log.info("找到会员, memberId: {}", member.getId()); - + if (member.getMiniappOpenId() == null || member.getMiniappOpenId().isEmpty()) { log.info("用户已有 UnionID,补充小程序 OpenID, memberId: {}", member.getId()); member.setMiniappOpenId(openid); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -89,7 +87,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { } else { log.info("老用户登录,更新最后登录时间, memberId: {}", member.getId()); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -105,7 +103,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { log.info("找到会员, memberId: {}", member.getId()); member.setUnionId(unionId); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -124,7 +122,7 @@ public class WechatAuthServiceImpl implements WechatAuthService { .flatMap(member -> { log.info("找到会员, memberId: {}", member.getId()); member.setLastLoginAt(LocalDateTime.now()); - + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { @@ -140,32 +138,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { }) .onErrorResume(e -> { log.error("小程序登录失败", e); - return Mono.error(new RuntimeException("登录失败: " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "登录失败: " + e.getMessage())); }); } - /** - * 绑定手机号 - 通过微信 code 获取并绑定手机号 - * - * @param memberId 会员 ID - * @param code 微信手机号 code - * @return Mono - */ @Override public Mono bindPhone(Long memberId, String code) { log.info("开始绑定手机号, memberId: {}", memberId); - + return wechatApiService.getPhoneNumber(code) .flatMap(phoneNumber -> { log.info("获取手机号: {}", phoneNumber); String encryptedPhone = encryptPhone(phoneNumber); - + return memberRepository.findByPhone(encryptedPhone) .flatMap(existingMember -> { if (!existingMember.getId().equals(memberId)) { log.warn("手机号已被其他会员绑定, currentMemberId={}, existingMemberId={}", memberId, existingMember.getId()); - return Mono.error(new RuntimeException("手机号已被其他会员绑定")); + throw new ConflictException(ErrorCode.CONFLICT_DUPLICATE_USER, "手机号已被其他会员绑定"); } else { log.info("更新会员手机号, memberId: {}", memberId); return updateMemberPhone(memberId, encryptedPhone); @@ -178,17 +172,13 @@ public class WechatAuthServiceImpl implements WechatAuthService { }) .onErrorResume(e -> { log.error("绑定手机号失败", e); - return Mono.error(new RuntimeException("绑定失败: " + e.getMessage())); + if (e instanceof SystemException) { + return Mono.error(e); + } + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "绑定失败: " + e.getMessage())); }); } - - /** - * 更新会员手机号 - * - * @param memberId 会员 ID - * @param encryptedPhone 加密后的手机号(Base64 编码) - * @return Mono 是否更新成功 - */ + private Mono updateMemberPhone(Long memberId, String encryptedPhone) { return memberRepository.findById(memberId) .flatMap(member -> { @@ -201,95 +191,68 @@ public class WechatAuthServiceImpl implements WechatAuthService { return true; }); }) - .switchIfEmpty(Mono.defer(() -> { + .switchIfEmpty(Mono.error(() -> { log.error("会员不存在, memberId={}", memberId); - return Mono.error(new RuntimeException("会员不存在")); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); })); } - - /** - * 手机号加密 - * - * @param phoneNumber 明文手机号 - * @return 加密后的手机号(Base64 编码) - */ + private String encryptPhone(String phoneNumber) { try { String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); - + String encryptedPhone = AesUtil.encrypt(phoneNumber, secretKey, iv); - + log.debug("手机号加密成功"); return encryptedPhone; } catch (Exception e) { log.error("手机号加密失败", e); - throw new RuntimeException("手机号加密失败 " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败 " + e.getMessage()); } } - /** - * AES 解密手机号 - * - * @param encryptedPhone 加密后的手机号(Base64 编码) - * @return 明文手机号 - */ public String decryptPhone(String encryptedPhone) { try { String secretKey = wechatProperties.getPhoneEncryption().getSecretKey(); String iv = wechatProperties.getPhoneEncryption().getIv(); - + String phoneNumber = AesUtil.decrypt(encryptedPhone, secretKey, iv); - + log.debug("手机号解密成功"); return phoneNumber; } catch (Exception e) { log.error("手机号解密失败", e); - throw new RuntimeException("手机号解密失败 " + e.getMessage()); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号解密失败 " + e.getMessage()); } } - /** - * 创建新会员(首次登录) - * - * @param unionId 微信 UnionID - * @param openid 小程序 OpenID - * @param sessionKey 会话密钥 - * @param phoneCode 手机号 code(可选,前端调用 wx.getPhoneNumber()) - * @return Mono 登录响应 - */ private Mono createNewMember(String unionId, String openid, String sessionKey, String phoneCode) { log.info("开始创建新会员, unionId: {}, openid: {}", unionId, openid); - - // Step 1: 生成会员号 + String memberNo = MemberNoGenerator.generate(); log.info("生成会员号: {}", memberNo); - - // Step 2: 构建 Member 实体(仅保存标识信息) + Member member = Member.builder() - .memberNo(memberNo) // 小程序注册时自动生成会员号 - .unionId(unionId) // 存储 UnionID 用于统一身份 - .miniappOpenId(openid) // 存储小程序 OpenID - .lastLoginAt(LocalDateTime.now()) // 记录首次登录时间 + .memberNo(memberNo) + .unionId(unionId) + .miniappOpenId(openid) + .lastLoginAt(LocalDateTime.now()) .build(); - + log.info("用户未注册,创建新会员(仅保存标识信息)"); - - // Step 3: 保存 Member + return memberRepository.save(member) .doOnSuccess(memberSyncer::sync) .flatMap(savedMember -> { log.info("保存 Member 成功, id: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); - // Step 4: 如果有 phoneCode,尝试获取手机号 if (phoneCode != null && !phoneCode.isEmpty()) { log.info("检测到 phoneCode,尝试获取手机号"); return wechatPhoneUtil.getPhoneNumber(phoneCode) .flatMap(phoneNumber -> { if (phoneNumber != null && !phoneNumber.isEmpty()) { log.info("获取到手机号: {}", phoneNumber); - // 加密手机号 String encryptedPhone = encryptPhone(phoneNumber); - // 为新会员绑定手机号 savedMember.setPhone(encryptedPhone); return memberRepository.save(savedMember) .doOnSuccess(memberSyncer::sync) @@ -303,40 +266,28 @@ public class WechatAuthServiceImpl implements WechatAuthService { } }); } else { - // 没有 phoneCode,直接返回 return Mono.just(buildLoginResponse(savedMember, true, sessionKey)); } }); } - /** - * 构建登录响应 - 封装返回数据(前端调用) - * - * @param member 会员实体 - * @param isNewUser 是否为新用户 - * @param sessionKey 会话密钥(后续可用于解密) - * @return WechatLoginVO 登录响应 - */ private WechatLoginVO buildLoginResponse(Member member, boolean isNewUser, String sessionKey) { log.debug("构建登录响应, memberId: {}, isNewUser: {}", member.getId(), isNewUser); - + boolean needCompleteInfo = member.getNickname() == null || member.getPhone() == null; if (needCompleteInfo) { - log.info("用户需要补全信息: nickname={}, phone={}", + log.info("用户需要补全信息: nickname={}, phone={}", member.getNickname() != null ? "已有" : "未设置", member.getPhone() != null ? "已绑定" : "未绑定"); } - - // 统一使用 JwtTokenProvider 生成 Token - // 使用空的角色列表,会员不需要角色 + List roles = new ArrayList<>(); String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles); - + log.info("JWT Token 生成成功, memberId: {}", member.getId()); - - // 使用 JwtTokenProvider 默认过期时间(86400 秒) + int expiresIn = 86400; - + return WechatLoginVO.builder() .memberId(member.getId()) .accessToken(accessToken) diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java index 018c200..b5b863a 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java @@ -234,10 +234,14 @@ public class WechatOfficialServiceImpl implements WechatOfficialService { */ @Override public Mono checkSubscribeStatus(Long memberId) { + log.info("查询用户关注状态, memberId: {}", memberId); + return memberRepository.findById(memberId) .map(member -> { Boolean subscribed = member.getSubscribed(); - return subscribed != null && subscribed; + boolean result = subscribed != null && subscribed; + log.info("查询用户关注状态结果, memberId: {}, subscribed: {}", memberId, result); + return result; }) .defaultIfEmpty(false); } diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index 6947791..406ea19 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -1,22 +1,23 @@ package cn.novalon.gym.manage.app.config; -import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; -import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; -import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; -import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; -import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; -import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; -import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; -import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; -import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; -import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; -import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; -import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; +import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; +import cn.novalon.gym.manage.file.handler.SysFileHandler; +import cn.novalon.gym.manage.member.handler.MemberHandler; +import cn.novalon.gym.manage.member.handler.WechatAuthHandler; import cn.novalon.gym.manage.notify.handler.SysNoticeHandler; import cn.novalon.gym.manage.notify.handler.SysUserMessageHandler; -import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.member.handler.WechatAuthHandler; -import cn.novalon.gym.manage.member.handler.MemberHandler; +import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; +import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; +import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; +import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; +import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; +import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; +import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; +import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; +import cn.novalon.gym.manage.sys.handler.permission.SysPermissionHandler; +import cn.novalon.gym.manage.sys.handler.role.SysRoleHandler; +import cn.novalon.gym.manage.sys.handler.stats.StatsHandler; +import cn.novalon.gym.manage.sys.handler.user.SysUserHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; @@ -55,7 +56,8 @@ public class SystemRouter { SysPermissionHandler permissionHandler, MemberHandler memberHandler, WechatAuthHandler wechatAuthHandler, - PasswordDiagnosticHandler passwordDiagnosticHandler) { + PasswordDiagnosticHandler passwordDiagnosticHandler, + CheckInHandler checkInHandler) { return route() // ========== 诊断路由 ========== @@ -214,6 +216,10 @@ public class SystemRouter { .PUT("/api/admin/member/{id}", memberHandler::adminUpdateMemberInfo) .GET("/api/admin/members", memberHandler::searchMembers) .GET("/api/admin/members/all", memberHandler::getAllMembers) + + // ========== 签到模块路由 ========== + .POST("/api/checkin", checkInHandler::checkIn) + .GET("/api/QRCode", checkInHandler::getQRCode) .build(); } } diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java new file mode 100644 index 0000000..14c5531 --- /dev/null +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/util/AuthUtil.java @@ -0,0 +1,32 @@ +package cn.novalon.gym.manage.sys.util; + +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ResponseStatusException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthUtil { + + private final JwtTokenProvider jwtTokenProvider; + + public String extractToken(ServerRequest request) { + String authorization = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); + if (authorization == null || !authorization.startsWith("Bearer ")) return null; + return authorization.substring(7); + } + + public Long getMemberIdOrThrow(ServerRequest request) { + String token = extractToken(request); + if (token == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "缺少 Token"); + if (!jwtTokenProvider.validateToken(token)) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token 无效或已过期"); + if (jwtTokenProvider.getUserIdFromToken(token) <= 0L) throw new IllegalArgumentException("ID无效"); + return jwtTokenProvider.getUserIdFromToken(token); + } +} \ No newline at end of file