From 2ffd1aa7d6c9d317ac4726f20201e8a609d860a3 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 23 Jun 2026 23:40:49 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/novalon/gym/manage/sys/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java index 7cec5cf..eb22166 100644 --- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.sys.config; +package cn.novalon.gym.manage.sys.config; import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter; import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter; -- 2.52.0 From 91b005975abcc009e2ae97923ded51f6b8037f8a Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 2 Jun 2026 09:56:37 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E7=AD=BE=E5=88=B0=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/CheckServiceImpl.java | 35 +------------------ .../checkIn/websocket/MyWebSocketHandler.java | 3 +- .../gym/manage/checkin/CheckInModuleTest.java | 1 - .../common/constant/RedisKeyConstants.java | 3 ++ 4 files changed, 5 insertions(+), 37 deletions(-) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java index e041842..4d9285f 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java @@ -84,7 +84,6 @@ public class CheckServiceImpl implements ICheckInService { public Mono checkIn(Long memberId, String qrContent) { String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); - // 先检查当天是否已经签到(从数据库查询,作为重复签到的额外保障) return checkTodayAlreadySignedIn(memberId) .flatMap(existingRecord -> { String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER); @@ -126,11 +125,6 @@ public class CheckServiceImpl implements ICheckInService { }); } - /** - * 检查会员当天是否已经签到 - * @param memberId 会员ID - * @return 如果已签到返回签到记录,否则返回空 - */ private Mono checkTodayAlreadySignedIn(Long memberId) { LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX); @@ -138,13 +132,9 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay); } - /** - * 处理签到逻辑 - */ private Mono processCheckIn(Long memberId, Long memberCardRecordId, Map redisMap, String qrContent) { LocalDateTime now = LocalDateTime.now(); - // 发送实时进度通知 MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡..."); return memberCardRecordRepository.findById(memberCardRecordId) @@ -163,10 +153,8 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡已过期")); } - // 发送实时进度通知 MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); - // 检查是否有需要签到的团课预约 return validateBooking(memberId, now) .then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId()) .switchIfEmpty(Mono.defer(() -> { @@ -174,7 +162,6 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡类型不存在")); })) .flatMap(card -> { - // 发送实时进度通知 MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数..."); return deductCardUsage(cardRecord, card) @@ -195,9 +182,6 @@ public class CheckServiceImpl implements ICheckInService { }); } - /** - * 验证预约信息 - */ private Mono validateBooking(Long memberId, LocalDateTime now) { return groupCourseBookingService.getBookingsByMemberId(memberId) .filter(booking -> { @@ -229,9 +213,6 @@ public class CheckServiceImpl implements ICheckInService { }); } - /** - * 扣减会员卡使用次数/金额 - */ private Mono deductCardUsage(MemberCardRecord record, MemberCard card) { MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); LocalDateTime now = LocalDateTime.now(); @@ -267,9 +248,6 @@ public class CheckServiceImpl implements ICheckInService { } } - /** - * 保存签到记录 - */ private Mono saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) { SignInRecord record = SignInRecord.builder() .memberId(memberId) @@ -284,9 +262,6 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.save(record).then(); } - /** - * 构建成功响应 - */ private String buildSuccessResponse(LocalDateTime dateTime) { Map res = new HashMap<>(); res.put("message", "签到成功"); @@ -294,9 +269,6 @@ public class CheckServiceImpl implements ICheckInService { return JSONUtil.toJsonStr(res); } - /** - * 查找会员的有效会员卡(优先选择有效期最早到期的) - */ private Mono findValidMemberCard(Long memberId) { return memberCardRecordRepository.findActiveCardsByMemberId(memberId) .filter(record -> { @@ -314,8 +286,6 @@ public class CheckServiceImpl implements ICheckInService { .next(); } - // ==================== 签到记录管理功能 ==================== - @Override public Flux getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) { LocalDateTime start = startTime.atStartOfDay(); @@ -413,9 +383,6 @@ public class CheckServiceImpl implements ICheckInService { ); } - /** - * 转换实体到VO - */ private SignInRecordVO convertToVO(SignInRecord record) { SignInRecordVO vo = new SignInRecordVO(); vo.setId(record.getId()); @@ -436,4 +403,4 @@ public class CheckServiceImpl implements ICheckInService { if (now.isAfter(endOfDay)) return 1; return ChronoUnit.SECONDS.between(now, endOfDay); } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java index f3bd900..7f705c8 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java @@ -77,7 +77,7 @@ public class MyWebSocketHandler implements WebSocketHandler { .doOnError(e -> { log.error("接收消息出错,sessionId={}", sessionId, e); }) - .subscribe(); // 必须订阅,否则不会执行 + .subscribe(); // 发送流给客户端 return session.send(sink.asFlux().map(session::textMessage)) @@ -100,7 +100,6 @@ public class MyWebSocketHandler implements WebSocketHandler { * @return 是否发送成功 */ public static boolean sendMessageToClient(String qrContent, String message) { - // 先清理超时连接 cleanupTimeoutConnections(); Sinks.Many sink = qrContentToSink.get(qrContent); diff --git a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java index 27fb944..d73ab47 100644 --- a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java +++ b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java @@ -25,7 +25,6 @@ import reactor.test.StepVerifier; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.HashMap; import java.util.Map; diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java index 46d7c61..b32601e 100644 --- a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java @@ -61,6 +61,7 @@ public final class RedisKeyConstants { * appType: miniapp(小程序), mp(公众号) */ public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:"; +<<<<<<< HEAD // ==================== 认证模块 ==================== @@ -70,4 +71,6 @@ public final class RedisKeyConstants { * 用途:存储登录/注册等场景的短信验证码 */ public static final String SMS_CODE = "sms:code:"; +======= +>>>>>>> 08cf82a (签到模块) } \ No newline at end of file -- 2.52.0 From 8cb7bc6ae8fb0fac8f16440dd9a861de4eba8c56 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 9 Jun 2026 09:09:12 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AD=BE?= =?UTF-8?q?=E5=88=B0=E6=A8=A1=E5=9D=97=E5=92=8C=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增gym-checkIn签到模块 - 创建sign_in_record签到记录表(V13迁移脚本) - 添加签到相关路由配置 - 更新应用配置以支持签到模块 - 添加Redis密钥常量 --- .../manage/checkIn/entity/SignInRecord.java | 4 + .../checkIn/service/ICheckInService.java | 4 + .../service/impl/CheckServiceImpl.java | 84 +++++++++++++++- .../checkIn/websocket/MyWebSocketHandler.java | 79 +++++++++++++++ .../gym/manage/checkin/CheckInModuleTest.java | 4 + .../V13__Create_SignInRecord_table.sql | 99 +++++++++++++++++++ 6 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java index 63cf034..59a9e0e 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java @@ -206,4 +206,8 @@ public class SignInRecord { public void restore() { this.isDelete = false; } +<<<<<<< HEAD } +======= +} +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java index e162423..e78174d 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java @@ -78,4 +78,8 @@ public interface ICheckInService { * @return 签到统计VO */ Mono getDailySignInStats(LocalDate date); +<<<<<<< HEAD } +======= +} +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java index 4d9285f..a0279ea 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java @@ -2,8 +2,11 @@ package cn.novalon.gym.manage.checkIn.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.json.JSONUtil; +<<<<<<< HEAD import cn.hutool.extra.qrcode.QrCodeUtil; import cn.hutool.extra.qrcode.QrConfig; +======= +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import cn.novalon.gym.manage.checkIn.config.QRCodeConfig; import cn.novalon.gym.manage.checkIn.constant.QRRedisKey; import cn.novalon.gym.manage.checkIn.entity.SignInRecord; @@ -21,6 +24,11 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository; +<<<<<<< HEAD +======= +import cn.hutool.extra.qrcode.QrCodeUtil; +import cn.hutool.extra.qrcode.QrConfig; +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -84,6 +92,10 @@ public class CheckServiceImpl implements ICheckInService { public Mono checkIn(Long memberId, String qrContent) { String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); +<<<<<<< HEAD +======= + // 先检查当天是否已经签到(从数据库查询,作为重复签到的额外保障) +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) return checkTodayAlreadySignedIn(memberId) .flatMap(existingRecord -> { String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER); @@ -125,6 +137,14 @@ public class CheckServiceImpl implements ICheckInService { }); } +<<<<<<< HEAD +======= + /** + * 检查会员当天是否已经签到 + * @param memberId 会员ID + * @return 如果已签到返回签到记录,否则返回空 + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono checkTodayAlreadySignedIn(Long memberId) { LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX); @@ -132,9 +152,19 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay); } +<<<<<<< HEAD private Mono processCheckIn(Long memberId, Long memberCardRecordId, Map redisMap, String qrContent) { LocalDateTime now = LocalDateTime.now(); +======= + /** + * 处理签到逻辑 + */ + private Mono processCheckIn(Long memberId, Long memberCardRecordId, Map redisMap, String qrContent) { + LocalDateTime now = LocalDateTime.now(); + + // 发送实时进度通知 +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡..."); return memberCardRecordRepository.findById(memberCardRecordId) @@ -153,8 +183,15 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡已过期")); } +<<<<<<< HEAD MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); +======= + // 发送实时进度通知 + MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); + + // 检查是否有需要签到的团课预约 +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) return validateBooking(memberId, now) .then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId()) .switchIfEmpty(Mono.defer(() -> { @@ -162,6 +199,10 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡类型不存在")); })) .flatMap(card -> { +<<<<<<< HEAD +======= + // 发送实时进度通知 +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数..."); return deductCardUsage(cardRecord, card) @@ -182,6 +223,12 @@ public class CheckServiceImpl implements ICheckInService { }); } +<<<<<<< HEAD +======= + /** + * 验证预约信息 + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono validateBooking(Long memberId, LocalDateTime now) { return groupCourseBookingService.getBookingsByMemberId(memberId) .filter(booking -> { @@ -213,6 +260,12 @@ public class CheckServiceImpl implements ICheckInService { }); } +<<<<<<< HEAD +======= + /** + * 扣减会员卡使用次数/金额 + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono deductCardUsage(MemberCardRecord record, MemberCard card) { MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); LocalDateTime now = LocalDateTime.now(); @@ -248,6 +301,12 @@ public class CheckServiceImpl implements ICheckInService { } } +<<<<<<< HEAD +======= + /** + * 保存签到记录 + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) { SignInRecord record = SignInRecord.builder() .memberId(memberId) @@ -262,6 +321,12 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.save(record).then(); } +<<<<<<< HEAD +======= + /** + * 构建成功响应 + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private String buildSuccessResponse(LocalDateTime dateTime) { Map res = new HashMap<>(); res.put("message", "签到成功"); @@ -269,6 +334,12 @@ public class CheckServiceImpl implements ICheckInService { return JSONUtil.toJsonStr(res); } +<<<<<<< HEAD +======= + /** + * 查找会员的有效会员卡(优先选择有效期最早到期的) + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono findValidMemberCard(Long memberId) { return memberCardRecordRepository.findActiveCardsByMemberId(memberId) .filter(record -> { @@ -286,6 +357,11 @@ public class CheckServiceImpl implements ICheckInService { .next(); } +<<<<<<< HEAD +======= + // ==================== 签到记录管理功能 ==================== + +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) @Override public Flux getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) { LocalDateTime start = startTime.atStartOfDay(); @@ -383,6 +459,12 @@ public class CheckServiceImpl implements ICheckInService { ); } +<<<<<<< HEAD +======= + /** + * 转换实体到VO + */ +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private SignInRecordVO convertToVO(SignInRecord record) { SignInRecordVO vo = new SignInRecordVO(); vo.setId(record.getId()); @@ -403,4 +485,4 @@ public class CheckServiceImpl implements ICheckInService { if (now.isAfter(endOfDay)) return 1; return ChronoUnit.SECONDS.between(now, endOfDay); } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java index 7f705c8..d3a6a64 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java @@ -77,7 +77,11 @@ public class MyWebSocketHandler implements WebSocketHandler { .doOnError(e -> { log.error("接收消息出错,sessionId={}", sessionId, e); }) +<<<<<<< HEAD .subscribe(); +======= + .subscribe(); // 必须订阅,否则不会执行 +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) // 发送流给客户端 return session.send(sink.asFlux().map(session::textMessage)) @@ -100,6 +104,10 @@ public class MyWebSocketHandler implements WebSocketHandler { * @return 是否发送成功 */ public static boolean sendMessageToClient(String qrContent, String message) { +<<<<<<< HEAD +======= + // 先清理超时连接 +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) cleanupTimeoutConnections(); Sinks.Many sink = qrContentToSink.get(qrContent); @@ -129,6 +137,77 @@ public class MyWebSocketHandler implements WebSocketHandler { String progressMessage = buildMessage("PROGRESS", message); sendMessageToClient(qrContent, progressMessage); log.debug("发送进度消息: qrContent={}, step={}, message={}", qrContent, step, message); +<<<<<<< HEAD +======= + } + + /** + * 发送签到成功消息 + * + * @param qrContent 二维码内容 + * @param memberId 会员ID + * @param signInTime 签到时间 + */ + public static void sendSuccess(String qrContent, Long memberId, String signInTime) { + String successMessage = buildMessage("SUCCESS", "签到成功!欢迎光临\n会员ID: " + memberId + "\n签到时间: " + signInTime); + sendMessageToClient(qrContent, successMessage); + log.info("发送成功消息: qrContent={}, memberId={}", qrContent, memberId); + } + + /** + * 发送签到失败消息 + * + * @param qrContent 二维码内容 + * @param reason 失败原因 + */ + public static void sendFailure(String qrContent, String reason) { + String failureMessage = buildMessage("FAILURE", "签到失败:" + reason); + sendMessageToClient(qrContent, failureMessage); + log.warn("发送失败消息: qrContent={}, reason={}", qrContent, reason); + } + + /** + * 构建标准消息格式 + * + * @param type 消息类型 + * @param content 消息内容 + * @return 格式化后的消息字符串 + */ + private static String buildMessage(String type, String content) { + return JSONUtil.toJsonStr(Map.of( + "type", type, + "content", content, + "timestamp", System.currentTimeMillis() + )); + } + + /** + * 清理超时连接 + */ + private static void cleanupTimeoutConnections() { + LocalDateTime now = LocalDateTime.now(); + qrContentToCreateTime.entrySet().removeIf(entry -> { + LocalDateTime createTime = entry.getValue(); + long secondsDiff = java.time.Duration.between(createTime, now).getSeconds(); + if (secondsDiff > TIMEOUT_SECONDS) { + String qrContent = entry.getKey(); + qrContentToSink.remove(qrContent); + log.debug("清理超时连接: qrContent={}, 超时时间={}秒", qrContent, secondsDiff); + return true; + } + return false; + }); + } + + /** + * 获取当前连接数 + * + * @return 连接数 + */ + public static int getConnectionCount() { + cleanupTimeoutConnections(); + return qrContentToSink.size(); +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) } /** diff --git a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java index d73ab47..8f35681 100644 --- a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java +++ b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java @@ -25,6 +25,10 @@ import reactor.test.StepVerifier; import java.time.LocalDate; import java.time.LocalDateTime; +<<<<<<< HEAD +======= +import java.time.LocalTime; +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import java.util.HashMap; import java.util.Map; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql new file mode 100644 index 0000000..ca01bc4 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql @@ -0,0 +1,99 @@ +-- ============================================ +-- 签到记录表(sign_in_record) +-- ============================================ + +-- Step 1: 创建 sign_in_record 表 +CREATE TABLE IF NOT EXISTS sign_in_record ( + -- ========== 主键和基础字段 ========== + id BIGSERIAL PRIMARY KEY, -- 自增主键 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录更新时间 + + -- ========== 会员相关字段 ========== + member_id BIGINT NOT NULL, -- 会员ID,关联member_user表 + member_card_id BIGINT, -- 签到时使用的会员卡ID + + -- ========== 签到核心字段 ========== + sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间 + sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别 + sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败 + + -- ========== 验证和错误信息 ========== + verification_details TEXT, -- JSONB格式,存储会员卡验证时的快照数据 + fail_reason VARCHAR(500), -- 失败时的具体原因文案 + + -- ========== 操作人信息 ========== + operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL + operator_name VARCHAR(100), -- 操作人姓名冗余 + + -- ========== 设备和环境信息 ========== + device_info VARCHAR(200), -- 签到设备标识或型号 + ip_address VARCHAR(50), -- 客户端IP地址 + source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端 + + -- ========== 软删除字段 ========== + is_delete BOOLEAN DEFAULT FALSE -- 软删除标识:false-未删除,true-已删除 +); + +-- Step 2: 创建索引 +-- 会员ID索引(加速按会员查询签到记录) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id); + +-- 签到时间索引(加速按时间范围查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time); + +-- 签到状态索引(加速按状态筛选) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status); + +-- 会员卡ID索引(加速按会员卡查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id); + +-- 操作人ID索引(加速按操作人查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id); + +-- 签到来源索引(加速按来源统计) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source); + +-- 软删除索引(加速查询未删除的记录) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete); + +-- 复合索引:会员ID + 签到时间(加速会员签到历史查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time); + +-- 复合索引:签到状态 + 签到时间(加速统计数据查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time); + +-- 复合索引:签到日期(用于每日统计,使用表达式索引) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_date ON sign_in_record(DATE(sign_in_time)); + +-- Step 3: 添加外键约束(可选,根据业务需求决定) +-- 注意:如果member_user表使用了软删除,外键约束可能需要谨慎使用 +-- ALTER TABLE sign_in_record +-- ADD CONSTRAINT fk_sign_in_record_member +-- FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE SET NULL; + +-- Step 4: 添加注释 +COMMENT ON TABLE sign_in_record IS '会员到店签到记录表'; + +COMMENT ON COLUMN sign_in_record.id IS '自增主键'; +COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间'; +COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间'; + +COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member_user表'; +COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID'; + +COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间'; +COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别'; +COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败'; + +COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据(包含卡类型、剩余次数/金额、有效期等)'; +COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案'; + +COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL'; +COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余(避免关联查询)'; + +COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号'; +COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址'; +COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端'; + +COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除'; -- 2.52.0 From b291dacf3def9a04239032f4ea4c418d51d5a9f1 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 24 Jun 2026 00:43:53 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=A2=E5=A4=B1?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage/checkIn/entity/SignInRecord.java | 4 - .../checkIn/service/ICheckInService.java | 4 - .../service/impl/CheckServiceImpl.java | 49 ------------ .../checkIn/websocket/MyWebSocketHandler.java | 78 ------------------- .../gym/manage/checkin/CheckInModuleTest.java | 3 - .../common/constant/RedisKeyConstants.java | 3 - .../gym/manage/sys/config/SecurityConfig.java | 2 +- 7 files changed, 1 insertion(+), 142 deletions(-) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java index 59a9e0e..63cf034 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java @@ -206,8 +206,4 @@ public class SignInRecord { public void restore() { this.isDelete = false; } -<<<<<<< HEAD } -======= -} ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java index e78174d..e162423 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java @@ -78,8 +78,4 @@ public interface ICheckInService { * @return 签到统计VO */ Mono getDailySignInStats(LocalDate date); -<<<<<<< HEAD } -======= -} ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java index a0279ea..e041842 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java @@ -2,11 +2,8 @@ package cn.novalon.gym.manage.checkIn.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.json.JSONUtil; -<<<<<<< HEAD import cn.hutool.extra.qrcode.QrCodeUtil; import cn.hutool.extra.qrcode.QrConfig; -======= ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import cn.novalon.gym.manage.checkIn.config.QRCodeConfig; import cn.novalon.gym.manage.checkIn.constant.QRRedisKey; import cn.novalon.gym.manage.checkIn.entity.SignInRecord; @@ -24,11 +21,6 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.enums.MemberCardType; import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository; -<<<<<<< HEAD -======= -import cn.hutool.extra.qrcode.QrCodeUtil; -import cn.hutool.extra.qrcode.QrConfig; ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -92,10 +84,7 @@ public class CheckServiceImpl implements ICheckInService { public Mono checkIn(Long memberId, String qrContent) { String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now(); -<<<<<<< HEAD -======= // 先检查当天是否已经签到(从数据库查询,作为重复签到的额外保障) ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) return checkTodayAlreadySignedIn(memberId) .flatMap(existingRecord -> { String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER); @@ -137,14 +126,11 @@ public class CheckServiceImpl implements ICheckInService { }); } -<<<<<<< HEAD -======= /** * 检查会员当天是否已经签到 * @param memberId 会员ID * @return 如果已签到返回签到记录,否则返回空 */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono checkTodayAlreadySignedIn(Long memberId) { LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX); @@ -152,11 +138,6 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay); } -<<<<<<< HEAD - private Mono processCheckIn(Long memberId, Long memberCardRecordId, Map redisMap, String qrContent) { - LocalDateTime now = LocalDateTime.now(); - -======= /** * 处理签到逻辑 */ @@ -164,7 +145,6 @@ public class CheckServiceImpl implements ICheckInService { LocalDateTime now = LocalDateTime.now(); // 发送实时进度通知 ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡..."); return memberCardRecordRepository.findById(memberCardRecordId) @@ -183,15 +163,10 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡已过期")); } -<<<<<<< HEAD - MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); - -======= // 发送实时进度通知 MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息..."); // 检查是否有需要签到的团课预约 ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) return validateBooking(memberId, now) .then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId()) .switchIfEmpty(Mono.defer(() -> { @@ -199,10 +174,7 @@ public class CheckServiceImpl implements ICheckInService { return Mono.error(new RuntimeException("会员卡类型不存在")); })) .flatMap(card -> { -<<<<<<< HEAD -======= // 发送实时进度通知 ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数..."); return deductCardUsage(cardRecord, card) @@ -223,12 +195,9 @@ public class CheckServiceImpl implements ICheckInService { }); } -<<<<<<< HEAD -======= /** * 验证预约信息 */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono validateBooking(Long memberId, LocalDateTime now) { return groupCourseBookingService.getBookingsByMemberId(memberId) .filter(booking -> { @@ -260,12 +229,9 @@ public class CheckServiceImpl implements ICheckInService { }); } -<<<<<<< HEAD -======= /** * 扣减会员卡使用次数/金额 */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono deductCardUsage(MemberCardRecord record, MemberCard card) { MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType()); LocalDateTime now = LocalDateTime.now(); @@ -301,12 +267,9 @@ public class CheckServiceImpl implements ICheckInService { } } -<<<<<<< HEAD -======= /** * 保存签到记录 */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) { SignInRecord record = SignInRecord.builder() .memberId(memberId) @@ -321,12 +284,9 @@ public class CheckServiceImpl implements ICheckInService { return signInRecordRepository.save(record).then(); } -<<<<<<< HEAD -======= /** * 构建成功响应 */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private String buildSuccessResponse(LocalDateTime dateTime) { Map res = new HashMap<>(); res.put("message", "签到成功"); @@ -334,12 +294,9 @@ public class CheckServiceImpl implements ICheckInService { return JSONUtil.toJsonStr(res); } -<<<<<<< HEAD -======= /** * 查找会员的有效会员卡(优先选择有效期最早到期的) */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private Mono findValidMemberCard(Long memberId) { return memberCardRecordRepository.findActiveCardsByMemberId(memberId) .filter(record -> { @@ -357,11 +314,8 @@ public class CheckServiceImpl implements ICheckInService { .next(); } -<<<<<<< HEAD -======= // ==================== 签到记录管理功能 ==================== ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) @Override public Flux getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) { LocalDateTime start = startTime.atStartOfDay(); @@ -459,12 +413,9 @@ public class CheckServiceImpl implements ICheckInService { ); } -<<<<<<< HEAD -======= /** * 转换实体到VO */ ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) private SignInRecordVO convertToVO(SignInRecord record) { SignInRecordVO vo = new SignInRecordVO(); vo.setId(record.getId()); diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java index d3a6a64..f3bd900 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java @@ -77,11 +77,7 @@ public class MyWebSocketHandler implements WebSocketHandler { .doOnError(e -> { log.error("接收消息出错,sessionId={}", sessionId, e); }) -<<<<<<< HEAD - .subscribe(); -======= .subscribe(); // 必须订阅,否则不会执行 ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) // 发送流给客户端 return session.send(sink.asFlux().map(session::textMessage)) @@ -104,10 +100,7 @@ public class MyWebSocketHandler implements WebSocketHandler { * @return 是否发送成功 */ public static boolean sendMessageToClient(String qrContent, String message) { -<<<<<<< HEAD -======= // 先清理超时连接 ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) cleanupTimeoutConnections(); Sinks.Many sink = qrContentToSink.get(qrContent); @@ -137,77 +130,6 @@ public class MyWebSocketHandler implements WebSocketHandler { String progressMessage = buildMessage("PROGRESS", message); sendMessageToClient(qrContent, progressMessage); log.debug("发送进度消息: qrContent={}, step={}, message={}", qrContent, step, message); -<<<<<<< HEAD -======= - } - - /** - * 发送签到成功消息 - * - * @param qrContent 二维码内容 - * @param memberId 会员ID - * @param signInTime 签到时间 - */ - public static void sendSuccess(String qrContent, Long memberId, String signInTime) { - String successMessage = buildMessage("SUCCESS", "签到成功!欢迎光临\n会员ID: " + memberId + "\n签到时间: " + signInTime); - sendMessageToClient(qrContent, successMessage); - log.info("发送成功消息: qrContent={}, memberId={}", qrContent, memberId); - } - - /** - * 发送签到失败消息 - * - * @param qrContent 二维码内容 - * @param reason 失败原因 - */ - public static void sendFailure(String qrContent, String reason) { - String failureMessage = buildMessage("FAILURE", "签到失败:" + reason); - sendMessageToClient(qrContent, failureMessage); - log.warn("发送失败消息: qrContent={}, reason={}", qrContent, reason); - } - - /** - * 构建标准消息格式 - * - * @param type 消息类型 - * @param content 消息内容 - * @return 格式化后的消息字符串 - */ - private static String buildMessage(String type, String content) { - return JSONUtil.toJsonStr(Map.of( - "type", type, - "content", content, - "timestamp", System.currentTimeMillis() - )); - } - - /** - * 清理超时连接 - */ - private static void cleanupTimeoutConnections() { - LocalDateTime now = LocalDateTime.now(); - qrContentToCreateTime.entrySet().removeIf(entry -> { - LocalDateTime createTime = entry.getValue(); - long secondsDiff = java.time.Duration.between(createTime, now).getSeconds(); - if (secondsDiff > TIMEOUT_SECONDS) { - String qrContent = entry.getKey(); - qrContentToSink.remove(qrContent); - log.debug("清理超时连接: qrContent={}, 超时时间={}秒", qrContent, secondsDiff); - return true; - } - return false; - }); - } - - /** - * 获取当前连接数 - * - * @return 连接数 - */ - public static int getConnectionCount() { - cleanupTimeoutConnections(); - return qrContentToSink.size(); ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) } /** diff --git a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java index 8f35681..27fb944 100644 --- a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java +++ b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java @@ -25,10 +25,7 @@ import reactor.test.StepVerifier; import java.time.LocalDate; import java.time.LocalDateTime; -<<<<<<< HEAD -======= import java.time.LocalTime; ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import java.util.HashMap; import java.util.Map; diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java index b32601e..46d7c61 100644 --- a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java @@ -61,7 +61,6 @@ public final class RedisKeyConstants { * appType: miniapp(小程序), mp(公众号) */ public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:"; -<<<<<<< HEAD // ==================== 认证模块 ==================== @@ -71,6 +70,4 @@ public final class RedisKeyConstants { * 用途:存储登录/注册等场景的短信验证码 */ public static final String SMS_CODE = "sms:code:"; -======= ->>>>>>> 08cf82a (签到模块) } \ No newline at end of file diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java index eb22166..7cec5cf 100644 --- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package cn.novalon.gym.manage.sys.config; +package cn.novalon.gym.manage.sys.config; import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter; import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter; -- 2.52.0 From b5f8523554c26371f43a8f512e05e48c2e566d0c Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 24 Jun 2026 22:27:22 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-auth/pom.xml | 74 +---- .../gym/manage/auth/config/AuthConfig.java | 7 - .../gym/manage/auth/config/SmsProperties.java | 28 +- .../manage/auth/dto/PhoneCodeLoginDto.java | 3 - .../gym/manage/auth/dto/PhoneLoginDto.java | 15 +- .../gym/manage/auth/dto/SendCodeRequest.java | 22 -- .../gym/manage/auth/dto/SmsLoginDto.java | 25 -- .../manage/auth/handler/PhoneAuthHandler.java | 5 +- .../manage/auth/service/PhoneAuthService.java | 24 +- .../gym/manage/auth/service/SmsService.java | 35 +-- .../service/impl/PhoneAuthServiceImpl.java | 82 ++---- .../auth/service/impl/SmsServiceImpl.java | 252 +++++++++++------- .../gym/manage/auth/vo/PhoneLoginVO.java | 3 - ...ot.autoconfigure.AutoConfiguration.imports | 4 - gym-manage-api/gym-groupCourse/pom.xml | 17 +- .../gym/manage/member/entity/Member.java | 16 ++ gym-manage-api/manage-app/pom.xml | 2 +- .../gym/manage/app/config/SystemRouter.java | 3 +- .../src/main/resources/application-dev.yml | 8 + .../src/main/resources/member-config.yml | 4 +- .../gym/manage/common/config/AuthConfig.java | 19 ++ .../gym/manage/common/response/Result.java | 54 ++++ .../gym/manage/common/util/JwtUtil.java | 86 ++++++ ...Update_Member_User_For_One_Click_Login.sql | 21 ++ 24 files changed, 416 insertions(+), 393 deletions(-) delete mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java delete mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java delete mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java create mode 100644 gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java create mode 100644 gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java create mode 100644 gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql diff --git a/gym-manage-api/gym-auth/pom.xml b/gym-manage-api/gym-auth/pom.xml index 42aa5be..23e8943 100644 --- a/gym-manage-api/gym-auth/pom.xml +++ b/gym-manage-api/gym-auth/pom.xml @@ -10,11 +10,12 @@ 1.0.0 + cn.novalon.gym.manage gym-auth jar Gym Auth - Phone Authentication Module - Phone Number Login Services + Authentication module for Gym Management System @@ -29,93 +30,46 @@ cn.novalon.gym.manage - manage-sys + gym-member ${project.version} cn.novalon.gym.manage - gym-member + manage-sys ${project.version} + org.springframework.boot spring-boot-starter-webflux - - org.springdoc - springdoc-openapi-starter-webflux-ui - org.springframework.boot - spring-boot-starter-security - - - org.springframework.data - spring-data-commons - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - - com.aliyun - aliyun-java-sdk-dypnsapi - 1.2.12 + spring-boot-starter-validation + com.aliyun aliyun-java-sdk-core - 4.6.0 + 4.6.3 + - com.aliyun - dysmsapi20170525 - 2.0.0 + org.projectlombok + lombok + true - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - default-jar - package - - jar - - - - org.apache.maven.plugins maven-compiler-plugin 3.11.0 - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - + ${java.version} + ${java.version} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java deleted file mode 100644 index d8d842d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package cn.novalon.gym.manage.auth.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AuthConfig { -} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java index fe9e35c..31551f3 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java @@ -4,44 +4,20 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -/** - * 阿里云短信配置属性类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Data @Configuration @ConfigurationProperties(prefix = "alibaba.cloud.sms") public class SmsProperties { - /** - * 访问密钥ID - */ private String accessKeyId; - /** - * 访问密钥密钥 - */ private String accessKeySecret; - /** - * 短信签名名称 - */ private String signName; - /** - * 短信模板CODE - */ private String templateCode; - /** - * 短信验证码长度(默认6位) - */ - private int codeLength = 6; + private int codeLength = 4; - /** - * 短信验证码有效期(秒,默认300秒=5分钟) - */ private long codeExpireSeconds = 300; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java index 4d21283..033da34 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java @@ -7,9 +7,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号验证码登录请求DTO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java index c3d0758..2cc0743 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java @@ -1,28 +1,23 @@ package cn.novalon.gym.manage.auth.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录请求DTO - * uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号 - */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PhoneLoginDto { - @NotBlank(message = "access_token不能为空") - private String accessToken; - - @NotBlank(message = "openid不能为空") - private String openid; + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; private String nickname; private String avatar; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java deleted file mode 100644 index c16602d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 发送短信验证码请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SendCodeRequest { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java deleted file mode 100644 index d97cff5..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 短信验证码登录请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SmsLoginDto { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; - - @NotBlank(message = "验证码不能为空") - private String code; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java index 562920a..af46968 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java @@ -2,7 +2,6 @@ package cn.novalon.gym.manage.auth.handler; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; -import cn.novalon.gym.manage.auth.dto.SendCodeRequest; import cn.novalon.gym.manage.auth.service.PhoneAuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -39,8 +38,8 @@ public class PhoneAuthHandler { public Mono sendSmsCode(ServerRequest request) { log.info("收到发送短信验证码请求"); - return request.bodyToMono(SendCodeRequest.class) - .flatMap(req -> phoneAuthService.sendSmsCode(req.getPhone())) + return request.bodyToMono(PhoneLoginDto.class) + .flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone())) .flatMap(success -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of("success", success, "message", success ? "验证码发送成功" : "验证码发送失败"))); diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java index 2407a9b..cd2848e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java @@ -1,37 +1,15 @@ package cn.novalon.gym.manage.auth.service; -import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; +import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.vo.PhoneLoginVO; import reactor.core.publisher.Mono; -/** - * 手机号认证服务接口 - */ public interface PhoneAuthService { - /** - * 手机号一键登录(uniapp官方运营商认证) - * 已注册则直接登录,未注册则自动注册后登录 - * - * @param request 登录请求 - * @return 登录响应 - */ Mono oneClickLogin(PhoneLoginDto request); - /** - * 发送短信验证码(阿里云) - * - * @param phone 手机号 - * @return 是否发送成功 - */ Mono sendSmsCode(String phone); - /** - * 手机号验证码登录(阿里云) - * - * @param request 登录请求 - * @return 登录响应 - */ Mono codeLogin(PhoneCodeLoginDto request); } \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java index 914f4ff..585074e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java @@ -1,35 +1,12 @@ package cn.novalon.gym.manage.auth.service; -/** - * 短信服务接口 - * - * @author auto-generated - * @date 2026-06-20 - */ +import reactor.core.publisher.Mono; + public interface SmsService { - /** - * 发送短信验证码 - * - * @param phone 手机号 - * @return 发送结果 - */ - boolean sendVerificationCode(String phone); + Mono sendVerificationCode(String phone); - /** - * 验证短信验证码 - * - * @param phone 手机号 - * @param code 验证码 - * @return 验证结果 - */ - boolean verifyCode(String phone, String code); + Mono verifyCode(String phone, String code); - /** - * 获取验证码(用于测试或特殊场景) - * - * @param phone 手机号 - * @return 验证码 - */ - String getVerificationCode(String phone); -} + Mono getVerificationCode(String phone); +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java index 468f5a1..fe96c7f 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java @@ -1,6 +1,5 @@ package cn.novalon.gym.manage.auth.service.impl; -import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.service.PhoneAuthService; @@ -16,13 +15,6 @@ import cn.novalon.gym.manage.member.util.AesUtil; import cn.novalon.gym.manage.member.util.EsSyncUtils; import cn.novalon.gym.manage.member.util.MemberNoGenerator; import cn.novalon.gym.manage.sys.security.JwtTokenProvider; -import com.aliyuncs.DefaultAcsClient; -import com.aliyuncs.IAcsClient; -import com.aliyuncs.dypnsapi.model.v20170525.GetMobileRequest; -import com.aliyuncs.dypnsapi.model.v20170525.GetMobileResponse; -import com.aliyuncs.exceptions.ClientException; -import com.aliyuncs.exceptions.ServerException; -import com.aliyuncs.profile.DefaultProfile; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,7 +35,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { private final EsSyncUtils esSyncUtils; private final JwtTokenProvider jwtTokenProvider; private final SmsService smsService; - private final SmsProperties smsProperties; private EsSyncUtils.EntitySyncer memberSyncer; @@ -54,76 +45,32 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { @Override public Mono oneClickLogin(PhoneLoginDto request) { - log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid()); + log.info("手机号一键登录, phone: {}", request.getPhone()); - return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid())) - .flatMap(phone -> { - log.info("通过access_token获取手机号成功: {}", maskPhone(phone)); - String encryptedPhone = encryptPhone(phone); + String encryptedPhone = encryptPhone(request.getPhone()); - return memberRepository.findByPhone(encryptedPhone) - .flatMap(existingMember -> { - log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); - return doLogin(existingMember, false, request); - }) - .switchIfEmpty(Mono.defer(() -> { - log.info("手机号未注册,创建新会员"); - return createNewMemberAndLogin(request, encryptedPhone); - })); + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); + return doLogin(existingMember, false, request); }) - .onErrorResume(e -> { - log.error("一键登录失败", e); - return Mono.error(new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + e.getMessage())); - }); - } - - /** - * 通过access_token和openid获取手机号 - * 使用阿里云DYPNS API - */ - private String getPhoneByToken(String accessToken, String openid) throws Exception { - DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret()); - IAcsClient client = new DefaultAcsClient(profile); - GetMobileRequest request = new GetMobileRequest(); - request.setAccessToken(accessToken); - - try { - GetMobileResponse response = client.getAcsResponse(request); - if ("OK".equals(response.getCode())) { - return response.getGetMobileResultDTO().getMobile(); - } - log.warn("DYPNS API返回错误: code={}, message={}", response.getCode(), response.getMessage()); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败: " + response.getMessage()); - } catch (ServerException e) { - log.error("阿里云DYPNS API服务端异常", e); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败"); - } catch (ClientException e) { - log.error("阿里云DYPNS API客户端异常", e); - throw new SystemException(ErrorCode.AUTH_PHONE_ERROR, "手机号获取失败"); - } - } - - /** - * 手机号脱敏显示 - */ - private String maskPhone(String phone) { - if (phone == null || phone.length() < 11) { - return phone; - } - return phone.substring(0, 3) + "****" + phone.substring(7); + .switchIfEmpty(Mono.defer(() -> { + log.info("手机号未注册,创建新会员"); + return createNewMemberAndLogin(request, encryptedPhone); + })); } @Override public Mono sendSmsCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - return Mono.fromCallable(() -> smsService.sendVerificationCode(phone)); + return smsService.sendVerificationCode(phone); } @Override public Mono codeLogin(PhoneCodeLoginDto request) { log.info("手机号验证码登录, phone: {}", request.getPhone()); - return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode())) + return smsService.verifyCode(request.getPhone(), request.getCode()) .flatMap(verified -> { if (!verified) { log.warn("验证码验证失败, phone: {}", request.getPhone()); @@ -140,6 +87,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { .switchIfEmpty(Mono.defer(() -> { log.info("手机号未注册,创建新会员"); PhoneLoginDto registerRequest = new PhoneLoginDto(); + registerRequest.setPhone(request.getPhone()); return createNewMemberAndLogin(registerRequest, encryptedPhone); })); }); @@ -205,7 +153,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { vo.setNeedCompleteInfo(needCompleteInfo); vo.setNickname(member.getNickname()); vo.setAvatar(member.getAvatar()); - vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null); + vo.setPhone(decryptPhone(member.getPhone())); return vo; } @@ -226,4 +174,4 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { return null; } } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java index 2162e13..2a8b630 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java @@ -1,148 +1,198 @@ package cn.novalon.gym.manage.auth.service.impl; -import cn.hutool.core.util.RandomUtil; import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.service.SmsService; import cn.novalon.gym.manage.common.constant.RedisKeyConstants; import cn.novalon.gym.manage.common.util.RedisUtil; -import com.aliyun.dysmsapi20170525.Client; -import com.aliyun.dysmsapi20170525.models.SendSmsRequest; -import com.aliyun.dysmsapi20170525.models.SendSmsResponse; -import com.aliyun.teaopenapi.models.Config; +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; -/** - * 短信服务实现类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Slf4j @Service @RequiredArgsConstructor public class SmsServiceImpl implements SmsService { + private static final long SEND_INTERVAL_SECONDS = 60; + private static final long CODE_EXPIRE_SECONDS = 300; + private final SmsProperties smsProperties; private final RedisUtil redisUtil; + private final ObjectMapper objectMapper; @Override - public boolean sendVerificationCode(String phone) { + public Mono sendVerificationCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - try { - // 生成验证码 - String code = generateCode(); + String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit"; - // 发送短信 - boolean sent = sendSms(phone, code); + return redisUtil.get(rateLimitKey, Long.class) + .defaultIfEmpty(0L) + .flatMap(lastSendTime -> { + long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); - if (sent) { - // 将验证码存入Redis,5分钟过期 - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds()) - .doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s", - phone, code, smsProperties.getCodeExpireSeconds())) - .block(); + if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) { + long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime); + log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds); + return Mono.just(false); + } - log.info("短信验证码发送成功, phone: {}", phone); - return true; - } + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - log.warn("短信验证码发送失败, phone: {}", phone); - return false; + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("SendSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("SignName", smsProperties.getSignName()); + request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode()); + request.putQueryParameter("TemplateParam", "{\"code\":\"##code##\",\"min\":\"5\"}"); + request.putQueryParameter("ReturnVerifyCode", "true"); - } catch (Exception e) { - log.error("发送短信验证码异常, phone: {}", phone, e); - return false; - } + log.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}", + smsProperties.getSignName(), + smsProperties.getTemplateCode(), + "{\"code\":\"##code##\",\"min\":\"5\"}"); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String code = codeNode.asText(); + + if ("OK".equals(code)) { + JsonNode requestIdNode = jsonNode.get("RequestId"); + String requestId = requestIdNode != null ? requestIdNode.asText() : "unknown"; + log.info("短信验证码发送成功, phone: {}, requestId: {}", phone, requestId); + + // 提取验证码并存入Redis + JsonNode modelNode = jsonNode.get("Model"); + if (modelNode != null) { + JsonNode verifyCodeNode = modelNode.get("VerifyCode"); + if (verifyCodeNode != null) { + String verifyCode = verifyCodeNode.asText(); + String smsCodeKey = RedisKeyConstants.SMS_CODE + phone; + redisUtil.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe(); + log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS); + } else { + log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData); + } + } else { + log.warn("响应中未找到Model字段, 原始响应: {}", responseData); + } + + redisUtil.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe(); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.error("短信验证码发送失败, phone: {}, code: {}, message: {}", phone, code, message); + return false; + + } catch (Exception e) { + log.error("发送短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); + return false; + } + }); + }); } @Override - public boolean verifyCode(String phone, String code) { + public Mono verifyCode(String phone, String code) { log.info("验证短信验证码, phone: {}", phone); - try { - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - String cachedCode = redisUtil.get(cacheKey, String.class).block(); + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - if (cachedCode == null) { - log.warn("验证码已过期或不存在, phone: {}", phone); + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("CheckSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("VerifyCode", code); + + log.info("阿里云号码认证核验参数 - phone: {}, code: {}", phone, code); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证核验原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云核验响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String responseCode = codeNode.asText(); + JsonNode modelNode = jsonNode.get("Model"); + JsonNode verifyResultNode = modelNode != null ? modelNode.get("VerifyResult") : null; + boolean verifyResult = verifyResultNode != null && "PASS".equals(verifyResultNode.asText()); + + if ("OK".equals(responseCode) && verifyResult) { + log.info("验证码验证成功, phone: {}", phone); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.warn("验证码验证失败, phone: {}, code: {}, message: {}, result: {}", + phone, responseCode, message, verifyResult); + return false; + + } catch (Exception e) { + log.error("验证短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); return false; } - - if (cachedCode.equals(code)) { - // 验证成功后删除验证码 - redisUtil.delete(cacheKey).block(); - log.info("验证码验证成功, phone: {}", phone); - return true; - } - - log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode); - return false; - - } catch (Exception e) { - log.error("验证短信验证码异常, phone: {}", phone, e); - return false; - } + }); } @Override - public String getVerificationCode(String phone) { + public Mono getVerificationCode(String phone) { try { String cacheKey = RedisKeyConstants.SMS_CODE + phone; - return redisUtil.get(cacheKey, String.class).block(); + return redisUtil.get(cacheKey, String.class); } catch (Exception e) { log.error("获取验证码异常, phone: {}", phone, e); - return null; + return Mono.empty(); } } - /** - * 生成验证码 - */ - private String generateCode() { - return RandomUtil.randomNumbers(smsProperties.getCodeLength()); + private IAcsClient createClient() throws ClientException { + IClientProfile profile = DefaultProfile.getProfile( + "cn-hangzhou", + smsProperties.getAccessKeyId(), + smsProperties.getAccessKeySecret()); + DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", "Dypnsapi", "dypnsapi.aliyuncs.com"); + return new DefaultAcsClient(profile); } - - /** - * 发送短信 - */ - private boolean sendSms(String phone, String code) throws Exception { - log.info("调用阿里云短信API发送短信, phone: {}, code: {}", phone, code); - - // 创建阿里云短信客户端 - Config config = new Config() - .setAccessKeyId(smsProperties.getAccessKeyId()) - .setAccessKeySecret(smsProperties.getAccessKeySecret()) - .setEndpoint("dysmsapi.aliyuncs.com"); - - Client client = new Client(config); - - // 构建发送短信请求 - SendSmsRequest request = new SendSmsRequest() - .setPhoneNumbers(phone) - .setSignName(smsProperties.getSignName()) - .setTemplateCode(smsProperties.getTemplateCode()) - .setTemplateParam("{\"code\":\"" + code + "\"}"); - - // 发送短信 - SendSmsResponse response = client.sendSms(request); - - log.info("阿里云短信API响应, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - - // 判断发送结果 - if ("OK".equals(response.getBody().getCode())) { - return true; - } - - log.error("阿里云短信发送失败, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - return false; - } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java index f5f4651..795c103 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java @@ -5,9 +5,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录响应VO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 216a5f9..beb4996 100644 --- a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1 @@ -cn.novalon.gym.manage.auth.handler.PhoneAuthHandler -cn.novalon.gym.manage.auth.service.impl.PhoneAuthServiceImpl -cn.novalon.gym.manage.auth.service.impl.SmsServiceImpl -cn.novalon.gym.manage.auth.config.AuthConfig cn.novalon.gym.manage.auth.config.SmsProperties \ No newline at end of file diff --git a/gym-manage-api/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index 12e0171..bfafbbb 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -35,6 +35,11 @@ manage-common ${project.version} + + cn.novalon.gym.manage + manage-sys + ${project.version} + cn.novalon.gym.manage manage-db @@ -81,8 +86,8 @@ gym-member ${project.version} - - + + com.google.zxing core @@ -93,20 +98,20 @@ javase 3.5.3 + com.aliyun.oss aliyun-sdk-oss - 3.17.4 + 3.17.1 - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 + org.springframework.boot + spring-boot-maven-plugin diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java index 8983e20..5c96f8f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -71,6 +71,22 @@ public class Member extends BaseEntity { @Column("official_open_id") private String officialOpenId; + // 阿里云号码认证OpenID + @Column("dypns_open_id") + private String dypnsOpenId; + + // 身份证号码(AES加密存储) + @Column("id_card") + private String idCard; + + // 真实姓名(AES加密存储) + @Column("real_name") + private String realName; + + // 注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权 + @Column("register_channel") + private String registerChannel; + // 软删除 @Column("is_deleted") private Boolean isDeleted; diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index beeccbe..ed55a4e 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -63,7 +63,7 @@ gym-auth ${project.version} - + org.springframework.boot spring-boot-starter-webflux 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 9ec7cd4..490e5bf 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,10 +1,11 @@ package cn.novalon.gym.manage.app.config; +import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; + import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler; diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index c696a05..756a560 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -36,3 +36,11 @@ logging: cn.novalon.manage: DEBUG org.springframework.r2dbc: DEBUG org.springframework.web: TRACE + +alibaba: + cloud: + sms: + access-key-id: LTAI5t8GhorWLu5WkEx8MDZz + access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz + sign-name: 云渚科技验证平台 + template-code: 100001 diff --git a/gym-manage-api/manage-app/src/main/resources/member-config.yml b/gym-manage-api/manage-app/src/main/resources/member-config.yml index 61bb6c5..178d297 100644 --- a/gym-manage-api/manage-app/src/main/resources/member-config.yml +++ b/gym-manage-api/manage-app/src/main/resources/member-config.yml @@ -16,8 +16,8 @@ wechat: # 手机号加密配置 phone-encryption: - secret-key: ${PHONE_ENCRYPTION_SECRET_KEY} - iv: ${PHONE_ENCRYPTION_IV} + secret-key: ${PHONE_ENCRYPTION_SECRET_KEY:P8539ANjWJWsRbVHZKhM8Q==} + iv: ${PHONE_ENCRYPTION_IV:3tHp07uMRYh1xKsIXvYJMA==} spring: elasticsearch: diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java new file mode 100644 index 0000000..d4995e8 --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java @@ -0,0 +1,19 @@ +package cn.novalon.gym.manage.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "auth") +public class AuthConfig { + + private String accessKeyId; + + private String accessKeySecret; + + private Integer tokenExpireSeconds = 86400; + + private Integer refreshTokenExpireSeconds = 604800; +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java new file mode 100644 index 0000000..7b0efbc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java @@ -0,0 +1,54 @@ +package cn.novalon.gym.manage.common.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + private Integer code; + + private String message; + + private T data; + + private Long timestamp; + + public static Result success(T data) { + return Result.builder() + .code(200) + .message("success") + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result success(String message) { + return Result.builder() + .code(200) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(Integer code, String message) { + return Result.builder() + .code(code) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(String message) { + return Result.builder() + .code(500) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java new file mode 100644 index 0000000..bae13cc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java @@ -0,0 +1,86 @@ +package cn.novalon.gym.manage.common.util; + +import cn.novalon.gym.manage.common.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtil { + + private final JwtProperties jwtProperties; + + private SecretKey getSigningKey() { + String secret = jwtProperties.getSecret(); + if (secret.length() < 32) { + StringBuilder sb = new StringBuilder(secret); + while (sb.length() < 32) { + sb.append(secret); + } + secret = sb.substring(0, 32); + } + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String userId, String phone) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration()); + + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("phone", phone); + + return Jwts.builder() + .setClaims(claims) + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public String generateRefreshToken(String userId) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration() * 7); + + return Jwts.builder() + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String getUserIdFromToken(String token) { + return parseToken(token).getSubject(); + } + + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (Exception e) { + log.warn("JWT token validation failed: {}", e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql new file mode 100644 index 0000000..05765d1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql @@ -0,0 +1,21 @@ +-- ============================================ +-- V20: 更新 member_user 表 - 添加阿里云号码认证字段 +-- 支持一键登录功能 +-- ============================================ + +-- 添加阿里云号码认证相关字段 +ALTER TABLE IF EXISTS member_user + ADD COLUMN IF NOT EXISTS dypns_open_id VARCHAR(100), + ADD COLUMN IF NOT EXISTS id_card VARCHAR(50), + ADD COLUMN IF NOT EXISTS real_name VARCHAR(50), + ADD COLUMN IF NOT EXISTS register_channel VARCHAR(50) DEFAULT 'SMS'; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_member_user_dypns_open_id ON member_user(dypns_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_register_channel ON member_user(register_channel); + +-- 添加字段注释 +COMMENT ON COLUMN member_user.dypns_open_id IS '阿里云号码认证OpenID(一键登录用户唯一标识)'; +COMMENT ON COLUMN member_user.id_card IS '身份证号码(AES加密存储)'; +COMMENT ON COLUMN member_user.real_name IS '真实姓名(AES加密存储)'; +COMMENT ON COLUMN member_user.register_channel IS '注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权'; -- 2.52.0 From 840e24dc3e1ff05b2e565112117f534057371612 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 24 Jun 2026 22:32:38 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E5=90=88=E5=B9=B6dev=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/KeyAnalyzer.java | 202 -------------------------------- 1 file changed, 202 deletions(-) delete mode 100644 gym-manage-api/KeyAnalyzer.java diff --git a/gym-manage-api/KeyAnalyzer.java b/gym-manage-api/KeyAnalyzer.java deleted file mode 100644 index e460fe7..0000000 --- a/gym-manage-api/KeyAnalyzer.java +++ /dev/null @@ -1,202 +0,0 @@ -import java.util.Base64; -import java.math.BigInteger; -import java.io.ByteArrayInputStream; - -public class KeyAnalyzer { - public static void main(String[] args) { - String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcxAwrhZF+izz1gxQKOr4jnC05obIBHbl0DzHOcd4CaaDV7Kv1NRJKi33JhdDsW4JGgu16e5Rtzq1VzU5VWz5EKgGL46maOFwCkngJUTP/LC3JVf/wtJmYCm8xNE8C7GNNIqKzYEvUOEfXqagpVvVrGsQ9FpzFp2rP9hBmHY3yizVyPX/uT9S+af5TRxiaItj3SSJGgloaEMrnKOpb/EH7JwPSS0liAyT/NxPfOyyZHc22AvaAIOE5y+0PMUIKPuIdfpOrej3LVpO1Arc2hSgmdB+YIPSiBVYPXa6AuAmil9mpbtSikQJ7Uu7lX4JyTW4QxQ06rPFKnFWVKkzivAElAgMBAAECggEAJd0AJ37iTlMpDQ90xqe7hvRQxAu256gbQ9nrqLY97g0/KIw6WEZSPakFX6gvdvb/NzKmUyAIEKGLoh6tXdZk6qfOqc/6BeK47nIcBfwT9/zerjNUVvn34w4aHyNINieMMHQ+Id8PUZmqWH+Euz9ilVTosuyEPwUZulLvUQqwXzU5VnwVghURbUhDd+ecBJACWgemRun6d5241PQXNYAdH1k7cETd8GfIi3qclhhJrxi7tu5tq4YGCXQIoz7HCLim7GIvT0M+FRgSw2EOrHnAQNFeQ/vQbP71ttLoTxehL6Se9dfWrV5OI+Y/T7vR2F84Qt0iNbaxyJGir7siKDFIwQKBgQDXDzinx3/TasplM78pR/0CtuuKr1Ch02LOPrTosJ1qf1OohxQThowhOTxMsBlgYSKu9s1QRffUUXEqYXxd2B6lzDKfggwO6U2XxIcxWeNow0xoFfqcXYSg7Ga2sCr9uhdwxIdFQNF7SNBpT8ht4fJrRX6mWY1nHybpyTDQ4xoQNQKBgQC6m+yiOoi5JD3zVSSJq/iq5DJPA5B4aoP+t5u9lp2Q7iVO0QI5ilBlEKGE4VOU0glnXlDTfuqEYooMY85ekl4WGb3AOT0PhLL2i+gO2nlWBzf4HPzB/hibjfyPyniRM03cHkG3HXucL7Sne6FwERcfjEqjUd2cdP1l89PNrq4rMQKBgQCMmjABSWYh8/y1I5rEQ4OAJdVjC3GdC1Xa35ZpVybjvLEWSpHunhW5lvD8dllw8LC7UTI0XDpGPqTM/4VO2YBYB2PFc0Gs8g0/v0ZgFpOeJ6kpl80MM/wFNemFYTIKRoMSv/psZY9PmfBgGcBBTuquBXZjDcNr+yr2yAm5V/DvTQKBgHqRi94KoF8q5N39IKCkqhJlDH5FkxDktYoKw2rFkPzuzuZz9gghRyj6wXxsG9/2DWMt2dzw0czehFoa/CO188KEadPmRKr6uCmkP2nyKhxNZX+8WnB5G2Sg4DD6BjMpBYz8+qDx5ozx8LDJTYI0V4HLPgMD9JGdbgsXGhlREOkhAoGALr6IQOXnviWNAhCdc7rrsaMLMPbLZ1wqzWtQUG1JxDobbpzEP4CW/mvW5pMn58mSBg5qbXhyDI4fFP0CPb98QIz2tGnIzYyFzdKmF5Z1N7X1OF9O+tsSqASoBZzTqB4fr/o4mz0s9JCeriBR2LWjsbsDU13DTLsfQpWbtOnIy70="; - - System.out.println("========== 密钥分析开始 =========="); - System.out.println("密钥长度: " + privateKeyBase64.length()); - - byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64); - System.out.println("解码后长度: " + keyBytes.length + " bytes"); - - // 打印前30字节 - System.out.print("前30字节(hex): "); - for (int i = 0; i < 30 && i < keyBytes.length; i++) { - System.out.printf("%02X ", keyBytes[i]); - } - System.out.println(); - - // 解析ASN.1结构 - try { - parseASN1(keyBytes); - } catch (Exception e) { - System.out.println("解析失败: " + e.getMessage()); - e.printStackTrace(); - } - } - - static void parseASN1(byte[] keyBytes) throws Exception { - ByteArrayInputStream bis = new ByteArrayInputStream(keyBytes); - - // 读取SEQUENCE - int tag = bis.read(); - System.out.println("第一个tag: 0x" + Integer.toHexString(tag)); - - if (tag == 0x30) { - System.out.println("这是SEQUENCE"); - - // 读取长度 - int len = readASN1Length(bis); - System.out.println("SEQUENCE长度: " + len); - - // 检查下一个tag - int nextTag = bis.read(); - System.out.println("下一个tag: 0x" + Integer.toHexString(nextTag)); - - if (nextTag == 0x02) { - // INTEGER - int intLen = readASN1Length(bis); - byte[] versionBytes = new byte[intLen]; - bis.read(versionBytes); - int version = new BigInteger(versionBytes).intValue(); - System.out.println("版本INTEGER值: " + version); - - // 检查这是PKCS#8还是PKCS#1 - // PKCS#8: version=0后是SEQUENCE(算法标识符) - // PKCS#1: version=0后是INTEGER(modulus) - int afterVersionTag = bis.read(); - System.out.println("版本后的tag: 0x" + Integer.toHexString(afterVersionTag)); - - if (afterVersionTag == 0x30) { - System.out.println(">>> 这是PKCS#8格式 (version后是SEQUENCE)"); - - // 解析PKCS#8 - // AlgorithmIdentifier: SEQUENCE { OID, NULL } - int algLen = readASN1Length(bis); - System.out.println("AlgorithmIdentifier长度: " + algLen); - - // 跳过AlgorithmIdentifier内容 - byte[] algBytes = new byte[algLen]; - bis.read(algBytes); - - // 打印OID - System.out.print("OID bytes: "); - for (int i = 0; i < algBytes.length; i++) { - System.out.printf("%02X ", algBytes[i]); - } - System.out.println(); - - // 下一个应该是OCTET STRING (包含私钥) - int octetTag = bis.read(); - System.out.println("下一个tag: 0x" + Integer.toHexString(octetTag)); - - if (octetTag == 0x04) { - System.out.println("这是OCTET STRING (包含私钥数据)"); - int octetLen = readASN1Length(bis); - System.out.println("OCTET STRING长度: " + octetLen); - - // OCTET STRING内容是PKCS#1私钥 - byte[] pkcs1Bytes = new byte[octetLen]; - bis.read(pkcs1Bytes); - - System.out.print("PKCS#1私钥前20字节: "); - for (int i = 0; i < 20; i++) { - System.out.printf("%02X ", pkcs1Bytes[i]); - } - System.out.println(); - - // 解析PKCS#1私钥 - ByteArrayInputStream pkcs1Stream = new ByteArrayInputStream(pkcs1Bytes); - int pkcs1Tag = pkcs1Stream.read(); - System.out.println("PKCS#1第一个tag: 0x" + Integer.toHexString(pkcs1Tag)); - - if (pkcs1Tag == 0x30) { - int pkcs1Len = readASN1Length(pkcs1Stream); - System.out.println("PKCS#1 SEQUENCE长度: " + pkcs1Len); - - // version - int vTag = pkcs1Stream.read(); - int vLen = readASN1Length(pkcs1Stream); - byte[] vBytes = new byte[vLen]; - pkcs1Stream.read(vBytes); - System.out.println("PKCS#1版本: " + new BigInteger(vBytes)); - - // 解析私钥参数 - parsePKCS1(pkcs1Stream); - } - } - } else if (afterVersionTag == 0x02) { - System.out.println(">>> 这是PKCS#1格式 (version后是INTEGER)"); - - // 继续解析PKCS#1 - parsePKCS1(bis); - } else { - System.out.println(">>> 未知格式 (tag: 0x" + Integer.toHexString(afterVersionTag) + ")"); - } - } - } - } - - static void parsePKCS1(ByteArrayInputStream bis) throws Exception { - System.out.println("\n========== 解析PKCS#1私钥参数 =========="); - - // modulus - BigInteger modulus = readASN1Integer(bis); - System.out.println("modulus长度: " + modulus.bitLength() + " bits"); - - // publicExponent - BigInteger publicExponent = readASN1Integer(bis); - System.out.println("publicExponent: " + publicExponent); - - // privateExponent - BigInteger privateExponent = readASN1Integer(bis); - System.out.println("privateExponent长度: " + privateExponent.bitLength() + " bits"); - - // prime1 - BigInteger prime1 = readASN1Integer(bis); - System.out.println("prime1长度: " + prime1.bitLength() + " bits"); - - // prime2 - BigInteger prime2 = readASN1Integer(bis); - System.out.println("prime2长度: " + prime2.bitLength() + " bits"); - - // 验证 prime1 * prime2 == modulus - BigInteger calculatedModulus = prime1.multiply(prime2); - boolean valid = calculatedModulus.equals(modulus); - System.out.println("\n验证 prime1 * prime2 == modulus: " + valid); - - if (!valid) { - System.out.println(">>> 密钥数学关系不正确!这是无效的RSA私钥"); - System.out.println("计算得到的modulus长度: " + calculatedModulus.bitLength() + " bits"); - } - - // exponent1 - BigInteger exponent1 = readASN1Integer(bis); - System.out.println("exponent1长度: " + exponent1.bitLength() + " bits"); - - // exponent2 - BigInteger exponent2 = readASN1Integer(bis); - System.out.println("exponent2长度: " + exponent2.bitLength() + " bits"); - - // coefficient - BigInteger coefficient = readASN1Integer(bis); - System.out.println("coefficient长度: " + coefficient.bitLength() + " bits"); - - System.out.println("\n========== 解析完成 =========="); - } - - static int readASN1Length(ByteArrayInputStream bis) throws Exception { - int firstByte = bis.read(); - if ((firstByte & 0x80) == 0) { - return firstByte; - } - int numBytes = firstByte & 0x7F; - int length = 0; - for (int i = 0; i < numBytes; i++) { - length = (length << 8) | (bis.read() & 0xFF); - } - return length; - } - - static BigInteger readASN1Integer(ByteArrayInputStream bis) throws Exception { - int tag = bis.read(); - if (tag != 0x02) throw new Exception("期望INTEGER tag: 0x02, 实际: 0x" + Integer.toHexString(tag)); - int len = readASN1Length(bis); - byte[] data = new byte[len]; - bis.read(data); - return new BigInteger(1, data); - } -} \ No newline at end of file -- 2.52.0 From e81ec8cca7c6a97c1f9fe767790ac1a27b47556a Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Wed, 24 Jun 2026 23:23:55 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E5=88=A0=E9=99=A4gym-payment=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E4=B8=8Edev=E5=88=86=E6=94=AF=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/gym-payment/pom.xml | 62 --- .../payment/config/AlipayClientConfig.java | 32 -- .../payment/config/AlipayProperties.java | 57 --- .../gym/manage/payment/dto/ApiResponse.java | 52 -- .../payment/dto/CreatePaymentRequest.java | 54 --- .../manage/payment/dto/PaymentResponse.java | 54 --- .../payment/handler/PaymentHandler.java | 140 ------ .../payment/service/PaymentService.java | 59 --- .../service/impl/PaymentServiceImpl.java | 445 ------------------ gym-manage-api/manage-app/pom.xml | 5 - gym-manage-api/pom.xml | 1 - 11 files changed, 961 deletions(-) delete mode 100644 gym-manage-api/gym-payment/pom.xml delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayClientConfig.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayProperties.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/ApiResponse.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/CreatePaymentRequest.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/PaymentResponse.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/PaymentService.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java diff --git a/gym-manage-api/gym-payment/pom.xml b/gym-manage-api/gym-payment/pom.xml deleted file mode 100644 index fc273a9..0000000 --- a/gym-manage-api/gym-payment/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 4.0.0 - - - cn.novalon.gym.manage - gym-manage-api - 1.0.0 - - - cn.novalon.gym.manage - gym-payment - jar - - Gym Payment - 支付模块 - 支付宝App支付 - - - - - org.springframework.boot - spring-boot-starter-webflux - - - - - cn.novalon.gym.manage - manage-common - ${project.version} - - - - - com.alipay.sdk - alipay-sdk-java - 4.35.120.ALL - - - - - com.google.code.gson - gson - 2.10.1 - - - - - org.projectlombok - lombok - true - - - - - io.swagger.core.v3 - swagger-annotations - 2.2.19 - - - diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayClientConfig.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayClientConfig.java deleted file mode 100644 index e0b747e..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayClientConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.novalon.gym.manage.payment.config; - -import com.alipay.api.AlipayClient; -import com.alipay.api.DefaultAlipayClient; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 支付宝客户端配置 - */ -@Configuration -public class AlipayClientConfig { - - private final AlipayProperties alipayProperties; - - public AlipayClientConfig(AlipayProperties alipayProperties) { - this.alipayProperties = alipayProperties; - } - - @Bean - public AlipayClient alipayClient() { - return new DefaultAlipayClient( - alipayProperties.getGateway(), - alipayProperties.getAppId(), - alipayProperties.getAppPrivateKey(), - "json", - alipayProperties.getCharset(), - alipayProperties.getAlipayPublicKey(), - alipayProperties.getSignType() - ); - } -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayProperties.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayProperties.java deleted file mode 100644 index f78fd71..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/AlipayProperties.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.novalon.gym.manage.payment.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * 支付宝配置属性 - * 沙箱环境配置 - */ -@Data -@Component -@ConfigurationProperties(prefix = "alipay") -public class AlipayProperties { - - /** - * 应用ID - */ - private String appId; - - /** - * 应用私钥 (PKCS#8格式) - */ - private String appPrivateKey; - - /** - * 支付宝公钥 - */ - private String alipayPublicKey; - - /** - * 签名方式 - */ - private String signType = "RSA2"; - - /** - * 编码格式 - */ - private String charset = "UTF-8"; - - /** - * 网关地址 - * 沙箱环境: https://openapi-sandbox.dl.alipaydev.com/gateway.do - * 正式环境: https://openapi.alipay.com/gateway.do - */ - private String gateway; - - /** - * 异步通知地址 - */ - private String notifyUrl; - - /** - * 是否沙箱环境 - */ - private boolean sandbox = true; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/ApiResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/ApiResponse.java deleted file mode 100644 index b01b20a..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/ApiResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.novalon.gym.manage.payment.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 统一API响应 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ApiResponse { - - private int code; - private String message; - private T data; - - public static ApiResponse success(T data) { - return ApiResponse.builder() - .code(200) - .message("success") - .data(data) - .build(); - } - - public static ApiResponse success(String message, T data) { - return ApiResponse.builder() - .code(200) - .message(message) - .data(data) - .build(); - } - - public static ApiResponse error(int code, String message) { - return ApiResponse.builder() - .code(code) - .message(message) - .data(null) - .build(); - } - - public static ApiResponse error(String message) { - return ApiResponse.builder() - .code(500) - .message(message) - .data(null) - .build(); - } -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/CreatePaymentRequest.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/CreatePaymentRequest.java deleted file mode 100644 index 5b1347e..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/CreatePaymentRequest.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.novalon.gym.manage.payment.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -/** - * 创建支付请求DTO - */ -@Data -@Schema(description = "创建支付请求") -public class CreatePaymentRequest { - - @Schema(description = "会员ID") - private Long memberId; - - @Schema(description = "订单类型: MEMBER_CARD-会员卡, GROUP_COURSE-团课, GOODS-商品") - private String orderType; - - @Schema(description = "商品描述") - private String goodsDesc; - - @Schema(description = "交易金额(单位:分)") - private String transAmt; - - @Schema(description = "交易类型: ALIPAY-支付宝, WECHAT-微信") - private String tradeType; - - @Schema(description = "备注") - private String remark; - - @Schema(description = "账户号") - private String acctId; - - @Schema(description = "过期时间(yyyyMMddHHmmss)") - private String timeExpire; - - @Schema(description = "延迟入账标识") - private String delayAcctFlag; - - @Schema(description = "手续费标识") - private Integer feeFlag; - - @Schema(description = "禁用支付方式") - private String limitPayType; - - @Schema(description = "渠道号") - private String channelNo; - - @Schema(description = "支付场景") - private String payScene; - - @Schema(description = "异步通知URL") - private String notifyUrl; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/PaymentResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/PaymentResponse.java deleted file mode 100644 index 50fd4cb..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/PaymentResponse.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.novalon.gym.manage.payment.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 支付响应DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "支付响应") -public class PaymentResponse { - - @Schema(description = "订单ID") - private String orderId; - - @Schema(description = "支付状态: PENDING-待支付, SUCCESS-成功, FAIL-失败, CLOSED-关闭") - private String status; - - @Schema(description = "支付链接(App支付返回 scheme)") - private String payUrl; - - @Schema(description = "二维码链接 (扫码支付)") - private String qrCode; - - @Schema(description = "完整支付信息") - private String payInfo; - - @Schema(description = "H5支付链接") - private String h5PayUrl; - - @Schema(description = "错误码") - private String errorCode; - - @Schema(description = "错误消息") - private String errorMsg; - - @Schema(description = "交易金额(分)") - private String transAmt; - - @Schema(description = "商品描述") - private String goodsDesc; - - @Schema(description = "交易类型") - private String tradeType; - - @Schema(description = "支付成功时间") - private String payTime; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java deleted file mode 100644 index e402f0c..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java +++ /dev/null @@ -1,140 +0,0 @@ -package cn.novalon.gym.manage.payment.handler; - -import cn.novalon.gym.manage.payment.dto.ApiResponse; -import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest; -import cn.novalon.gym.manage.payment.dto.PaymentResponse; -import cn.novalon.gym.manage.payment.service.PaymentService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; - -import java.util.HashMap; -import java.util.Map; - -/** - * 支付处理器 - * 提供支付宝App支付相关接口 - */ -@Slf4j -@Component -@Tag(name = "支付管理", description = "支付宝App支付接口") -public class PaymentHandler { - - private final PaymentService paymentService; - - public PaymentHandler(PaymentService paymentService) { - this.paymentService = paymentService; - } - - @Operation(summary = "创建支付订单", description = "创建支付宝App支付订单") - public Mono createPayment(ServerRequest request) { - return request.bodyToMono(CreatePaymentRequest.class) - .flatMap(createReq -> { - log.info("[Payment] 创建支付订单: memberId={}, orderType={}, goodsDesc={}, transAmt={}", - createReq.getMemberId(), createReq.getOrderType(), createReq.getGoodsDesc(), createReq.getTransAmt()); - - PaymentResponse result = paymentService.alipayAppPay(createReq); - - if ("FAIL".equals(result.getStatus())) { - return ServerResponse.ok() - .bodyValue(ApiResponse.error(result.getErrorMsg())); - } - - return ServerResponse.ok() - .bodyValue(ApiResponse.success(result)); - }) - .onErrorResume(e -> { - log.error("[Payment] 创建支付订单异常", e); - return ServerResponse.ok() - .bodyValue(ApiResponse.error("创建支付订单失败: " + e.getMessage())); - }); - } - - @Operation(summary = "创建扫码支付订单", description = "创建支付宝扫码支付订单,返回二维码链接") - public Mono createQrCodePayment(ServerRequest request) { - return request.bodyToMono(CreatePaymentRequest.class) - .flatMap(createReq -> { - log.info("[Payment] 创建扫码支付订单: memberId={}, orderType={}, goodsDesc={}, transAmt={}", - createReq.getMemberId(), createReq.getOrderType(), createReq.getGoodsDesc(), createReq.getTransAmt()); - - PaymentResponse result = paymentService.alipayQrCodePay(createReq); - - if ("FAIL".equals(result.getStatus())) { - return ServerResponse.ok() - .bodyValue(ApiResponse.error(result.getErrorMsg())); - } - - return ServerResponse.ok() - .bodyValue(ApiResponse.success(result)); - }) - .onErrorResume(e -> { - log.error("[Payment] 创建扫码支付订单异常", e); - return ServerResponse.ok() - .bodyValue(ApiResponse.error("创建扫码支付订单失败: " + e.getMessage())); - }); - } - - @Operation(summary = "查询支付状态", description = "查询支付订单状态") - public Mono getPaymentStatus(ServerRequest request) { - String orderId = request.pathVariable("orderId"); - log.info("[Payment] 查询支付状态: orderId={}", orderId); - - PaymentResponse result = paymentService.getPaymentStatus(orderId); - return ServerResponse.ok() - .bodyValue(ApiResponse.success(result)); - } - - @Operation(summary = "支付宝异步通知", description = "接收支付宝异步回调通知") - public Mono alipayNotify(ServerRequest request) { - return request.bodyToMono(Map.class) - .flatMap(params -> { - log.info("[Payment] 收到支付宝异步通知: params={}", params); - - // 转换Map类型 - Map notifyParams = new HashMap<>(); - if (params instanceof Map) { - ((Map) params).forEach((key, value) -> - notifyParams.put(String.valueOf(key), String.valueOf(value))); - } - - String result = paymentService.handleAlipayNotify(notifyParams); - return ServerResponse.ok() - .bodyValue(result); - }) - .onErrorResume(e -> { - log.error("[Payment] 异步通知处理异常", e); - return ServerResponse.ok() - .bodyValue("fail"); - }); - } - - @Operation(summary = "申请退款", description = "申请支付退款") - public Mono refundPayment(ServerRequest request) { - String orderId = request.pathVariable("orderId"); - - return request.bodyToMono(Map.class) - .flatMap(body -> { - String refundAmt = String.valueOf(body.get("refundAmt")); - log.info("[Payment] 申请退款: orderId={}, refundAmt={}", orderId, refundAmt); - - boolean success = paymentService.refund(orderId, refundAmt); - if (success) { - return ServerResponse.ok() - .bodyValue(ApiResponse.success("退款申请成功")); - } else { - return ServerResponse.ok() - .bodyValue(ApiResponse.error("退款申请失败")); - } - }) - .onErrorResume(e -> { - log.error("[Payment] 退款异常: orderId={}", orderId, e); - return ServerResponse.ok() - .bodyValue(ApiResponse.error("退款失败: " + e.getMessage())); - }); - } -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/PaymentService.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/PaymentService.java deleted file mode 100644 index 7170ce5..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/PaymentService.java +++ /dev/null @@ -1,59 +0,0 @@ -package cn.novalon.gym.manage.payment.service; - -import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest; -import cn.novalon.gym.manage.payment.dto.PaymentResponse; - -/** - * 支付服务接口 - */ -public interface PaymentService { - - /** - * 创建支付订单 - * - * @param request 创建支付请求 - * @return 支付响应 - */ - PaymentResponse createPayment(CreatePaymentRequest request); - - /** - * 查询支付状态 - * - * @param orderId 订单ID - * @return 支付响应 - */ - PaymentResponse getPaymentStatus(String orderId); - - /** - * 支付宝App支付 - * - * @param request 创建支付请求 - * @return 支付响应 - */ - PaymentResponse alipayAppPay(CreatePaymentRequest request); - - /** - * 支付宝扫码支付(二维码支付) - * - * @param request 创建支付请求 - * @return 支付响应(包含二维码链接) - */ - PaymentResponse alipayQrCodePay(CreatePaymentRequest request); - - /** - * 处理支付宝异步通知 - * - * @param params 通知参数 - * @return 处理结果 - */ - String handleAlipayNotify(java.util.Map params); - - /** - * 申请退款 - * - * @param orderId 订单ID - * @param refundAmt 退款金额(分) - * @return 退款结果 - */ - boolean refund(String orderId, String refundAmt); -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java deleted file mode 100644 index 2731d27..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java +++ /dev/null @@ -1,445 +0,0 @@ -package cn.novalon.gym.manage.payment.service.impl; - -import cn.novalon.gym.manage.common.util.RedisUtil; -import cn.novalon.gym.manage.payment.config.AlipayProperties; -import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest; -import cn.novalon.gym.manage.payment.dto.PaymentResponse; -import cn.novalon.gym.manage.payment.service.PaymentService; -import com.alipay.api.AlipayApiException; -import com.alipay.api.AlipayClient; -import com.alipay.api.internal.util.AlipaySignature; -import com.alipay.api.request.AlipayTradeAppPayRequest; -import com.alipay.api.request.AlipayTradePrecreateRequest; -import com.alipay.api.request.AlipayTradeQueryRequest; -import com.alipay.api.request.AlipayTradeRefundRequest; -import com.alipay.api.response.AlipayTradeAppPayResponse; -import com.alipay.api.response.AlipayTradePrecreateResponse; -import com.alipay.api.response.AlipayTradeQueryResponse; -import com.alipay.api.response.AlipayTradeRefundResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 支付服务实现 - * App支付 (alipay.trade.app.pay) - */ -@Slf4j -@Service -public class PaymentServiceImpl implements PaymentService { - - private static final String PROCESSED_TRADE_PREFIX = "alipay:processed:trade:"; - private static final String REFUND_ORDER_PREFIX = "alipay:refund:order:"; - private static final String ORDER_CACHE_PREFIX = "alipay:order:"; - private static final long IDEMPOTENT_EXPIRE_SECONDS = 86400; // 24小时过期 - - private final AlipayClient alipayClient; - private final AlipayProperties alipayProperties; - private final RedisUtil redisUtil; - - /** - * 订单本地缓存 (作为Redis的二级缓存) - */ - private final Map localOrderCache = new ConcurrentHashMap<>(); - - public PaymentServiceImpl(AlipayClient alipayClient, AlipayProperties alipayProperties, RedisUtil redisUtil) { - this.alipayClient = alipayClient; - this.alipayProperties = alipayProperties; - this.redisUtil = redisUtil; - } - - @Override - public PaymentResponse createPayment(CreatePaymentRequest request) { - // App支付使用 alipay.trade.app.pay - return alipayAppPay(request); - } - - @Override - public PaymentResponse getPaymentStatus(String orderId) { - // 先查本地缓存 - PaymentResponse cached = localOrderCache.get(orderId); - if (cached != null && "SUCCESS".equals(cached.getStatus())) { - return cached; - } - - // 查Redis缓存 - PaymentResponse redisCached = getOrderFromRedis(orderId); - if (redisCached != null && "SUCCESS".equals(redisCached.getStatus())) { - localOrderCache.put(orderId, redisCached); - return redisCached; - } - - try { - AlipayTradeQueryRequest queryRequest = new AlipayTradeQueryRequest(); - queryRequest.setBizContent("{\"out_trade_no\":\"" + orderId + "\"}"); - - AlipayTradeQueryResponse response = alipayClient.execute(queryRequest); - if (response.isSuccess()) { - Date sendPayDate = response.getSendPayDate(); - String payTimeStr = sendPayDate != null ? new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(sendPayDate) : null; - - PaymentResponse result = PaymentResponse.builder() - .orderId(orderId) - .status(response.getTradeStatus()) - .payTime(payTimeStr) - .transAmt(response.getTotalAmount()) - .build(); - - if ("TRADE_SUCCESS".equals(response.getTradeStatus())) { - result.setStatus("SUCCESS"); - // 更新缓存 - saveOrderToRedis(orderId, result); - localOrderCache.put(orderId, result); - } - return result; - } - } catch (AlipayApiException e) { - log.error("查询支付状态失败: orderId={}", orderId, e); - } - - return PaymentResponse.builder() - .orderId(orderId) - .status(cached != null ? cached.getStatus() : (redisCached != null ? redisCached.getStatus() : "PENDING")) - .build(); - } - - @Override - public PaymentResponse alipayAppPay(CreatePaymentRequest request) { - // 生成订单号 - String orderId = generateOrderId(); - - try { - AlipayTradeAppPayRequest payRequest = new AlipayTradeAppPayRequest(); - payRequest.setNotifyUrl(alipayProperties.getNotifyUrl()); - - // 构建业务参数 (测试金额:固定1分钱) - String bizContent = "{\"out_trade_no\":\"" + orderId + "\"," - + "\"total_amount\":\"0.01\"," - + "\"subject\":\"" + request.getGoodsDesc() + "\"," - + "\"product_code\":\"QUICK_MSECURITY_PAY\"," - + "\"timeout_express\":\"30m\"}"; - - log.info("[Alipay] App支付请求: orderId={}, bizContent={}", orderId, bizContent); - - payRequest.setBizContent(bizContent); - - AlipayTradeAppPayResponse response = alipayClient.sdkExecute(payRequest); - - if (response.isSuccess()) { - String payUrl = response.getBody(); - - // App唤起支付宝的scheme格式: alipay://... - // 实际使用时,前端使用 plus.runtime.openURL(payUrl) 唤起支付宝 - log.info("[Alipay] App支付创建成功: orderId={}, payUrl={}", orderId, payUrl); - - PaymentResponse result = PaymentResponse.builder() - .orderId(orderId) - .status("PENDING") - .payUrl(payUrl) - .payInfo(response.getBody()) - .goodsDesc(request.getGoodsDesc()) - .transAmt(request.getTransAmt()) - .tradeType(request.getTradeType()) - .build(); - - // 缓存订单到Redis和本地 - saveOrderToRedis(orderId, result); - localOrderCache.put(orderId, result); - - return result; - } else { - log.error("[Alipay] App支付创建失败: orderId={}, error={}", orderId, response.getMsg()); - return PaymentResponse.builder() - .orderId(orderId) - .status("FAIL") - .errorCode(response.getCode()) - .errorMsg(response.getMsg()) - .build(); - } - } catch (AlipayApiException e) { - log.error("[Alipay] App支付异常: orderId={}", orderId, e); - String errCode = e.getErrCode() != null ? e.getErrCode() : "NETWORK_ERROR"; - String errMsg = e.getErrMsg() != null ? e.getErrMsg() : extractErrorMessage(e); - return PaymentResponse.builder() - .orderId(orderId) - .status("FAIL") - .errorCode(errCode) - .errorMsg(errMsg) - .build(); - } - } - - /** - * 支付宝扫码支付 (alipay.trade.precreate) - * 生成付款二维码,用户使用支付宝扫码完成支付 - */ - @Override - public PaymentResponse alipayQrCodePay(CreatePaymentRequest request) { - String orderId = generateOrderId(); - - try { - AlipayTradePrecreateRequest precreateRequest = new AlipayTradePrecreateRequest(); - precreateRequest.setNotifyUrl(alipayProperties.getNotifyUrl()); - - // 构建业务参数 - String bizContent = "{\"out_trade_no\":\"" + orderId + "\"," - + "\"total_amount\":\"0.01\"," - + "\"subject\":\"" + request.getGoodsDesc() + "\"," - + "\"timeout_express\":\"30m\"}"; - - log.info("[Alipay] 扫码支付请求: orderId={}, bizContent={}", orderId, bizContent); - - precreateRequest.setBizContent(bizContent); - AlipayTradePrecreateResponse response = alipayClient.execute(precreateRequest); - - if (response.isSuccess()) { - String qrCode = response.getQrCode(); // 二维码链接 - - log.info("[Alipay] 扫码支付创建成功: orderId={}, qrCode={}", orderId, qrCode); - - PaymentResponse result = PaymentResponse.builder() - .orderId(orderId) - .status("PENDING") - .qrCode(qrCode) - .payInfo(response.getBody()) - .goodsDesc(request.getGoodsDesc()) - .transAmt(request.getTransAmt()) - .tradeType("QRCODE") - .build(); - - // 缓存订单到Redis和本地 - saveOrderToRedis(orderId, result); - localOrderCache.put(orderId, result); - - return result; - } else { - log.error("[Alipay] 扫码支付创建失败: orderId={}, error={}", orderId, response.getMsg()); - return PaymentResponse.builder() - .orderId(orderId) - .status("FAIL") - .errorCode(response.getCode()) - .errorMsg(response.getMsg()) - .build(); - } - } catch (AlipayApiException e) { - log.error("[Alipay] 扫码支付异常: orderId={}", orderId, e); - String errCode = e.getErrCode() != null ? e.getErrCode() : "NETWORK_ERROR"; - String errMsg = e.getErrMsg() != null ? e.getErrMsg() : extractErrorMessage(e); - return PaymentResponse.builder() - .orderId(orderId) - .status("FAIL") - .errorCode(errCode) - .errorMsg(errMsg) - .build(); - } - } - - @Override - public String handleAlipayNotify(Map params) { - String outTradeNo = params.get("out_trade_no"); - - try { - // 1. 幂等处理:检查是否已处理过该通知(使用Redis) - if (isTradeProcessed(outTradeNo)) { - log.info("[Alipay] 幂等处理:通知已处理,跳过, outTradeNo={}", outTradeNo); - return "success"; - } - - // 2. 验签 - boolean signVerified = AlipaySignature.rsaCheckV1( - params, - alipayProperties.getAlipayPublicKey(), - alipayProperties.getCharset(), - alipayProperties.getSignType() - ); - - if (!signVerified) { - log.warn("[Alipay] 异步通知验签失败: outTradeNo={}", outTradeNo); - return "fail"; - } - - // 3. 关键信息校验 - // 校验 app_id 是否匹配 - String notifyAppId = params.get("app_id"); - if (!alipayProperties.getAppId().equals(notifyAppId)) { - log.warn("[Alipay] app_id 不匹配: expected={}, actual={}", alipayProperties.getAppId(), notifyAppId); - return "fail"; - } - - // 校验卖家ID (seller_id) - String sellerId = params.get("seller_id"); - if (sellerId != null && !sellerId.isEmpty()) { - // 商家ID校验逻辑根据实际情况添加 - log.debug("[Alipay] 卖家ID校验: seller_id={}", sellerId); - } - - // 校验交易金额 - String notifyTotalAmount = params.get("total_amount"); - PaymentResponse cachedOrder = getOrderFromRedis(outTradeNo); - if (cachedOrder != null && notifyTotalAmount != null) { - // 金额校验:确保通知金额与订单金额一致(防止金额篡改) - log.debug("[Alipay] 交易金额校验: orderId={}, amount={}", outTradeNo, notifyTotalAmount); - } - - String tradeStatus = params.get("trade_status"); - log.info("[Alipay] 异步通知验签成功: outTradeNo={}, tradeStatus={}", outTradeNo, tradeStatus); - - // 4. 判断交易状态 - if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) { - // 更新订单状态 - if (cachedOrder != null) { - cachedOrder.setStatus("SUCCESS"); - cachedOrder.setPayTime(params.get("gmt_payment")); - saveOrderToRedis(outTradeNo, cachedOrder); - localOrderCache.put(outTradeNo, cachedOrder); - } - - // 5. 标记为已处理(幂等,使用Redis) - markTradeProcessed(outTradeNo); - - return "success"; - } - - return "fail"; - } catch (AlipayApiException e) { - log.error("[Alipay] 异步通知处理异常: outTradeNo={}", outTradeNo, e); - return "fail"; - } - } - - @Override - public boolean refund(String orderId, String refundAmt) { - // 1. 退款幂等检查:防止重复退款(使用Redis) - if (isOrderRefunded(orderId)) { - log.info("[Alipay] 退款幂等:订单已退款过,跳过, orderId={}", orderId); - return true; - } - - try { - AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest(); - refundRequest.setBizContent("{\"out_trade_no\":\"" + orderId + "\",\"refund_amount\":\"" + refundAmt + "\"}"); - - AlipayTradeRefundResponse response = alipayClient.execute(refundRequest); - if (response.isSuccess()) { - // 2. 校验退款是否真正成功:必须判断 fund_change=Y - String fundChange = response.getFundChange(); - if ("Y".equals(fundChange)) { - log.info("[Alipay] 退款成功: orderId={}, refundAmt={}, fundChange={}", orderId, refundAmt, fundChange); - // 标记为已退款(幂等,使用Redis) - markOrderRefunded(orderId); - return true; - } else { - log.warn("[Alipay] 退款返回Y但fund_change!=Y: orderId={}, fundChange={}", orderId, fundChange); - return false; - } - } else { - log.error("[Alipay] 退款失败: orderId={}, error={}", orderId, response.getMsg()); - } - } catch (AlipayApiException e) { - log.error("[Alipay] 退款异常: orderId={}", orderId, e); - } - return false; - } - - // ==================== Redis操作方法 ==================== - - /** - * 检查交易是否已处理(幂等) - */ - private boolean isTradeProcessed(String outTradeNo) { - try { - return Boolean.TRUE.equals(redisUtil.hasKey(PROCESSED_TRADE_PREFIX + outTradeNo).block()); - } catch (Exception e) { - log.error("[Alipay] 检查交易处理状态异常: outTradeNo={}", outTradeNo, e); - return false; - } - } - - /** - * 标记交易已处理(幂等) - */ - private void markTradeProcessed(String outTradeNo) { - try { - redisUtil.setWithExpire(PROCESSED_TRADE_PREFIX + outTradeNo, "1", IDEMPOTENT_EXPIRE_SECONDS).block(); - } catch (Exception e) { - log.error("[Alipay] 标记交易处理状态异常: outTradeNo={}", outTradeNo, e); - } - } - - /** - * 检查订单是否已退款(幂等) - */ - private boolean isOrderRefunded(String orderId) { - try { - return Boolean.TRUE.equals(redisUtil.hasKey(REFUND_ORDER_PREFIX + orderId).block()); - } catch (Exception e) { - log.error("[Alipay] 检查订单退款状态异常: orderId={}", orderId, e); - return false; - } - } - - /** - * 标记订单已退款(幂等) - */ - private void markOrderRefunded(String orderId) { - try { - redisUtil.setWithExpire(REFUND_ORDER_PREFIX + orderId, "1", IDEMPOTENT_EXPIRE_SECONDS).block(); - } catch (Exception e) { - log.error("[Alipay] 标记订单退款状态异常: orderId={}", orderId, e); - } - } - - /** - * 从Redis获取订单 - */ - private PaymentResponse getOrderFromRedis(String orderId) { - try { - return redisUtil.get(ORDER_CACHE_PREFIX + orderId, PaymentResponse.class).block(); - } catch (Exception e) { - log.error("[Alipay] 获取Redis订单异常: orderId={}", orderId, e); - return null; - } - } - - /** - * 保存订单到Redis - */ - private void saveOrderToRedis(String orderId, PaymentResponse response) { - try { - redisUtil.setWithExpire(ORDER_CACHE_PREFIX + orderId, response, IDEMPOTENT_EXPIRE_SECONDS).block(); - } catch (Exception e) { - log.error("[Alipay] 保存订单到Redis异常: orderId={}", orderId, e); - } - } - - /** - * 生成订单号 - */ - private String generateOrderId() { - String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); - String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8); - return timestamp + uuid; - } - - /** - * 提取异常中的错误信息(兜底) - */ - private String extractErrorMessage(AlipayApiException e) { - Throwable cause = e.getCause(); - if (cause != null && cause.getMessage() != null && cause.getMessage().contains("504")) { - return "支付宝沙箱网关连接超时(504),请检查服务器网络能否访问 openapi-sandbox.dl.alipaydev.com"; - } - if (cause != null) { - return "网络异常: " + cause.getClass().getSimpleName() + " - " + (cause.getMessage() != null ? cause.getMessage().substring(0, Math.min(cause.getMessage().length(), 200)) : ""); - } - if (e.getMessage() != null) { - return "支付宝接口异常: " + (e.getMessage().length() > 200 ? e.getMessage().substring(0, 200) : e.getMessage()); - } - return "支付宝接口调用失败,请稍后重试"; - } -} diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index ed55a4e..7a15e65 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -48,11 +48,6 @@ gym-checkIn ${project.version} - - cn.novalon.gym.manage - gym-payment - ${project.version} - cn.novalon.gym.manage gym-dataCount diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 69441c7..8746a4a 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -47,7 +47,6 @@ gym-checkIn gym-dataCount gym-auth - gym-payment -- 2.52.0