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 diff --git a/gym-manage-api/gym-auth/pom.xml b/gym-manage-api/gym-auth/pom.xml index bff30df..fe2c32b 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,25 +30,26 @@ 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.boot - spring-boot-starter-security + spring-boot-starter-validation org.springframework.data @@ -83,45 +85,25 @@ 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/impl/PhoneAuthServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java index 4bd5050..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,63 +45,19 @@ 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 @@ -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/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/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 beeccbe..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 @@ -63,7 +58,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 482d931..2a4da75 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 51a74d2..f430e72 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/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-已删除'; 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-微信授权'; 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