From af707e1bb4ecba94e30d0e8caaef63de5f7003ba Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 9 Jun 2026 17:27:44 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AD=BE=E5=88=B0?= =?UTF-8?q?=E8=A1=A8=E8=BF=81=E7=A7=BB=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V13__Create_sign_in_record_table.sql | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql new file mode 100644 index 0000000..98d3425 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql @@ -0,0 +1,74 @@ +-- ============================================ +-- 会员到店签到记录表 +-- 版本: V13 +-- 描述: 创建sign_in_record表,用于记录会员签到信息 +-- ============================================ + +-- 创建签到记录表 +CREATE TABLE IF NOT EXISTS sign_in_record ( + id BIGSERIAL PRIMARY KEY, -- 自增主键 + member_id BIGINT NOT NULL, -- 会员ID,关联member表 + 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, -- JSON格式,存储会员卡验证时的快照数据 + 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-已删除 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -- 记录更新时间 +); + +-- 创建索引 +-- 会员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); + +-- 添加表注释 +COMMENT ON TABLE sign_in_record IS '会员到店签到记录表'; + +-- 添加字段注释 +COMMENT ON COLUMN sign_in_record.id IS '自增主键'; +COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member表'; +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-已删除'; +COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间'; +COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间'; -- 2.52.0 From 3586a7d74be3a847be6d8e5eea789a4c6cf04ef6 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 23 Jun 2026 22:47:35 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/KeyAnalyzer.java | 202 +++++ gym-manage-api/META-INF/MANIFEST.MF | 6 + .../gym-payment/pom.properties | 3 + .../cn.novalon.gym.manage/gym-payment/pom.xml | 85 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + gym-manage-api/gym-auth/pom.xml | 123 +++ .../gym/manage/auth/config/AuthConfig.java | 7 + .../gym/manage/auth/config/SmsProperties.java | 47 ++ .../manage/auth/dto/PhoneCodeLoginDto.java | 26 + .../gym/manage/auth/dto/PhoneLoginDto.java | 27 + .../gym/manage/auth/dto/SendCodeRequest.java | 22 + .../gym/manage/auth/dto/SmsLoginDto.java | 25 + .../manage/auth/handler/PhoneAuthHandler.java | 58 ++ .../manage/auth/service/PhoneAuthService.java | 37 + .../gym/manage/auth/service/SmsService.java | 35 + .../service/impl/PhoneAuthServiceImpl.java | 177 +++++ .../auth/service/impl/SmsServiceImpl.java | 148 ++++ .../gym/manage/auth/vo/PhoneLoginVO.java | 36 + ...ot.autoconfigure.AutoConfiguration.imports | 5 + .../gym-checkIn/spotbugs-exclude.xml | 12 + gym-manage-api/gym-dataCount/pom.xml | 16 +- gym-manage-api/gym-groupCourse/pom.xml | 21 +- .../gym/manage/groupcourse/util/OSSUtil.java | 46 +- .../gym-member/spotbugs-exclude.xml | 12 + .../handler/MemberCardRecordHandler.java | 1 + .../manage/member/handler/MemberHandler.java | 8 +- .../service/impl/MemberServiceImpl.java | 34 +- gym-manage-api/gym-payment/pom.xml | 95 +++ .../gym/manage/payment/config/CorsConfig.java | 23 + .../manage/payment/config/HuifuPayConfig.java | 34 + .../dto/request/PaymentCreateRequest.java | 58 ++ .../dto/response/PaymentCreateResponse.java | 28 + .../dto/response/PaymentQueryResponse.java | 30 + .../dto/response/PaymentRefundResponse.java | 22 + .../payment/handler/PaymentHandler.java | 78 ++ .../payment/service/IPaymentService.java | 19 + .../service/impl/PaymentServiceImpl.java | 723 ++++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + gym-manage-api/manage-app/pom.xml | 5 + .../gym/manage/app/ManageApplication.java | 3 +- .../gym/manage/app/config/SystemRouter.java | 13 +- .../src/main/resources/application-dev.yml | 14 +- .../src/main/resources/application-local.yml | 8 +- .../src/main/resources/application-test.yml | 6 +- .../src/main/resources/application.yml | 26 +- .../src/main/resources/keys.properties | 2 + .../src/main/resources/member-config.yml | 15 +- .../common/constant/RedisKeyConstants.java | 9 + .../manage/common/exception/ErrorCode.java | 4 + .../db/entity/SysUserMessageEntity.java | 19 +- .../repository/SysUserMessageRepository.java | 85 ++ .../filter/JwtAuthenticationFilter.java | 4 +- .../notify/core/domain/SysUserMessage.java | 4 + .../repository/ISysUserMessageRepository.java | 8 + .../core/service/ISysUserMessageService.java | 8 + .../impl/SysUserMessageServiceImpl.java | 17 + .../notify/handler/SysUserMessageHandler.java | 43 ++ .../gym/manage/sys/config/SecurityConfig.java | 8 +- .../gym/manage/sys/TokenGenerator.java | 46 ++ gym-manage-api/pom.xml | 2 + gym-manage-api/test-jar/META-INF/MANIFEST.MF | 6 + .../gym-payment/pom.properties | 3 + .../cn.novalon.gym.manage/gym-payment/pom.xml | 85 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + vue | 1 + 65 files changed, 2694 insertions(+), 82 deletions(-) create mode 100644 gym-manage-api/KeyAnalyzer.java create mode 100644 gym-manage-api/META-INF/MANIFEST.MF create mode 100644 gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties create mode 100644 gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml create mode 100644 gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 gym-manage-api/gym-auth/pom.xml create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java create mode 100644 gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java create mode 100644 gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 gym-manage-api/gym-checkIn/spotbugs-exclude.xml create mode 100644 gym-manage-api/gym-member/spotbugs-exclude.xml create mode 100644 gym-manage-api/gym-payment/pom.xml create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java create mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java create mode 100644 gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 gym-manage-api/manage-app/src/main/resources/keys.properties create mode 100644 gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/TokenGenerator.java create mode 100644 gym-manage-api/test-jar/META-INF/MANIFEST.MF create mode 100644 gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties create mode 100644 gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml create mode 100644 gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 160000 vue diff --git a/gym-manage-api/KeyAnalyzer.java b/gym-manage-api/KeyAnalyzer.java new file mode 100644 index 0000000..e460fe7 --- /dev/null +++ b/gym-manage-api/KeyAnalyzer.java @@ -0,0 +1,202 @@ +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/META-INF/MANIFEST.MF b/gym-manage-api/META-INF/MANIFEST.MF new file mode 100644 index 0000000..19b83f2 --- /dev/null +++ b/gym-manage-api/META-INF/MANIFEST.MF @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +Created-By: Maven JAR Plugin 3.4.2 +Build-Jdk-Spec: 21 +Implementation-Title: Gym Payment +Implementation-Version: 1.0.0 + diff --git a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties new file mode 100644 index 0000000..e4cde16 --- /dev/null +++ b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties @@ -0,0 +1,3 @@ +artifactId=gym-payment +groupId=cn.novalon.gym.manage +version=1.0.0 diff --git a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml new file mode 100644 index 0000000..39709f1 --- /dev/null +++ b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-payment + jar + + Gym Payment + Payment Module - Integrates Huifu Payment Gateway + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + cn.hutool + hutool-all + 5.8.25 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..691774a --- /dev/null +++ b/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.novalon.gym.manage.payment.config.HuifuPayConfig diff --git a/gym-manage-api/gym-auth/pom.xml b/gym-manage-api/gym-auth/pom.xml new file mode 100644 index 0000000..42aa5be --- /dev/null +++ b/gym-manage-api/gym-auth/pom.xml @@ -0,0 +1,123 @@ + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-auth + jar + + Gym Auth + Phone Authentication Module - Phone Number Login Services + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + cn.novalon.gym.manage + manage-db + ${project.version} + + + cn.novalon.gym.manage + manage-sys + ${project.version} + + + cn.novalon.gym.manage + gym-member + ${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 + + + com.aliyun + aliyun-java-sdk-core + 4.6.0 + + + com.aliyun + dysmsapi20170525 + 2.0.0 + + + + + + + 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} + + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..d8d842d --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..fe9e35c --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java @@ -0,0 +1,47 @@ +package cn.novalon.gym.manage.auth.config; + +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; + + /** + * 短信验证码有效期(秒,默认300秒=5分钟) + */ + private long codeExpireSeconds = 300; +} 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 new file mode 100644 index 0000000..4d21283 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java @@ -0,0 +1,26 @@ +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 PhoneCodeLoginDto { + + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + @NotBlank(message = "验证码不能为空") + @Pattern(regexp = "^\\d{4,6}$", message = "验证码格式不正确") + private String code; +} \ No newline at end of file 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 new file mode 100644 index 0000000..1666761 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java @@ -0,0 +1,27 @@ +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官方一键登录流程:前端通过云函数获取手机号,直接传给后端 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PhoneLoginDto { + + @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 new file mode 100644 index 0000000..c16602d --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..d97cff5 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..af46968 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java @@ -0,0 +1,58 @@ +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.service.PhoneAuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +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 java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +@Tag(name = "手机号认证", description = "手机号一键登录与验证码登录") +public class PhoneAuthHandler { + + private final PhoneAuthService phoneAuthService; + + @Operation(summary = "手机号一键登录", description = "使用uniapp官方运营商认证,直接手机号登录或注册") + public Mono oneClickLogin(ServerRequest request) { + log.info("收到手机号一键登录请求"); + + return request.bodyToMono(PhoneLoginDto.class) + .flatMap(phoneAuthService::oneClickLogin) + .flatMap(response -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response)); + } + + @Operation(summary = "发送短信验证码", description = "使用阿里云发送短信验证码") + public Mono sendSmsCode(ServerRequest request) { + log.info("收到发送短信验证码请求"); + + 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 ? "验证码发送成功" : "验证码发送失败"))); + } + + @Operation(summary = "手机号验证码登录", description = "使用阿里云短信验证码登录或注册") + public Mono codeLogin(ServerRequest request) { + log.info("收到手机号验证码登录请求"); + + return request.bodyToMono(PhoneCodeLoginDto.class) + .flatMap(phoneAuthService::codeLogin) + .flatMap(response -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(response)); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..2407a9b --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java @@ -0,0 +1,37 @@ +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.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 new file mode 100644 index 0000000..914f4ff --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java @@ -0,0 +1,35 @@ +package cn.novalon.gym.manage.auth.service; + +/** + * 短信服务接口 + * + * @author auto-generated + * @date 2026-06-20 + */ +public interface SmsService { + + /** + * 发送短信验证码 + * + * @param phone 手机号 + * @return 发送结果 + */ + boolean sendVerificationCode(String phone); + + /** + * 验证短信验证码 + * + * @param phone 手机号 + * @param code 验证码 + * @return 验证结果 + */ + boolean verifyCode(String phone, String code); + + /** + * 获取验证码(用于测试或特殊场景) + * + * @param phone 手机号 + * @return 验证码 + */ + String getVerificationCode(String phone); +} 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 new file mode 100644 index 0000000..2ad6588 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java @@ -0,0 +1,177 @@ +package cn.novalon.gym.manage.auth.service.impl; + +import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; +import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; +import cn.novalon.gym.manage.auth.service.PhoneAuthService; +import cn.novalon.gym.manage.auth.service.SmsService; +import cn.novalon.gym.manage.auth.vo.PhoneLoginVO; +import cn.novalon.gym.manage.common.exception.ErrorCode; +import cn.novalon.gym.manage.common.exception.SystemException; +import cn.novalon.gym.manage.member.entity.Member; +import cn.novalon.gym.manage.member.es.entity.MemberES; +import cn.novalon.gym.manage.member.es.repository.MemberESRepository; +import cn.novalon.gym.manage.member.repository.IMemberRepository; +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 jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PhoneAuthServiceImpl implements PhoneAuthService { + + private final IMemberRepository memberRepository; + private final MemberESRepository memberESRepository; + private final EsSyncUtils esSyncUtils; + private final JwtTokenProvider jwtTokenProvider; + private final SmsService smsService; + + private EsSyncUtils.EntitySyncer memberSyncer; + + @PostConstruct + public void init() { + this.memberSyncer = esSyncUtils.bind(Member.class, MemberES.class, memberESRepository); + } + + @Override + public Mono oneClickLogin(PhoneLoginDto request) { + log.info("手机号一键登录, phone: {}", request.getPhone()); + + 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); + })); + } + + @Override + public Mono sendSmsCode(String phone) { + log.info("发送短信验证码, phone: {}", phone); + return Mono.fromCallable(() -> smsService.sendVerificationCode(phone)); + } + + @Override + public Mono codeLogin(PhoneCodeLoginDto request) { + log.info("手机号验证码登录, phone: {}", request.getPhone()); + + return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode())) + .flatMap(verified -> { + if (!verified) { + log.warn("验证码验证失败, phone: {}", request.getPhone()); + return Mono.error(new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "验证码错误或已过期")); + } + + String encryptedPhone = encryptPhone(request.getPhone()); + + return memberRepository.findByPhone(encryptedPhone) + .flatMap(existingMember -> { + log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); + return doLogin(existingMember, false, null); + }) + .switchIfEmpty(Mono.defer(() -> { + log.info("手机号未注册,创建新会员"); + PhoneLoginDto registerRequest = new PhoneLoginDto(); + registerRequest.setPhone(request.getPhone()); + return createNewMemberAndLogin(registerRequest, encryptedPhone); + })); + }); + } + + private Mono createNewMemberAndLogin(PhoneLoginDto request, String encryptedPhone) { + String memberNo = MemberNoGenerator.generate(); + log.info("生成会员号: {}", memberNo); + + Member member = new Member(); + member.setMemberNo(memberNo); + member.setPhone(encryptedPhone); + member.setNickname(request != null ? request.getNickname() : null); + member.setAvatar(request != null ? request.getAvatar() : null); + member.setLastLoginAt(LocalDateTime.now()); + member.setIsDeleted(false); + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .flatMap(savedMember -> { + log.info("新会员创建成功, memberId: {}, memberNo: {}", savedMember.getId(), savedMember.getMemberNo()); + return doLogin(savedMember, true, request); + }); + } + + private Mono doLogin(Member member, boolean isNewUser, PhoneLoginDto request) { + log.info("登录成功, memberId: {}", member.getId()); + + member.setLastLoginAt(LocalDateTime.now()); + + if (isNewUser && request != null) { + if ((member.getNickname() == null || member.getNickname().isEmpty()) + && request.getNickname() != null && !request.getNickname().isEmpty()) { + member.setNickname(request.getNickname()); + } + if ((member.getAvatar() == null || member.getAvatar().isEmpty()) + && request.getAvatar() != null && !request.getAvatar().isEmpty()) { + member.setAvatar(request.getAvatar()); + } + } + + return memberRepository.save(member) + .doOnSuccess(memberSyncer::sync) + .map(this::buildLoginResponse); + } + + private PhoneLoginVO buildLoginResponse(Member member) { + boolean needCompleteInfo = member.getNickname() == null || member.getNickname().isEmpty(); + + List roles = new ArrayList<>(); + String accessToken = jwtTokenProvider.generateToken(String.valueOf(member.getId()), member.getId(), roles); + + log.info("JWT Token 生成成功, memberId: {}", member.getId()); + + PhoneLoginVO vo = new PhoneLoginVO(); + vo.setMemberId(member.getId()); + vo.setMemberNo(member.getMemberNo()); + vo.setAccessToken(accessToken); + vo.setRefreshToken(accessToken); + vo.setExpiresIn(86400); + vo.setIsNewUser(member.getCreatedAt() == null ? false : + member.getCreatedAt().isAfter(LocalDateTime.now().minusMinutes(1))); + vo.setNeedCompleteInfo(needCompleteInfo); + vo.setNickname(member.getNickname()); + vo.setAvatar(member.getAvatar()); + vo.setPhone(decryptPhone(member.getPhone())); + return vo; + } + + private String encryptPhone(String phoneNumber) { + try { + return AesUtil.encrypt(phoneNumber); + } catch (Exception e) { + log.error("手机号加密失败", e); + throw new SystemException(ErrorCode.SYSTEM_INTERNAL_ERROR, "手机号加密失败"); + } + } + + private String decryptPhone(String encryptedPhone) { + try { + return AesUtil.decrypt(encryptedPhone); + } catch (Exception e) { + log.error("手机号解密失败", e); + 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 new file mode 100644 index 0000000..2162e13 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java @@ -0,0 +1,148 @@ +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * 短信服务实现类 + * + * @author auto-generated + * @date 2026-06-20 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SmsServiceImpl implements SmsService { + + private final SmsProperties smsProperties; + private final RedisUtil redisUtil; + + @Override + public boolean sendVerificationCode(String phone) { + log.info("发送短信验证码, phone: {}", phone); + + try { + // 生成验证码 + String code = generateCode(); + + // 发送短信 + boolean sent = sendSms(phone, code); + + 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(); + + log.info("短信验证码发送成功, phone: {}", phone); + return true; + } + + log.warn("短信验证码发送失败, phone: {}", phone); + return false; + + } catch (Exception e) { + log.error("发送短信验证码异常, phone: {}", phone, e); + return false; + } + } + + @Override + public boolean verifyCode(String phone, String code) { + log.info("验证短信验证码, phone: {}", phone); + + try { + String cacheKey = RedisKeyConstants.SMS_CODE + phone; + String cachedCode = redisUtil.get(cacheKey, String.class).block(); + + if (cachedCode == null) { + log.warn("验证码已过期或不存在, phone: {}", phone); + 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) { + try { + String cacheKey = RedisKeyConstants.SMS_CODE + phone; + return redisUtil.get(cacheKey, String.class).block(); + } catch (Exception e) { + log.error("获取验证码异常, phone: {}", phone, e); + return null; + } + } + + /** + * 生成验证码 + */ + private String generateCode() { + return RandomUtil.randomNumbers(smsProperties.getCodeLength()); + } + + /** + * 发送短信 + */ + 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; + } +} 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 new file mode 100644 index 0000000..f5f4651 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java @@ -0,0 +1,36 @@ +package cn.novalon.gym.manage.auth.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 手机号一键登录响应VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PhoneLoginVO { + + private Long memberId; + + private String memberNo; + + private String phone; + + private String accessToken; + + private String refreshToken; + + private Integer expiresIn; + + private Boolean isNewUser; + + private Boolean needCompleteInfo; + + private String nickname; + + private String avatar; +} \ No newline at end of file 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 new file mode 100644 index 0000000..216a5f9 --- /dev/null +++ b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,5 @@ +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-checkIn/spotbugs-exclude.xml b/gym-manage-api/gym-checkIn/spotbugs-exclude.xml new file mode 100644 index 0000000..369b0aa --- /dev/null +++ b/gym-manage-api/gym-checkIn/spotbugs-exclude.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/pom.xml b/gym-manage-api/gym-dataCount/pom.xml index 9e8ddec..32e1c3f 100644 --- a/gym-manage-api/gym-dataCount/pom.xml +++ b/gym-manage-api/gym-dataCount/pom.xml @@ -100,8 +100,20 @@ - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + diff --git a/gym-manage-api/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index bfafbbb..2ef9fd2 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -35,11 +35,6 @@ manage-common ${project.version} - - cn.novalon.gym.manage - manage-sys - ${project.version} - cn.novalon.gym.manage manage-db @@ -86,8 +81,8 @@ gym-member ${project.version} - - + + com.google.zxing core @@ -98,20 +93,14 @@ javase 3.5.3 - - - - com.aliyun.oss - aliyun-sdk-oss - 3.17.1 - - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java index 391e27f..c3cd21b 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java @@ -1,8 +1,8 @@ package cn.novalon.gym.manage.groupcourse.util; -import com.aliyun.oss.OSS; -import com.aliyun.oss.OSSClientBuilder; -import com.aliyun.oss.model.PutObjectRequest; +//import com.aliyun.oss.OSS; +//import com.aliyun.oss.OSSClientBuilder; +//import com.aliyun.oss.model.PutObjectRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,24 +14,24 @@ import java.time.format.DateTimeFormatter; * 阿里云OSS工具类 */ public class OSSUtil { - + private static final Logger logger = LoggerFactory.getLogger(OSSUtil.class); - + // OSS配置信息 private static final String ENDPOINT = "oss-cn-beijing.aliyuncs.com"; private static final String ACCESS_KEY_ID = "LTAI5t9TFh9Vayeahz45kZjg"; private static final String ACCESS_KEY_SECRET = "zD6NlCeH5UhjBs4vnQVqn8Ksi3CaZz"; private static final String BUCKET_NAME = "ycc-filesaver"; - + // OSS访问地址前缀 private static final String OSS_URL_PREFIX = "https://" + BUCKET_NAME + "." + ENDPOINT + "/"; - + // 文件存储目录 private static final String QRCODE_DIR = "qrcode/"; - + /** * 上传文件到阿里云OSS - * + * * @param localFilePath 本地文件路径 * @param fileName 文件名(不含路径) * @return OSS访问地址 @@ -41,22 +41,22 @@ public class OSSUtil { try { // 创建OSS客户端 ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET); - + // 构建OSS文件路径:qrcode/2026/06/18/xxx.png String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); String ossFilePath = QRCODE_DIR + datePath + "/" + fileName; - + // 创建上传请求 PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath)); - + // 上传文件 ossClient.putObject(putObjectRequest); - + // 构建访问地址 String accessUrl = OSS_URL_PREFIX + ossFilePath; - + logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl); - + return accessUrl; } catch (Exception e) { logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e); @@ -67,10 +67,10 @@ public class OSSUtil { } } } - + /** * 上传文件到阿里云OSS(自定义存储路径) - * + * * @param localFilePath 本地文件路径 * @param ossDirectory OSS存储目录 * @param fileName 文件名(不含路径) @@ -81,21 +81,21 @@ public class OSSUtil { try { // 创建OSS客户端 ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET); - + // 构建OSS文件路径 String ossFilePath = ossDirectory + fileName; - + // 创建上传请求 PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, ossFilePath, new File(localFilePath)); - + // 上传文件 ossClient.putObject(putObjectRequest); - + // 构建访问地址 String accessUrl = OSS_URL_PREFIX + ossFilePath; - + logger.info("文件上传到OSS成功: localPath={}, ossUrl={}", localFilePath, accessUrl); - + return accessUrl; } catch (Exception e) { logger.error("文件上传到OSS失败 - localPath: {}, error: {}", localFilePath, e.getMessage(), e); diff --git a/gym-manage-api/gym-member/spotbugs-exclude.xml b/gym-manage-api/gym-member/spotbugs-exclude.xml new file mode 100644 index 0000000..369b0aa --- /dev/null +++ b/gym-manage-api/gym-member/spotbugs-exclude.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java index 6d5b091..bc3669d 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberCardRecordHandler.java @@ -32,6 +32,7 @@ public class MemberCardRecordHandler { @Operation(summary = "购买会员卡", description = "支持时长卡、次卡、储值卡,自动设置到期提醒") public Mono purchaseCard(ServerRequest request) { + return request.bodyToMono(PurchaseRequest.class) .flatMap(body -> memberCardService.purchaseCard( body.getMemberId(), diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java index baa75f7..977ca11 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/handler/MemberHandler.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.handler; +import cn.novalon.gym.manage.common.exception.NotFoundException; import cn.novalon.gym.manage.member.config.WechatProperties; import cn.novalon.gym.manage.member.dto.AdminUpdatePhoneDto; import cn.novalon.gym.manage.member.dto.SearchMemberDto; @@ -16,6 +17,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; @@ -50,7 +52,11 @@ public class MemberHandler { return memberService.getMemberInfo(memberId) .flatMap(info -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) - .bodyValue(info)); + .bodyValue(info)) + .onErrorResume(NotFoundException.class, e -> + ServerResponse.status(HttpStatus.NOT_FOUND) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(java.util.Map.of("code", 404, "message", e.getMessage()))); } @Operation(summary = "更新会员信息", description = "更新会员昵称、性别、生日、头像、地址等信息") diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java index e40ff5f..38b7083 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java @@ -65,21 +65,37 @@ public class MemberServiceImpl implements MemberService { public Mono getMemberInfo(Long memberId) { String cacheKey = MEMBER_INFO_CACHE_PREFIX + memberId; + // 先查缓存 return redisUtil.get(cacheKey, MemberInfoVO.class) .flatMap(cached -> { if (cached != null) { log.debug("从缓存获取会员信息, memberId: {}", memberId); return Mono.just(cached); } - return memberRepository.findById(memberId) - .map(this::buildMemberInfoResponse) - .flatMap(vo -> redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) - .then(Mono.just(vo))) - .switchIfEmpty(Mono.error(() -> { - log.error("会员不存在: memberId={}", memberId); - throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); - })); - }); + // 缓存没有,查数据库 + return queryFromDatabaseAndCache(memberId, cacheKey); + }) + .switchIfEmpty(Mono.defer(() -> { + // 缓存返回null,查数据库 + return queryFromDatabaseAndCache(memberId, cacheKey); + })); + } + + /** + * 从数据库查询并更新缓存 + */ + private Mono queryFromDatabaseAndCache(Long memberId, String cacheKey) { + return memberRepository.findById(memberId) + .map(this::buildMemberInfoResponse) + .flatMap(vo -> { + // 查询到数据后更新缓存 + return redisUtil.setWithExpire(cacheKey, vo, CACHE_EXPIRE_SECONDS) + .then(Mono.just(vo)); + }) + .switchIfEmpty(Mono.error(() -> { + log.error("会员不存在: memberId={}", memberId); + throw new NotFoundException(ErrorCode.NOT_FOUND_USER, "会员不存在"); + })); } @Override diff --git a/gym-manage-api/gym-payment/pom.xml b/gym-manage-api/gym-payment/pom.xml new file mode 100644 index 0000000..b8d2fcd --- /dev/null +++ b/gym-manage-api/gym-payment/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-payment + jar + + Gym Payment + Payment Module - Integrates Huifu Payment Gateway + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + cn.hutool + hutool-all + 5.8.25 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java new file mode 100644 index 0000000..5669d26 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java @@ -0,0 +1,23 @@ +package cn.novalon.gym.manage.payment.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + @Bean + public CorsWebFilter corsWebFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + config.setAllowCredentials(false); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsWebFilter(source); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java new file mode 100644 index 0000000..c9596cc --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java @@ -0,0 +1,34 @@ + +package cn.novalon.gym.manage.payment.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "payment.huifu") +public class HuifuPayConfig { + + private String sysId; + + private String productId; + + private String huifuId; + + private String acctId; + + private String privateKey; + + private String publicKey; + + private String createUrl; + + private String queryUrl; + + private String refundUrl; + + private String notifyUrl; + + private String version = "1.0"; +} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java new file mode 100644 index 0000000..b37b8a8 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java @@ -0,0 +1,58 @@ + +package cn.novalon.gym.manage.payment.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentCreateRequest { + + @NotNull(message = "会员ID不能为空") + private Long memberId; + + @NotBlank(message = "订单类型不能为空") + @Size(max = 50, message = "订单类型长度不能超过50") + private String orderType; + + @NotBlank(message = "商品描述不能为空") + @Size(max = 128, message = "商品描述长度不能超过128") + private String goodsDesc; + + @NotBlank(message = "交易金额不能为空") + private String transAmt; + + @NotBlank(message = "交易类型不能为空") + @Size(max = 16, message = "交易类型长度不能超过16") + private String tradeType; + + @Size(max = 255, message = "备注长度不能超过255") + private String remark; + + @Size(max = 9, message = "账户号长度不能超过9") + private String acctId; + + private String timeExpire; + + private String delayAcctFlag; + + private Integer feeFlag; + + @Size(max = 128, message = "禁用支付方式长度不能超过128") + private String limitPayType; + + @Size(max = 32, message = "渠道号长度不能超过32") + private String channelNo; + + private String payScene; + + private String notifyUrl; +} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java new file mode 100644 index 0000000..dc89b50 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java @@ -0,0 +1,28 @@ + +package cn.novalon.gym.manage.payment.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentCreateResponse { + + private String orderId; + + private String tradeType; + + private String qrCode; + + private String payInfo; + + private String transAmt; + + private String payStatus; + + private String message; +} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java new file mode 100644 index 0000000..b7b0b71 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java @@ -0,0 +1,30 @@ + +package cn.novalon.gym.manage.payment.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentQueryResponse { + + private String orderId; + + private String tradeType; + + private String transAmt; + + private String payStatus; + + private String outTransId; + + private String qrCode; + + private String payTime; + + private String message; +} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java new file mode 100644 index 0000000..92b3c41 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java @@ -0,0 +1,22 @@ + +package cn.novalon.gym.manage.payment.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentRefundResponse { + + private String orderId; + + private String refundAmt; + + private String refundStatus; + + private String message; +} 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 new file mode 100644 index 0000000..eab9746 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java @@ -0,0 +1,78 @@ + +package cn.novalon.gym.manage.payment.handler; + +import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; +import cn.novalon.gym.manage.payment.service.IPaymentService; +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 java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@Tag(name = "支付管理", description = "斗拱聚合支付相关操作") +public class PaymentHandler { + + private final IPaymentService paymentService; + + public PaymentHandler(IPaymentService paymentService) { + this.paymentService = paymentService; + } + + @Operation(summary = "创建支付", description = "创建支付订单,支持微信、支付宝等多种支付方式") + public Mono createPayment(ServerRequest request) { + log.info("========== PaymentHandler.createPayment 被调用 =========="); + return request.bodyToMono(PaymentCreateRequest.class) + .flatMap(paymentService::createPayment) + .flatMap(response -> ServerResponse.ok().bodyValue(response)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("创建支付失败: " + e.getMessage()))); + } + + @Operation(summary = "查询支付状态", description = "根据订单号查询支付状态") + public Mono queryPayment(ServerRequest request) { + String orderId = request.pathVariable("orderId"); + return paymentService.queryPayment(orderId) + .flatMap(response -> ServerResponse.ok().bodyValue(response)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse(e.getMessage()))); + } + + @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")); + return paymentService.refundPayment(orderId, refundAmt); + }) + .flatMap(response -> ServerResponse.ok().bodyValue(response)) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("退款失败: " + e.getMessage()))); + } + + @Operation(summary = "支付回调", description = "支付成功回调通知") + public Mono handleNotify(ServerRequest request) { + return request.bodyToMono(String.class) + .flatMap(paymentService::handleNotify) + .then(ServerResponse.ok().bodyValue(buildSuccessResponse("处理成功"))) + .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("处理失败: " + e.getMessage()))); + } + + private Map buildErrorResponse(String message) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", message); + return response; + } + + private Map buildSuccessResponse(String message) { + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", message); + return response; + } +} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java new file mode 100644 index 0000000..d8f70a5 --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java @@ -0,0 +1,19 @@ + +package cn.novalon.gym.manage.payment.service; + +import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; +import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse; +import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse; +import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse; +import reactor.core.publisher.Mono; + +public interface IPaymentService { + + Mono createPayment(PaymentCreateRequest request); + + Mono queryPayment(String orderId); + + Mono refundPayment(String orderId, String refundAmt); + + Mono handleNotify(String notifyBody); +} 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 new file mode 100644 index 0000000..8cbecce --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java @@ -0,0 +1,723 @@ +package cn.novalon.gym.manage.payment.service.impl; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; +import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse; +import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse; +import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse; +import cn.novalon.gym.manage.payment.service.IPaymentService; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class PaymentServiceImpl implements IPaymentService { + + private final OkHttpClient okHttpClient; + private final Map paymentCache = new ConcurrentHashMap<>(); + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + // ==================== 配置 ==================== + private static final String SYS_ID = "6666000207573586"; // 代理商 + private static final String PRODUCT_ID = "XLSISV"; + private static final String HUIFU_ID = "6666000207581039"; // 商户号 + private static final String ACCT_ID = "F28308086"; + private static final String ALIPAY_CHANNEL = "hlm001"; + private static final String NOTIFY_URL = "http://localhost:8084/api/payment/notify"; + + // ===== v4 接口 ===== + private static final String CREATE_URL = "https://api.huifu.com/v4/trade/payment/create"; + private static final String QUERY_URL = "https://api.huifu.com/v4/trade/payment/query"; + private static final String REFUND_URL = "https://api.huifu.com/v4/trade/payment/refund"; + private static final String CLOSE_URL = "https://api.huifu.com/v4/trade/payment/close"; + + // ==================== 密钥 ==================== + // 商户私钥(用于签名请求) + private static final String MERCHANT_PRIVATE_KEY = + "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="; + + // 汇付公钥(用于验证回调签名) + private static final String HUIFU_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqolFneAH66z3/3gaIYIaRZOIk/UYzdsXIm0RyawYBYAOu/NJ7ul8CRIrRlt5vd98HodW2yPrXA4+VHF3AS9UE4WTDpo9qV5brhqQSr/lAuZtEwMZwUWwgdnGFMkUFd9RvyGXAqY0bsQrcQgQ6zGjZHzlMljogDR3iblG0ak5ssD2TSC2W+1cxu+id+FP6onZXlXizuClTyIRh17m7CbS6rl0P3M96MlTdCzTeBw/Y54CiegBJI2wOrm2Qa6Dg6KRc+YkaJWjuRJVJkwjk8JhSyALno9oEzuDKaAXlsQlxeIhmAy4esRrZGrMV8SG0gwUZIP8lduPjQE95lCqqJ0gQIDAQAB"; + + public PaymentServiceImpl() { + this.okHttpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .retryOnConnectionFailure(false) + .build(); + + // 验证私钥是否有效 + validatePrivateKey(); + } + + private void validatePrivateKey() { + try { + log.info("========== 验证私钥 =========="); + String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", ""); + log.info("私钥长度(去空格后): {}", privateKeyBase64.length()); + + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64); + log.info("私钥解码后长度: {} bytes", privateKeyBytes.length); + + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(spec); + log.info("私钥加载成功,算法: {}", privateKey.getAlgorithm()); + + // 测试签名 + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(privateKey); + signature.update("test".getBytes(StandardCharsets.UTF_8)); + signature.sign(); + log.info("私钥签名测试成功!"); + log.info("========== 私钥验证完成 =========="); + } catch (Exception e) { + log.error("========== 私钥验证失败 ==========", e); + } + } + + // ==================== 1. 创建支付订单 ==================== + @Override + public Mono createPayment(PaymentCreateRequest request) { + log.info("========== createPayment 方法被调用 =========="); + String orderId = UUID.randomUUID().toString().replace("-", ""); + String reqDate = LocalDateTime.now().format(DATE_FORMATTER); + String reqSeqId = "RQ" + System.currentTimeMillis(); + + // ===== 构建 data 参数 ===== + Map data = new HashMap<>(); + data.put("req_date", reqDate); + data.put("req_seq_id", reqSeqId); + data.put("huifu_id", HUIFU_ID); + data.put("trade_type", "ALIPAY"); + data.put("pay_type", "APP"); // APP 支付,返回 alipay_scheme + data.put("trans_amt", request.getTransAmt() != null ? request.getTransAmt() : "1"); + data.put("goods_desc", request.getGoodsDesc() != null ? request.getGoodsDesc() : "会员卡"); + data.put("acct_id", ACCT_ID); + data.put("notify_url", NOTIFY_URL); + data.put("remark", request.getRemark() != null ? request.getRemark() : ""); + + // ===== 支付宝参数 ===== + data.put("alipay_channel", ALIPAY_CHANNEL); + + String dataJson = JSONUtil.toJsonStr(data); + String sign = generateSign(dataJson); + + Map requestBody = new HashMap<>(); + requestBody.put("sys_id", SYS_ID); + requestBody.put("product_id", PRODUCT_ID); + requestBody.put("data", dataJson); + requestBody.put("sign", sign); + + return Mono.fromCallable(() -> { + String jsonBody = JSONUtil.toJsonStr(requestBody); + log.info("========== 发起支付宝支付请求 (APP) =========="); + log.info("请求URL: {}", CREATE_URL); + log.info("请求Body: {}", jsonBody); + log.info("=================================="); + + Request httpRequest = new Request.Builder() + .url(CREATE_URL) + .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) + .addHeader("Content-Type", "application/json; charset=UTF-8") + .addHeader("Accept", "application/json") + .build(); + + try (Response response = okHttpClient.newCall(httpRequest).execute()) { + int httpCode = response.code(); + String responseBody = response.body() != null ? response.body().string() : ""; + + log.info("========== 收到响应 =========="); + log.info("HTTP状态码: {}", httpCode); + log.info("响应Body: {}", responseBody); + log.info("==============================="); + + if (httpCode >= 400) { + if (responseBody != null && !responseBody.isEmpty()) { + try { + JSONObject errorJson = JSONUtil.parseObj(responseBody); + String errorMsg = errorJson.getStr("error_msg"); + String errorCode = errorJson.getStr("error_code"); + if (errorMsg != null) { + throw new RuntimeException("支付失败: " + errorMsg + " (code: " + errorCode + ")"); + } + } catch (Exception e) { + // 忽略 + } + } + throw new RuntimeException("支付请求失败: HTTP " + httpCode); + } + + if (responseBody == null || responseBody.isEmpty()) { + throw new RuntimeException("响应体为空"); + } + + return parseCreateResponse(responseBody, orderId, request); + } catch (IOException e) { + log.error("网络请求异常", e); + throw new RuntimeException("网络请求异常: " + e.getMessage(), e); + } + }); + } + + // ==================== 2. 查询支付状态 ==================== + @Override + public Mono queryPayment(String orderId) { + PaymentInfo paymentInfo = paymentCache.get(orderId); + if (paymentInfo == null) { + return Mono.error(new RuntimeException("支付记录不存在")); + } + + String reqDate = LocalDateTime.now().format(DATE_FORMATTER); + String reqSeqId = "RQ" + System.currentTimeMillis(); + + Map data = new HashMap<>(); + data.put("req_date", reqDate); + data.put("req_seq_id", reqSeqId); + data.put("huifu_id", HUIFU_ID); + data.put("out_trans_id", paymentInfo.outTransId); + + String dataJson = JSONUtil.toJsonStr(data); + String sign = generateSign(dataJson); + + Map requestBody = new HashMap<>(); + requestBody.put("sys_id", SYS_ID); + requestBody.put("product_id", PRODUCT_ID); + requestBody.put("data", dataJson); + requestBody.put("sign", sign); + + return Mono.fromCallable(() -> { + String jsonBody = JSONUtil.toJsonStr(requestBody); + log.info("查询支付状态, orderId={}", orderId); + + Request httpRequest = new Request.Builder() + .url(QUERY_URL) + .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) + .addHeader("Content-Type", "application/json; charset=UTF-8") + .addHeader("Accept", "application/json") + .build(); + + try (Response response = okHttpClient.newCall(httpRequest).execute()) { + int httpCode = response.code(); + String responseBody = response.body() != null ? response.body().string() : ""; + + log.info("查询响应: code={}, body={}", httpCode, responseBody); + + if (httpCode >= 400) { + throw new RuntimeException("查询失败: HTTP " + httpCode); + } + + if (responseBody == null || responseBody.isEmpty()) { + throw new RuntimeException("查询响应为空"); + } + + return parseQueryResponse(responseBody, orderId); + } + }); + } + + // ==================== 3. 申请退款 ==================== + @Override + public Mono refundPayment(String orderId, String refundAmt) { + PaymentInfo paymentInfo = paymentCache.get(orderId); + if (paymentInfo == null) { + return Mono.error(new RuntimeException("支付记录不存在")); + } + + String reqDate = LocalDateTime.now().format(DATE_FORMATTER); + String reqSeqId = "RQ" + System.currentTimeMillis(); + + Map data = new HashMap<>(); + data.put("req_date", reqDate); + data.put("req_seq_id", reqSeqId); + data.put("huifu_id", HUIFU_ID); + data.put("out_trans_id", paymentInfo.outTransId); + data.put("trans_amt", refundAmt); + + String dataJson = JSONUtil.toJsonStr(data); + String sign = generateSign(dataJson); + + Map requestBody = new HashMap<>(); + requestBody.put("sys_id", SYS_ID); + requestBody.put("product_id", PRODUCT_ID); + requestBody.put("data", dataJson); + requestBody.put("sign", sign); + + return Mono.fromCallable(() -> { + String jsonBody = JSONUtil.toJsonStr(requestBody); + log.info("发起退款请求, orderId={}", orderId); + + Request httpRequest = new Request.Builder() + .url(REFUND_URL) + .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) + .addHeader("Content-Type", "application/json; charset=UTF-8") + .addHeader("Accept", "application/json") + .build(); + + try (Response response = okHttpClient.newCall(httpRequest).execute()) { + int httpCode = response.code(); + String responseBody = response.body() != null ? response.body().string() : ""; + + log.info("退款响应: code={}, body={}", httpCode, responseBody); + + if (httpCode >= 400) { + throw new RuntimeException("退款失败: HTTP " + httpCode); + } + + return parseRefundResponse(responseBody, orderId, refundAmt); + } + }); + } + + // ==================== 4. 关闭订单 ==================== + public Mono closePayment(String orderId) { + PaymentInfo paymentInfo = paymentCache.get(orderId); + if (paymentInfo == null) { + return Mono.error(new RuntimeException("支付记录不存在")); + } + + String reqDate = LocalDateTime.now().format(DATE_FORMATTER); + String reqSeqId = "RQ" + System.currentTimeMillis(); + + Map data = new HashMap<>(); + data.put("req_date", reqDate); + data.put("req_seq_id", reqSeqId); + data.put("huifu_id", HUIFU_ID); + data.put("out_trans_id", paymentInfo.outTransId); + + String dataJson = JSONUtil.toJsonStr(data); + String sign = generateSign(dataJson); + + Map requestBody = new HashMap<>(); + requestBody.put("sys_id", SYS_ID); + requestBody.put("product_id", PRODUCT_ID); + requestBody.put("data", dataJson); + requestBody.put("sign", sign); + + return Mono.fromRunnable(() -> { + try { + String jsonBody = JSONUtil.toJsonStr(requestBody); + log.info("关闭订单请求, orderId={}", orderId); + + Request httpRequest = new Request.Builder() + .url(CLOSE_URL) + .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) + .addHeader("Content-Type", "application/json; charset=UTF-8") + .addHeader("Accept", "application/json") + .build(); + + try (Response response = okHttpClient.newCall(httpRequest).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + log.info("关闭订单响应, code={}, body={}", response.code(), responseBody); + } + } catch (Exception e) { + log.error("关闭订单异常", e); + } + }); + } + + // ==================== 5. 处理异步通知 ==================== + @Override + public Mono handleNotify(String notifyBody) { + return Mono.fromRunnable(() -> { + log.info("========== 收到支付回调通知 =========="); + log.info("回调内容: {}", notifyBody); + + try { + JSONObject notifyJson = JSONUtil.parseObj(notifyBody); + String sign = notifyJson.getStr("sign"); + String data = notifyJson.getStr("data"); + + // 直接打印回调信息,不验证签名 + log.info("签名: {}", sign); + log.info("数据: {}", data); + + if (data != null && !data.isEmpty()) { + JSONObject dataJson = JSONUtil.parseObj(data); + String respCode = dataJson.getStr("resp_code"); + String respDesc = dataJson.getStr("resp_desc"); + String outTransId = dataJson.getStr("out_trans_id"); + String transStatus = dataJson.getStr("trans_status"); + String transAmt = dataJson.getStr("trans_amt"); + String finishDate = dataJson.getStr("finish_date"); + String finishTime = dataJson.getStr("finish_time"); + + log.info("========== 回调数据解析 =========="); + log.info("响应码: {}", respCode); + log.info("响应描述: {}", respDesc); + log.info("商户订单号: {}", outTransId); + log.info("交易状态: {}", transStatus); + log.info("交易金额: {}", transAmt); + log.info("交易完成日期: {}", finishDate); + log.info("交易完成时间: {}", finishTime); + log.info("==================================="); + + // 更新本地支付状态 + if ("S".equals(transStatus) || "TRADE_SUCCESS".equals(transStatus)) { + paymentCache.values().stream() + .filter(info -> outTransId != null && outTransId.equals(info.outTransId)) + .findFirst() + .ifPresent(info -> { + info.payStatus = transStatus; + info.payTime = LocalDateTime.now(); + log.info("支付状态更新成功, orderId={}, status={}", info.orderId, transStatus); + }); + } + } + } catch (Exception e) { + log.error("处理回调异常", e); + } + }); + } + + // ==================== 签名方法 ==================== + private String generateSign(String dataJson) { + Exception originalException = null; + System.err.println(">>>>>>>>> [PaymentServiceImpl] generateSign 开始执行 <<<<<<<<<<"); + System.err.flush(); + try { + System.err.println(">>>>>>>>> 开始签名方法 <<<<<<<<<<"); + System.err.flush(); + log.error("========== 开始签名 =========="); + + // 打印密钥的前几个字节来检查格式 + String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", ""); + byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64); + System.err.println(">>>>>>>>> 密钥前20字节(hex): " + bytesToHex(keyBytes, 20) + " <<<<<<<<<<"); + System.err.println(">>>>>>>>> 密钥长度: " + keyBytes.length + " bytes <<<<<<<<<<"); + System.err.flush(); + + // 检查是否是PKCS#8格式 (应该以30 82开头) + if (keyBytes.length > 2) { + System.err.println(">>>>>>>>> 密钥前2字节(hex): " + bytesToHex(keyBytes, 2) + " <<<<<<<<<<"); + boolean startsWith30 = (keyBytes[0] & 0xFF) == 0x30; + System.err.println(">>>>>>>>> 密钥是否以0x30开头: " + startsWith30 + " <<<<<<<<<<"); + System.err.flush(); + + // 打印更多头部字节来识别密钥类型 + if (keyBytes.length > 4) { + System.err.println(">>>>>>>>> 密钥前5字节(hex): " + bytesToHex(keyBytes, 5) + " <<<<<<<<<<"); + System.err.flush(); + // RSA PKCS#8 私钥通常以 30 82 04** 开头 + // EC PKCS#8 私钥通常以 30 82 04** 开头 (和RSA一样,需要看后面的内容区分) + // 如果是 30 82,且第4个字节是 0x00 或 0x01,可能是RSA私钥 + // 如果是 30 82,且第4个字节是 0x02 或 0x03,可能是EC私钥 + } + } + + log.info("私钥长度(去空格后): {}", privateKeyBase64.length()); + + // 检查私钥格式 + if (privateKeyBase64.length() < 100) { + throw new RuntimeException("私钥长度异常: " + privateKeyBase64.length()); + } + + log.info("私钥前50字符: {}", privateKeyBase64.substring(0, 50)); + log.info("私钥后50字符: {}", privateKeyBase64.substring(privateKeyBase64.length() - 50)); + + log.info("开始解码私钥..."); + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64); + log.info("私钥解码后长度: {} bytes", privateKeyBytes.length); + + // 直接尝试使用PKCS8EncodedKeySpec加载 + log.info("开始生成 PrivateKey 对象..."); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(spec); + log.info("私钥生成成功,算法: {}", privateKey.getAlgorithm()); + + log.info("开始签名..."); + Signature signature = Signature.getInstance("SHA256withRSA"); + log.info("调用 initSign..."); + signature.initSign(privateKey); + log.info("initSign 完成"); + log.info("开始 update..."); + signature.update(dataJson.getBytes(StandardCharsets.UTF_8)); + log.info("update 完成,开始 sign..."); + String sign = Base64.getEncoder().encodeToString(signature.sign()); + log.info("签名生成成功, 签名长度: {}", sign.length()); + log.info("========== 签名完成 =========="); + return sign; + } catch (RuntimeException e) { + log.error("签名生成失败(RuntimeException): {} - {}", e.getClass().getName(), e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error("签名生成失败(Exception): {} - {}", e.getClass().getName(), e.getMessage(), e); + throw new RuntimeException("签名生成失败: " + e.getMessage(), e); + } + } + + private boolean verifySign(String sign, String dataJson) { + try { + String publicKeyBase64 = HUIFU_PUBLIC_KEY.replaceAll("\\s", ""); + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64); + X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(spec); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(publicKey); + signature.update(dataJson.getBytes(StandardCharsets.UTF_8)); + return signature.verify(Base64.getDecoder().decode(sign)); + } catch (Exception e) { + log.error("验签失败", e); + return false; + } + } + + // ==================== 响应解析方法 ==================== + + private PaymentCreateResponse parseCreateResponse(String responseBody, String orderId, PaymentCreateRequest request) { + JSONObject responseJson = JSONUtil.parseObj(responseBody); + String dataStr = responseJson.getStr("data"); + JSONObject data = JSONUtil.parseObj(dataStr); + + String respCode = data.getStr("resp_code"); + if (!"000000".equals(respCode)) { + throw new RuntimeException("支付创建失败: " + data.getStr("resp_desc") + " (code: " + respCode + ")"); + } + + // APP 支付优先取 alipay_scheme + String payUrl = data.getStr("alipay_scheme"); + if (payUrl == null) { + payUrl = data.getStr("pay_url"); + } + if (payUrl == null) { + payUrl = data.getStr("pay_info"); + } + + PaymentInfo paymentInfo = new PaymentInfo(); + paymentInfo.orderId = orderId; + paymentInfo.memberId = request.getMemberId(); + paymentInfo.orderType = request.getOrderType(); + paymentInfo.transAmt = request.getTransAmt(); + paymentInfo.goodsDesc = request.getGoodsDesc(); + paymentInfo.payStatus = "PENDING"; + paymentInfo.outTransId = data.getStr("out_trans_id"); + paymentInfo.payUrl = payUrl; + paymentInfo.remark = request.getRemark(); + paymentCache.put(orderId, paymentInfo); + + log.info("支付创建成功, orderId={}, outTransId={}, payUrl={}", orderId, paymentInfo.outTransId, payUrl); + + return PaymentCreateResponse.builder() + .orderId(orderId) + .payInfo(payUrl) + .transAmt(request.getTransAmt()) + .payStatus("PENDING") + .message("支付创建成功") + .build(); + } + + private PaymentQueryResponse parseQueryResponse(String responseBody, String orderId) { + JSONObject responseJson = JSONUtil.parseObj(responseBody); + String dataStr = responseJson.getStr("data"); + JSONObject data = JSONUtil.parseObj(dataStr); + + String respCode = data.getStr("resp_code"); + if (!"000000".equals(respCode)) { + throw new RuntimeException("查询失败: " + data.getStr("resp_desc")); + } + + PaymentInfo paymentInfo = paymentCache.get(orderId); + if (paymentInfo != null) { + paymentInfo.payStatus = data.getStr("trans_status"); + } + + return PaymentQueryResponse.builder() + .orderId(orderId) + .tradeType(data.getStr("trade_type")) + .transAmt(data.getStr("trans_amt")) + .payStatus(data.getStr("trans_status")) + .outTransId(data.getStr("out_trans_id")) + .payTime(data.getStr("end_time")) + .message("查询成功") + .build(); + } + + private PaymentRefundResponse parseRefundResponse(String responseBody, String orderId, String refundAmt) { + JSONObject responseJson = JSONUtil.parseObj(responseBody); + String dataStr = responseJson.getStr("data"); + JSONObject data = JSONUtil.parseObj(dataStr); + + String respCode = data.getStr("resp_code"); + if (!"000000".equals(respCode)) { + throw new RuntimeException("退款失败: " + data.getStr("resp_desc")); + } + + PaymentInfo paymentInfo = paymentCache.get(orderId); + if (paymentInfo != null) { + paymentInfo.payStatus = "REFUNDED"; + } + + return PaymentRefundResponse.builder() + .orderId(orderId) + .refundAmt(refundAmt) + .refundStatus(data.getStr("trans_status")) + .message("退款成功") + .build(); + } + + // ==================== 工具方法 ==================== + private static String bytesToHex(byte[] bytes, int length) { + int len = Math.min(bytes.length, length); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append(String.format("%02X ", bytes[i] & 0xFF)); + } + return sb.toString(); + } + + /** + * 将PKCS#1格式的RSA私钥转换为PKCS#8格式 + * PKCS#1: SEQUENCE { version, modulus, publicExponent, privateExponent, prime1, prime2, ... } + * PKCS#8: SEQUENCE { version, algorithmIdentifier, OCTET STRING (containing PKCS#1) } + */ + private byte[] convertPKCS1ToPKCS8(byte[] pkcs1Key) { + try { + log.info("检测到PKCS#1格式私钥,开始转换为PKCS#8格式..."); + log.info("PKCS#1密钥长度: {} bytes", pkcs1Key.length); + + // RSA算法OID: 1.2.840.113549.1.1.1 + byte[] rsaOid = new byte[] { + 0x06, 0x09, 0x2A, (byte)0x86, 0x48, (byte)0x86, (byte)0xF7, 0x0D, 0x01, 0x01, 0x01 + }; + + // 构建AlgorithmIdentifier: SEQUENCE { OID, NULL } + // SEQUENCE (2 bytes: tag + length) + OID + NULL = 15 bytes total + byte[] algorithmIdentifier = new byte[15]; + int idx = 0; + algorithmIdentifier[idx++] = 0x30; // SEQUENCE tag + algorithmIdentifier[idx++] = 0x0D; // length = 13 bytes for OID + NULL + System.arraycopy(rsaOid, 0, algorithmIdentifier, idx, rsaOid.length); + idx += rsaOid.length; + algorithmIdentifier[idx++] = 0x05; // NULL tag + algorithmIdentifier[idx++] = 0x00; // NULL value + + log.info("AlgorithmIdentifier长度: {} bytes", algorithmIdentifier.length); + + // 计算 OCTET STRING 长度编码需要的字节数 + int octetStringLengthBytes = 1; // tag + if (pkcs1Key.length > 127) { + if (pkcs1Key.length > 255) { + octetStringLengthBytes += 3; // 0x82 + 2 length bytes + } else { + octetStringLengthBytes += 2; // 0x81 + 1 length byte + } + } else { + octetStringLengthBytes += 1; // 1 length byte + } + + // 计算 version INTEGER 编码 (3 bytes: tag + length + value) + int versionBytes = 3; + + // 计算 total length of inner content (version + algorithmId + octetString) + int innerContentLength = versionBytes + algorithmIdentifier.length + octetStringLengthBytes + pkcs1Key.length; + + // 计算 outer SEQUENCE 长度编码需要的字节数 + int sequenceLengthBytes = 1; // tag + if (innerContentLength > 127) { + if (innerContentLength > 255) { + sequenceLengthBytes += 3; // 0x82 + 2 length bytes + } else { + sequenceLengthBytes += 2; // 0x81 + 1 length byte + } + } else { + sequenceLengthBytes += 1; + } + + // 分配最终数组 + int totalLen = sequenceLengthBytes + innerContentLength; + byte[] pkcs8Key = new byte[totalLen]; + log.info("PKCS#8密钥分配: {} bytes", totalLen); + + int offset = 0; + + // outer SEQUENCE + pkcs8Key[offset++] = 0x30; + if (innerContentLength > 127) { + if (innerContentLength > 255) { + pkcs8Key[offset++] = (byte)0x82; + pkcs8Key[offset++] = (byte)((innerContentLength >> 8) & 0xFF); + pkcs8Key[offset++] = (byte)(innerContentLength & 0xFF); + } else { + pkcs8Key[offset++] = (byte)0x81; + pkcs8Key[offset++] = (byte)innerContentLength; + } + } else { + pkcs8Key[offset++] = (byte)innerContentLength; + } + + // version = 0 + pkcs8Key[offset++] = 0x02; + pkcs8Key[offset++] = 0x01; + pkcs8Key[offset++] = 0x00; + + // algorithmIdentifier + System.arraycopy(algorithmIdentifier, 0, pkcs8Key, offset, algorithmIdentifier.length); + offset += algorithmIdentifier.length; + + // privateKey as OCTET STRING + pkcs8Key[offset++] = 0x04; + if (pkcs1Key.length > 127) { + if (pkcs1Key.length > 255) { + pkcs8Key[offset++] = (byte)0x82; + pkcs8Key[offset++] = (byte)((pkcs1Key.length >> 8) & 0xFF); + pkcs8Key[offset++] = (byte)(pkcs1Key.length & 0xFF); + } else { + pkcs8Key[offset++] = (byte)0x81; + pkcs8Key[offset++] = (byte)pkcs1Key.length; + } + } else { + pkcs8Key[offset++] = (byte)pkcs1Key.length; + } + + // 复制 PKCS#1 私钥 + System.arraycopy(pkcs1Key, 0, pkcs8Key, offset, pkcs1Key.length); + + log.info("PKCS#1到PKCS#8转换完成,最终长度: {} bytes", pkcs8Key.length); + return pkcs8Key; + } catch (Exception e) { + log.error("PKCS#1到PKCS#8转换失败", e); + throw new RuntimeException("密钥格式转换失败: " + e.getMessage(), e); + } + } + + // ==================== 内部缓存类 ==================== + private static class PaymentInfo { + String orderId; + Long memberId; + String orderType; + String tradeType; + String transAmt; + String goodsDesc; + String payStatus; + String outTransId; + String payUrl; + String hfSeqId; + LocalDateTime payTime; + String remark; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..691774a --- /dev/null +++ b/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.novalon.gym.manage.payment.config.HuifuPayConfig diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 5682d05..6e95ea5 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -53,6 +53,11 @@ gym-dataCount ${project.version} + + cn.novalon.gym.manage + gym-auth + ${project.version} + org.springframework.boot diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java index 7d11ca4..c0c53cd 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/ManageApplication.java @@ -10,14 +10,13 @@ import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDeta import org.springframework.context.annotation.Bean; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; -import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.server.WebFilter; import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = { ReactiveUserDetailsServiceAutoConfiguration.class }) -@EnableScheduling +//@EnableScheduling @EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository" , 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 e75fcf0..482d931 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 @@ -4,6 +4,7 @@ package cn.novalon.gym.manage.app.config; 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; @@ -76,7 +77,8 @@ public class SystemRouter { GroupCourseTypeHandler groupCourseTypeHandler, CourseLabelHandler courseLabelHandler, CheckInHandler checkInHandler, - DataStatisticsHandler dataStatisticsHandler) { + DataStatisticsHandler dataStatisticsHandler, + PhoneAuthHandler phoneAuthHandler) { return route() // ========== 诊断路由 ========== @@ -192,10 +194,13 @@ public class SystemRouter { // ========== 消息路由 ========== .GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser) + .GET("/api/messages/user/{userId}/page", messageHandler::getMessagesByUserPage) .GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount) .GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList) + .GET("/api/messages/user/{userId}/unread/page", messageHandler::getUnreadMessagesPage) .POST("/api/messages", messageHandler::createMessage) .PUT("/api/messages/{id}/read", messageHandler::markAsRead) + .PUT("/api/messages/user/{userId}/read", messageHandler::markAllAsRead) .DELETE("/api/messages/{id}", messageHandler::deleteMessage) // ========== 文件路由 ========== @@ -218,6 +223,11 @@ public class SystemRouter { .PUT("/api/permissions/{id}", permissionHandler::updatePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission) + // ========== 手机号认证路由 ========== + .POST("/api/auth/phone/one-click-login", phoneAuthHandler::oneClickLogin) + .POST("/api/auth/phone/send-code", phoneAuthHandler::sendSmsCode) + .POST("/api/auth/phone/code-login", phoneAuthHandler::codeLogin) + // ========== 会员模块路由 - 微信认证 ========== .POST("/api/member/auth/miniapp/login", wechatAuthHandler::miniappLogin) .GET("/api/member/auth/mp/callback", wechatAuthHandler::verifyMpSignature) @@ -349,6 +359,7 @@ public class SystemRouter { .GET("/api/datacount/signin", dataStatisticsHandler::getSignInStatistics) .GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics) .GET("/api/datacount/export", dataStatisticsHandler::exportStatistics) + .build(); } } 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 33e428e..46b6a66 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 @@ -2,9 +2,9 @@ spring: cache: type: none r2dbc: - url: r2dbc:postgresql://localhost:55432/manage_system - username: novalon - password: novalon123 + url: r2dbc:postgresql://localhost:5432/manage_system + username: postgres + password: 123456 pool: initial-size: 5 max-size: 20 @@ -12,10 +12,10 @@ spring: max-life-time: 30m acquire-timeout: 3s flyway: - url: jdbc:postgresql://localhost:55432/manage_system - user: novalon - password: novalon123 - enabled: true + url: jdbc:postgresql://localhost:5432/manage_system + user: postgres + password: 123456 + enabled: false locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true diff --git a/gym-manage-api/manage-app/src/main/resources/application-local.yml b/gym-manage-api/manage-app/src/main/resources/application-local.yml index 73b95b3..e1035f1 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-local.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-local.yml @@ -4,8 +4,8 @@ spring: activate: on-profile: local r2dbc: - url: r2dbc:postgresql://localhost:55432/manage_system - username: novalon + url: r2dbc:postgresql://localhost:5432/manage_system + username: postgres password: 123456 pool: initial-size: 5 @@ -14,8 +14,8 @@ spring: max-life-time: 30m acquire-timeout: 3s datasource: - url: jdbc:postgresql://localhost:55432/manage_system - username: novalon + url: jdbc:postgresql://localhost:5432/manage_system + username: postgres password: 123456 driver-class-name: org.postgresql.Driver flyway: diff --git a/gym-manage-api/manage-app/src/main/resources/application-test.yml b/gym-manage-api/manage-app/src/main/resources/application-test.yml index 0b1fba2..d131944 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-test.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-test.yml @@ -5,9 +5,9 @@ spring: application: name: manage-app r2dbc: - url: r2dbc:postgresql://localhost:55432/manage_system - username: novalon - password: novalon123 + url: r2dbc:postgresql://localhost:5432/manage_system + username: postgres + password: 123456 pool: initial-size: 5 max-size: 20 diff --git a/gym-manage-api/manage-app/src/main/resources/application.yml b/gym-manage-api/manage-app/src/main/resources/application.yml index 4fee231..e1fe965 100644 --- a/gym-manage-api/manage-app/src/main/resources/application.yml +++ b/gym-manage-api/manage-app/src/main/resources/application.yml @@ -15,9 +15,9 @@ spring: exclude: - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration r2dbc: - url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:novalon} - password: ${DB_PASSWORD:novalon123} + url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:123456} pool: initial-size: 10 max-size: 50 @@ -25,12 +25,12 @@ spring: max-life-time: 1h acquire-timeout: 5s datasource: - url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} - username: ${DB_USERNAME:novalon} - password: ${DB_PASSWORD:novalon123} + url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:manage_system} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:123456} driver-class-name: org.postgresql.Driver flyway: - enabled: true + enabled: false locations: classpath:db/migration baseline-on-migrate: true baseline-version: 0 @@ -54,7 +54,7 @@ spring: profiles: active: dev config: - import: classpath:member-config.yml + import: classpath:keys.properties,classpath:member-config.yml @@ -94,3 +94,13 @@ springdoc: show-actuator: false default-consumes-media-type: application/json default-produces-media-type: application/json + +alibaba: + cloud: + sms: + access-key-id: ${ALIBABA_ACCESS_KEY_ID:} + access-key-secret: ${ALIBABA_ACCESS_KEY_SECRET:} + sign-name: ${ALIBABA_SMS_SIGN_NAME:} + template-code: ${ALIBABA_SMS_TEMPLATE_CODE:} + code-length: 6 + code-expire-seconds: 300 diff --git a/gym-manage-api/manage-app/src/main/resources/keys.properties b/gym-manage-api/manage-app/src/main/resources/keys.properties new file mode 100644 index 0000000..086c9b0 --- /dev/null +++ b/gym-manage-api/manage-app/src/main/resources/keys.properties @@ -0,0 +1,2 @@ +huifu.private-key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCNUdQE59I4bV1dIgLs2IRN3Wl8KI2yS4KpTUQWrvvlUcBTztB8rIeN5lr6Yv4VsZ1TG0FAb0D80JjxEQ1+0HizYNOQ1h2935v2r8h7rx3VTLbMOkmsg8Lb8LQRTbPaJOZfsIZMzBytYiRzWunHaMlCr800EA3q5NMj9VjHQuamxKqyzyHqfkIjA/sZ8q9atVjn8ahUjPKdrGA8b79HexHhCgOSLdK+fWw0eMCsWWYP2qECLsvZ+tjfvqSBXx1kg7womwT1VBCf+0Dx+jJPKR3mxQfz2szoucJYuXRo55kA6yCwoeNsjanLDRkPBSy3NHdKrffP6YODhRHG6KHyayyNAgMBAAECggEATIR69TEETVNCEzRgOxe9A2AYRoa6ukhSdhMFA/c5IuCR747yqh7MwtNwfVRuWRazpZUDTr0uhfT4asad9QUx5YZO54RX1EAn9XkWZ4nY8G46J/iDfapWLrp09U2KTVpfdn5hKWH3QRX7wI4AON2O49HGnSL4NjAx9q1YpYOe2bqevxmB4uD9vR/FHMjZ2qWzPaL7pId98x4DCJCiZctBQqb7gxR8EseD1Ddn2HbH5DEUjoxz2umtL+zrcGKDFBy4kM3r+10FP5jFG4fJfZFQwrzMTEkXJriau9DMRTQ83rWHzNvhdTNUuyqah74ez5V1piVEOXCPudzFsMttD2DyzQKBgQDokQH3WtcQqwFTUo+uWVVup4NoQzNZpEGIHK17afRsf9YnkoJVqjhywq1dhQT+yuyW9geJJKWpHQIAS4gGudhNK+j6d//bbvvbkOmtsdWyZ6Z2EmY8JrPkHdHAcadNJX7vrIc1xPeyci4AHhErDHc3hhD5njuSLx3DUp4iODNxKwKBgQCbjyHvP90YGjh49DWklh2NJgR2+OR6Fd0afdmwtGKeCUP5l2To6rfLBbcSRIWCON4iLME5wLLdHcm5wAQukC4dsUB5iZffp21+7hrrwUw/G0MUzFudfhZkjsDU5i+FwnVMqmsLJPUsjJTOfKsJdA2DjnwfMcgxw9FeClGveSFNJwKBgQDCpjuDECDY7oeZeYyQXGzIxKOTbEtaR8Qha/83QCM3fHd9f35evK2qP45iq6bWqnkCkMEV4/pTZNf77zvWhU2oqYvBtxYKTwW1a8BphGJbg60rPZMb3TjLQLoB3B4uz6dCaqBwPH8kd7RQnNm5siFF84vZoLozTAQZKtj3wxorKQKBgQCGKnEONHqwaw0B5T7O8VoTfxKiug/07B6C1sCGk03rF/q0rkquSKK0S/2Vl9u+cOXFe+w7r2OVKjfuKRpyPpBHs7T0HiQLFhBuRVaat2DXnN/CdG8f6rvNhwHxnYanSwx4TxN7zShYf/doEEZEJP/y01ViYkFUCpvtC+FgAo0iSQKBgFrkJGxPo6T7i5ZF7VoZWdARnypfbOl4gF9GMrXykk0fnKBDTof9Bg34a8OX54cf4yEH1ifZzVtNyzPTH9FxV88TksWRouqi1Uo6///kJI6gXROspE91TpNtNGHPgLhebA83WlFtS/H7i5G6RO+6KO0uflSImfy3Owp/ChyFUGmn +huifu.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjVHUBOfSOG1dXSIC7NiETd1pfCiNskuCqU1EFq775VHAU87QfKyHjeZa+mL+FbGdUxtBQG9A/NCY8RENftB4s2DTkNYdvd+b9q/Ie68d1Uy2zDpJrIPC2/C0EU2z2iTmX7CGTMwcrWIkc1rpx2jJQq/NNBAN6uTTI/VYx0LmpsSqss8h6n5CIwP7GfKvWrVY5/GoVIzynaxgPG+/R3sR4QoDki3Svn1sNHjArFlmD9qhAi7L2frY376kgV8dZIO8KJsE9VQQn/tA8foyTykd5sUH89rM6LnCWLl0aOeZAOsgsKHjbI2pyw0ZDwUstzR3Sq33z+mDg4URxuih8mssjQIDAQAB \ No newline at end of file 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 a76b243..05ce7cb 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 @@ -21,4 +21,17 @@ wechat: spring: elasticsearch: - uris: http://localhost:9200 \ No newline at end of file + uris: http://localhost:9200 + +payment: + huifu: + sys-id: "6666000207573586" + product-id: "XLSISV" + huifu-id: "6666000207573586" + acct-id: "F28226571" + private-key: ${huifu.private-key} + public-key: ${huifu.public-key} + create-url: "https://api.huifu.com/v4/trade/payment/create" + query-url: "https://api.huifu.com/v4/trade/payment/query" + refund-url: "https://api.huifu.com/v4/trade/refund" + notify-url: "http://localhost:8084/api/payment/notify" 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 053180d..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,4 +61,13 @@ public final class RedisKeyConstants { * appType: miniapp(小程序), mp(公众号) */ public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:"; + + // ==================== 认证模块 ==================== + + /** + * 手机短信验证码缓存 + * 格式:sms:code:{phone} + * 用途:存储登录/注册等场景的短信验证码 + */ + public static final String SMS_CODE = "sms:code:"; } \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/exception/ErrorCode.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/exception/ErrorCode.java index baea580..cd05077 100644 --- a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/exception/ErrorCode.java +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/exception/ErrorCode.java @@ -7,6 +7,7 @@ public class ErrorCode { public static final String PERMISSION_PREFIX = "PERMISSION_"; public static final String CONFLICT_PREFIX = "CONFLICT_"; public static final String SYSTEM_PREFIX = "SYSTEM_"; + public static final String AUTH_PREFIX = "AUTH_"; public static final String VALIDATION_REQUIRED = VALIDATION_PREFIX + "001"; public static final String VALIDATION_INVALID_FORMAT = VALIDATION_PREFIX + "002"; @@ -29,4 +30,7 @@ public class ErrorCode { public static final String SYSTEM_INTERNAL_ERROR = SYSTEM_PREFIX + "001"; public static final String SYSTEM_DATABASE_ERROR = SYSTEM_PREFIX + "002"; public static final String SYSTEM_NETWORK_ERROR = SYSTEM_PREFIX + "003"; + + public static final String AUTH_PHONE_ERROR = AUTH_PREFIX + "001"; + public static final String AUTH_CODE_ERROR = AUTH_PREFIX + "002"; } diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/SysUserMessageEntity.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/SysUserMessageEntity.java index b443460..9c3d13c 100644 --- a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/SysUserMessageEntity.java +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/entity/SysUserMessageEntity.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.db.entity; +import com.fasterxml.jackson.annotation.JsonFormat; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -21,10 +22,10 @@ public class SysUserMessageEntity { @Column("user_id") private Long userId; - @Column("title") + @Column("message_title") private String title; - @Column("content") + @Column("message_content") private String content; @Column("message_type") @@ -33,9 +34,13 @@ public class SysUserMessageEntity { @Column("is_read") private String isRead; - @Column("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column("created_at") private LocalDateTime createTime; + @Column("deleted_at") + private LocalDateTime deletedAt; + public Long getId() { return id; } @@ -91,4 +96,12 @@ public class SysUserMessageEntity { public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } + + public LocalDateTime getDeletedAt() { + return deletedAt; + } + + public void setDeletedAt(LocalDateTime deletedAt) { + this.deletedAt = deletedAt; + } } diff --git a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/SysUserMessageRepository.java b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/SysUserMessageRepository.java index 7548b7b..c2d6ac4 100644 --- a/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/SysUserMessageRepository.java +++ b/gym-manage-api/manage-db/src/main/java/cn/novalon/gym/manage/db/repository/SysUserMessageRepository.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.db.repository; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository; import cn.novalon.gym.manage.db.converter.SysUserMessageConverter; @@ -13,6 +15,8 @@ import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + /** * 用户消息仓储实现类 * @@ -75,6 +79,87 @@ public class SysUserMessageRepository implements ISysUserMessageRepository { return r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class); } + @Override + public Mono> findByUserIdPage(Long userId, PageRequest pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + String sort = pageRequest.getSort(); + String order = pageRequest.getOrder(); + + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isEmpty()) { + sortObj = Sort.by(Sort.Direction.fromString(order), sort); + } + + org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj); + + SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria(); + criteria.setUserId(userId); + org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria); + + return r2dbcEntityTemplate.select(SysUserMessageEntity.class) + .matching(dbQuery.with(pageable)) + .all() + .collectList() + .zipWith(r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class)) + .map(tuple -> { + long total = tuple.getT2(); + int totalPages = (int) Math.ceil((double) total / size); + List messageList = tuple.getT1().stream() + .map(sysUserMessageConverter::toDomain) + .toList(); + return new PageResponse<>(messageList, totalPages, total, page, size); + }); + } + + @Override + public Mono> findByUserIdAndIsReadPage(Long userId, String isRead, PageRequest pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + String sort = pageRequest.getSort(); + String order = pageRequest.getOrder(); + + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isEmpty()) { + sortObj = Sort.by(Sort.Direction.fromString(order), sort); + } + + org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj); + + SysUserMessageQueryCriteria criteria = new SysUserMessageQueryCriteria(); + criteria.setUserId(userId); + criteria.setIsRead(isRead); + org.springframework.data.relational.core.query.Query dbQuery = QueryUtil.getQuery(criteria); + + return r2dbcEntityTemplate.select(SysUserMessageEntity.class) + .matching(dbQuery.with(pageable)) + .all() + .collectList() + .zipWith(r2dbcEntityTemplate.count(dbQuery, SysUserMessageEntity.class)) + .map(tuple -> { + long total = tuple.getT2(); + int totalPages = (int) Math.ceil((double) total / size); + List messageList = tuple.getT1().stream() + .map(sysUserMessageConverter::toDomain) + .toList(); + return new PageResponse<>(messageList, totalPages, total, page, size); + }); + } + + @Override + public Mono markAllAsReadByUserId(Long userId) { + org.springframework.data.relational.core.query.Update update = org.springframework.data.relational.core.query.Update.update("is_read", "1"); + + org.springframework.data.relational.core.query.Query query = org.springframework.data.relational.core.query.Query.query( + org.springframework.data.relational.core.query.Criteria.where("user_id").is(userId) + .and("deleted_at").isNull() + ); + + return r2dbcEntityTemplate.update(SysUserMessageEntity.class) + .matching(query) + .apply(update); + } + @Override public Mono save(SysUserMessage message) { SysUserMessageEntity entity = sysUserMessageConverter.toEntity(message); diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index 54c4a4e..b2d1a4a 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -61,7 +61,9 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory countByUserIdAndIsRead(Long userId, String isRead); + Mono> findByUserIdPage(Long userId, PageRequest pageRequest); + + Mono> findByUserIdAndIsReadPage(Long userId, String isRead, PageRequest pageRequest); + + Mono markAllAsReadByUserId(Long userId); + Mono save(SysUserMessage message); Mono findById(Long id); diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/ISysUserMessageService.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/ISysUserMessageService.java index 587da34..bc79f0f 100644 --- a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/ISysUserMessageService.java +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/ISysUserMessageService.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.notify.core.service; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -12,6 +14,12 @@ public interface ISysUserMessageService { Flux getUnreadMessages(Long userId); + Mono> getMessagesByUserPage(Long userId, PageRequest pageRequest); + + Mono> getUnreadMessagesPage(Long userId, PageRequest pageRequest); + + Mono markAllAsRead(Long userId); + Mono createMessage(SysUserMessage message); Mono markAsRead(Long id); diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImpl.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImpl.java index 664c565..19db56c 100644 --- a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImpl.java +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/core/service/impl/SysUserMessageServiceImpl.java @@ -1,5 +1,7 @@ package cn.novalon.gym.manage.notify.core.service.impl; +import cn.novalon.gym.manage.common.dto.PageRequest; +import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; import cn.novalon.gym.manage.notify.core.repository.ISysUserMessageRepository; import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService; @@ -33,6 +35,16 @@ public class SysUserMessageServiceImpl implements ISysUserMessageService { return messageRepository.findByUserIdAndIsReadOrderByCreateTimeDesc(userId, "0"); } + @Override + public Mono> getMessagesByUserPage(Long userId, PageRequest pageRequest) { + return messageRepository.findByUserIdPage(userId, pageRequest); + } + + @Override + public Mono> getUnreadMessagesPage(Long userId, PageRequest pageRequest) { + return messageRepository.findByUserIdAndIsReadPage(userId, "0", pageRequest); + } + @Override public Mono createMessage(SysUserMessage message) { message.setCreateTime(LocalDateTime.now()); @@ -40,6 +52,11 @@ public class SysUserMessageServiceImpl implements ISysUserMessageService { return messageRepository.save(message); } + @Override + public Mono markAllAsRead(Long userId) { + return messageRepository.markAllAsReadByUserId(userId); + } + @Override public Mono markAsRead(Long id) { return messageRepository.findById(id) diff --git a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandler.java b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandler.java index 4e14d3d..14416e0 100644 --- a/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandler.java +++ b/gym-manage-api/manage-notify/src/main/java/cn/novalon/gym/manage/notify/handler/SysUserMessageHandler.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.notify.handler; +import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.notify.core.domain.SysUserMessage; import cn.novalon.gym.manage.notify.core.service.ISysUserMessageService; import org.springframework.stereotype.Component; @@ -23,6 +24,24 @@ public class SysUserMessageHandler { return ServerResponse.ok().body(messages, SysUserMessage.class); } + public Mono getMessagesByUserPage(ServerRequest request) { + Long userId = Long.parseLong(request.pathVariable("userId")); + + int page = Integer.parseInt(request.queryParam("page").orElse("0")); + int size = Integer.parseInt(request.queryParam("size").orElse("10")); + String sort = request.queryParam("sort").orElse("createTime"); + String order = request.queryParam("order").orElse("desc"); + + PageRequest pageRequest = new PageRequest(); + pageRequest.setPage(page); + pageRequest.setSize(size); + pageRequest.setSort(sort); + pageRequest.setOrder(order); + + return messageService.getMessagesByUserPage(userId, pageRequest) + .flatMap(pageResponse -> ServerResponse.ok().bodyValue(pageResponse)); + } + public Mono getUnreadCount(ServerRequest request) { Long userId = Long.parseLong(request.pathVariable("userId")); return messageService.getUnreadCount(userId) @@ -35,6 +54,24 @@ public class SysUserMessageHandler { return ServerResponse.ok().body(messages, SysUserMessage.class); } + public Mono getUnreadMessagesPage(ServerRequest request) { + Long userId = Long.parseLong(request.pathVariable("userId")); + + int page = Integer.parseInt(request.queryParam("page").orElse("0")); + int size = Integer.parseInt(request.queryParam("size").orElse("10")); + String sort = request.queryParam("sort").orElse("createTime"); + String order = request.queryParam("order").orElse("desc"); + + PageRequest pageRequest = new PageRequest(); + pageRequest.setPage(page); + pageRequest.setSize(size); + pageRequest.setSort(sort); + pageRequest.setOrder(order); + + return messageService.getUnreadMessagesPage(userId, pageRequest) + .flatMap(pageResponse -> ServerResponse.ok().bodyValue(pageResponse)); + } + public Mono createMessage(ServerRequest request) { return request.bodyToMono(SysUserMessage.class) .flatMap(messageService::createMessage) @@ -48,6 +85,12 @@ public class SysUserMessageHandler { .switchIfEmpty(ServerResponse.notFound().build()); } + public Mono markAllAsRead(ServerRequest request) { + Long userId = Long.parseLong(request.pathVariable("userId")); + return messageService.markAllAsRead(userId) + .flatMap(count -> ServerResponse.ok().bodyValue(count)); + } + public Mono deleteMessage(ServerRequest request) { Long id = Long.parseLong(request.pathVariable("id")); return messageService.deleteMessage(id) 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 1c3ea7c..5b289e1 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 @@ -58,7 +58,13 @@ public class SecurityConfig { .pathMatchers("/api/member-card-records/**").permitAll() .pathMatchers("/**").permitAll() .pathMatchers("/api/member-card-transactions/**").permitAll() - .pathMatchers("/api/checkIn/**").permitAll(); + .pathMatchers("/api/groupCourse/page").permitAll() + .pathMatchers("/api/checkIn/**").permitAll() + .pathMatchers("/api/payment/**").permitAll() + .pathMatchers("/api/payment/create").permitAll() + .pathMatchers("/api/payment/query").permitAll() + .pathMatchers("/api/payment/notify").permitAll() + .pathMatchers("/api/payment/refund").permitAll(); if (isDevOrTest) { diff --git a/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/TokenGenerator.java b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/TokenGenerator.java new file mode 100644 index 0000000..8632ad7 --- /dev/null +++ b/gym-manage-api/manage-sys/src/test/java/cn/novalon/gym/manage/sys/TokenGenerator.java @@ -0,0 +1,46 @@ +package cn.novalon.gym.manage.sys; + +import cn.novalon.gym.manage.sys.security.JwtTokenProvider; +import cn.novalon.gym.manage.common.config.JwtProperties; +import org.junit.jupiter.api.Test; + +public class TokenGenerator { + + @Test + public void generateTokenForUser1_Dev() { + // dev环境secret + JwtProperties jwtProperties = new JwtProperties(); + jwtProperties.setSecret("novalon-gym-manage-jwt-secret-key-for-development-only-2026"); + JwtTokenProvider provider = new JwtTokenProvider(jwtProperties); + + String token = provider.generateToken("admin", 1L); + System.out.println("【Dev环境】Token for userId=1:"); + System.out.println(token); + System.out.println(); + } + + @Test + public void generateTokenForUser1_Local() { + // local环境secret + JwtProperties jwtProperties = new JwtProperties(); + jwtProperties.setSecret("U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4"); + JwtTokenProvider provider = new JwtTokenProvider(jwtProperties); + + String token = provider.generateToken("admin", 1L); + System.out.println("【Local环境】Token for userId=1:"); + System.out.println(token); + System.out.println(); + } + + @Test + public void generateTokenForUser1_Default() { + // 默认环境secret + JwtProperties jwtProperties = new JwtProperties(); + jwtProperties.setSecret("U2FsdGVkX1+vZ5Y9QmKxL8nN3rP7tW2jH4fG6dA8sB1cE5yN0zX3qV7wM4"); + JwtTokenProvider provider = new JwtTokenProvider(jwtProperties); + + String token = provider.generateToken("admin", 1L); + System.out.println("【默认环境】Token for userId=1:"); + System.out.println(token); + } +} diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 91a2e5a..06ebb61 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -46,6 +46,8 @@ gym-groupCourse gym-checkIn gym-dataCount + gym-payment + gym-auth diff --git a/gym-manage-api/test-jar/META-INF/MANIFEST.MF b/gym-manage-api/test-jar/META-INF/MANIFEST.MF new file mode 100644 index 0000000..19b83f2 --- /dev/null +++ b/gym-manage-api/test-jar/META-INF/MANIFEST.MF @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +Created-By: Maven JAR Plugin 3.4.2 +Build-Jdk-Spec: 21 +Implementation-Title: Gym Payment +Implementation-Version: 1.0.0 + diff --git a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties new file mode 100644 index 0000000..e4cde16 --- /dev/null +++ b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties @@ -0,0 +1,3 @@ +artifactId=gym-payment +groupId=cn.novalon.gym.manage +version=1.0.0 diff --git a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml new file mode 100644 index 0000000..39709f1 --- /dev/null +++ b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-payment + jar + + Gym Payment + Payment Module - Integrates Huifu Payment Gateway + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + cn.hutool + hutool-all + 5.8.25 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.springframework.boot + spring-boot-starter-validation + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + diff --git a/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..691774a --- /dev/null +++ b/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.novalon.gym.manage.payment.config.HuifuPayConfig diff --git a/vue b/vue new file mode 160000 index 0000000..bc48e69 --- /dev/null +++ b/vue @@ -0,0 +1 @@ +Subproject commit bc48e695cb1fdc19fd04ce30cecb386658fcf3e2 -- 2.52.0 From e518d5d0a09e4abe78001f1e748d304899fae9a3 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 23 Jun 2026 23:40:49 +0800 Subject: [PATCH 3/6] =?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 --- gym-manage-api/META-INF/MANIFEST.MF | 6 - .../gym-payment/pom.properties | 3 - .../cn.novalon.gym.manage/gym-payment/pom.xml | 85 -- ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../db/migration/V1__Create_all_tables.sql | 407 ---------- gym-manage-api/gym-payment/pom.xml | 95 --- .../gym/manage/payment/config/CorsConfig.java | 23 - .../manage/payment/config/HuifuPayConfig.java | 34 - .../dto/request/PaymentCreateRequest.java | 58 -- .../dto/response/PaymentCreateResponse.java | 28 - .../dto/response/PaymentQueryResponse.java | 30 - .../dto/response/PaymentRefundResponse.java | 22 - .../payment/handler/PaymentHandler.java | 78 -- .../payment/service/IPaymentService.java | 19 - .../service/impl/PaymentServiceImpl.java | 723 ------------------ ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../gym/manage/sys/config/SecurityConfig.java | 2 +- gym-manage-api/pom.xml | 1 - gym-manage-api/test-jar/META-INF/MANIFEST.MF | 6 - .../gym-payment/pom.properties | 3 - .../cn.novalon.gym.manage/gym-payment/pom.xml | 85 -- ...ot.autoconfigure.AutoConfiguration.imports | 1 - 22 files changed, 1 insertion(+), 1710 deletions(-) delete mode 100644 gym-manage-api/META-INF/MANIFEST.MF delete mode 100644 gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties delete mode 100644 gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml delete mode 100644 gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 gym-manage-api/db/migration/V1__Create_all_tables.sql 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/CorsConfig.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.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/IPaymentService.java delete mode 100644 gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java delete mode 100644 gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 gym-manage-api/test-jar/META-INF/MANIFEST.MF delete mode 100644 gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties delete mode 100644 gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml delete mode 100644 gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/gym-manage-api/META-INF/MANIFEST.MF b/gym-manage-api/META-INF/MANIFEST.MF deleted file mode 100644 index 19b83f2..0000000 --- a/gym-manage-api/META-INF/MANIFEST.MF +++ /dev/null @@ -1,6 +0,0 @@ -Manifest-Version: 1.0 -Created-By: Maven JAR Plugin 3.4.2 -Build-Jdk-Spec: 21 -Implementation-Title: Gym Payment -Implementation-Version: 1.0.0 - diff --git a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties deleted file mode 100644 index e4cde16..0000000 --- a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=gym-payment -groupId=cn.novalon.gym.manage -version=1.0.0 diff --git a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml b/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml deleted file mode 100644 index 39709f1..0000000 --- a/gym-manage-api/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - 4.0.0 - - - cn.novalon.gym.manage - gym-manage-api - 1.0.0 - - - gym-payment - jar - - Gym Payment - Payment Module - Integrates Huifu Payment Gateway - - - - cn.novalon.gym.manage - manage-common - ${project.version} - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - org.springframework.boot - spring-boot-starter-validation - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - - - - - - diff --git a/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 691774a..0000000 --- a/gym-manage-api/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.novalon.gym.manage.payment.config.HuifuPayConfig diff --git a/gym-manage-api/db/migration/V1__Create_all_tables.sql b/gym-manage-api/db/migration/V1__Create_all_tables.sql deleted file mode 100644 index ab538c2..0000000 --- a/gym-manage-api/db/migration/V1__Create_all_tables.sql +++ /dev/null @@ -1,407 +0,0 @@ --- Novalon管理系统数据库初始化脚本 --- 版本: V1 --- 描述: 创建所有核心表结构(合并版) - --- ============================================ --- 用户与角色相关表 --- ============================================ - --- 用户表 -CREATE TABLE IF NOT EXISTS sys_user ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - email VARCHAR(100), - phone VARCHAR(20), - nickname VARCHAR(100), - status INTEGER DEFAULT 1, - role_id BIGINT, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 角色表 -CREATE TABLE IF NOT EXISTS sys_role ( - id BIGSERIAL PRIMARY KEY, - role_name VARCHAR(100) NOT NULL, - role_key VARCHAR(100) NOT NULL UNIQUE, - role_sort INTEGER DEFAULT 0, - status INTEGER DEFAULT 1, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 用户角色关联表(支持多对多关系) -CREATE TABLE IF NOT EXISTS user_role ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - created_by VARCHAR(50), - CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE, - CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, - CONSTRAINT uk_user_role UNIQUE (user_id, role_id) -); - --- ============================================ --- 权限相关表 --- ============================================ - --- 权限表 -CREATE TABLE IF NOT EXISTS sys_permission ( - id BIGSERIAL PRIMARY KEY, - permission_name VARCHAR(100) NOT NULL, - permission_code VARCHAR(100) NOT NULL UNIQUE, - resource VARCHAR(200) NOT NULL, - action VARCHAR(50) NOT NULL, - description VARCHAR(500), - status INTEGER DEFAULT 1, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 角色权限关联表 -CREATE TABLE IF NOT EXISTS sys_role_permission ( - id BIGSERIAL PRIMARY KEY, - role_id BIGINT NOT NULL, - permission_id BIGINT NOT NULL, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE, - UNIQUE (role_id, permission_id) -); - --- ============================================ --- 菜单相关表 --- ============================================ - --- 菜单表 -CREATE TABLE IF NOT EXISTS sys_menu ( - id BIGSERIAL PRIMARY KEY, - menu_name VARCHAR(50) NOT NULL, - parent_id BIGINT DEFAULT 0, - order_num INTEGER DEFAULT 0, - menu_type VARCHAR(1) DEFAULT 'C', - perms VARCHAR(100), - component VARCHAR(200), - status INTEGER DEFAULT 1, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- 字典相关表 --- ============================================ - --- 字典类型表 -CREATE TABLE IF NOT EXISTS sys_dict_type ( - id BIGSERIAL PRIMARY KEY, - dict_name VARCHAR(100) NOT NULL, - dict_type VARCHAR(100) NOT NULL UNIQUE, - status VARCHAR(1) DEFAULT '0', - remark VARCHAR(500), - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 字典数据表 -CREATE TABLE IF NOT EXISTS sys_dict_data ( - id BIGSERIAL PRIMARY KEY, - dict_sort INTEGER DEFAULT 0, - dict_label VARCHAR(100) NOT NULL, - dict_value VARCHAR(100) NOT NULL, - dict_type VARCHAR(100) NOT NULL, - css_class VARCHAR(100), - list_class VARCHAR(100), - is_default VARCHAR(1) DEFAULT 'N', - status VARCHAR(1) DEFAULT '0', - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 字典表(通用字典) -CREATE TABLE IF NOT EXISTS sys_dictionary ( - id BIGSERIAL PRIMARY KEY, - type VARCHAR(100) NOT NULL, - code VARCHAR(100) NOT NULL, - name VARCHAR(100) NOT NULL, - value VARCHAR(500), - remark VARCHAR(500), - sort INTEGER DEFAULT 0, - create_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- 系统配置表 --- ============================================ - --- 系统配置表 -CREATE TABLE IF NOT EXISTS sys_config ( - id BIGSERIAL PRIMARY KEY, - config_name VARCHAR(100) NOT NULL, - config_key VARCHAR(100) NOT NULL UNIQUE, - config_value VARCHAR(500) NOT NULL, - config_type VARCHAR(1) DEFAULT 'N', - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- 日志相关表 --- ============================================ - --- 登录日志表 -CREATE TABLE IF NOT EXISTS sys_login_log ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(50), - ip VARCHAR(50), - location VARCHAR(255), - browser VARCHAR(50), - os VARCHAR(50), - status VARCHAR(1), - message VARCHAR(255), - login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 异常日志表 -CREATE TABLE IF NOT EXISTS sys_exception_log ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(50), - title VARCHAR(100), - exception_name VARCHAR(100), - method_name VARCHAR(255), - method_params TEXT, - exception_msg TEXT, - exception_stack TEXT, - ip VARCHAR(50), - create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 操作日志表 -CREATE TABLE IF NOT EXISTS operation_log ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(50), - operation VARCHAR(100), - method VARCHAR(200), - params TEXT, - result TEXT, - ip VARCHAR(50), - duration BIGINT, - status VARCHAR(1) DEFAULT '0', - error_msg TEXT, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 审计日志表 -CREATE TABLE IF NOT EXISTS audit_log ( - id BIGSERIAL PRIMARY KEY, - entity_type VARCHAR(100) NOT NULL, - entity_id BIGINT, - operation_type VARCHAR(20) NOT NULL, - operator VARCHAR(100), - operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - before_data JSONB, - after_data JSONB, - changed_fields TEXT[], - ip_address VARCHAR(50), - user_agent TEXT, - description TEXT, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 审计日志归档表 -CREATE TABLE IF NOT EXISTS audit_log_archive ( - id BIGSERIAL PRIMARY KEY, - entity_type VARCHAR(100) NOT NULL, - entity_id BIGINT, - operation_type VARCHAR(20) NOT NULL, - operator VARCHAR(100), - operation_time TIMESTAMP, - before_data JSONB, - after_data JSONB, - changed_fields TEXT[], - ip_address VARCHAR(50), - user_agent TEXT, - description TEXT, - created_at TIMESTAMP, - archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- ============================================ --- 通知与消息表 --- ============================================ - --- 系统公告表 -CREATE TABLE IF NOT EXISTS sys_notice ( - id BIGSERIAL PRIMARY KEY, - notice_title VARCHAR(50) NOT NULL, - notice_type VARCHAR(1) NOT NULL, - notice_content TEXT, - status VARCHAR(1) DEFAULT '0', - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- 用户消息表 -CREATE TABLE IF NOT EXISTS sys_user_message ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - notice_id BIGINT, - message_title VARCHAR(255), - message_content TEXT, - is_read VARCHAR(1) DEFAULT '0', - read_time TIMESTAMP, - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- 文件管理表 --- ============================================ - --- 文件管理表 -CREATE TABLE IF NOT EXISTS sys_file ( - id BIGSERIAL PRIMARY KEY, - file_name VARCHAR(255) NOT NULL, - file_path VARCHAR(500) NOT NULL, - file_size BIGINT, - file_type VARCHAR(100), - file_extension VARCHAR(10), - storage_type VARCHAR(50), - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- OAuth2相关表 --- ============================================ - --- OAuth2客户端表 -CREATE TABLE IF NOT EXISTS oauth2_client ( - id BIGSERIAL PRIMARY KEY, - client_id VARCHAR(100) NOT NULL UNIQUE, - client_secret VARCHAR(255) NOT NULL, - client_name VARCHAR(100), - web_server_redirect_uri VARCHAR(500), - scope VARCHAR(500), - authorized_grant_types VARCHAR(500), - access_token_validity_seconds INTEGER, - refresh_token_validity_seconds INTEGER, - auto_approve VARCHAR(1) DEFAULT 'false', - enabled VARCHAR(1) DEFAULT 'true', - create_by VARCHAR(50), - update_by VARCHAR(50), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - deleted_at TIMESTAMP -); - --- ============================================ --- 表注释 --- ============================================ - -COMMENT ON TABLE sys_user IS '系统用户表'; -COMMENT ON TABLE sys_role IS '系统角色表'; -COMMENT ON TABLE user_role IS '用户角色关联表'; -COMMENT ON TABLE sys_permission IS '系统权限表'; -COMMENT ON TABLE sys_role_permission IS '角色权限关联表'; -COMMENT ON TABLE sys_menu IS '系统菜单表'; -COMMENT ON TABLE sys_dict_type IS '字典类型表'; -COMMENT ON TABLE sys_dict_data IS '字典数据表'; -COMMENT ON TABLE sys_dictionary IS '通用字典表'; -COMMENT ON TABLE sys_config IS '系统配置表'; -COMMENT ON TABLE sys_login_log IS '登录日志表'; -COMMENT ON TABLE sys_exception_log IS '异常日志表'; -COMMENT ON TABLE operation_log IS '操作日志表'; -COMMENT ON TABLE audit_log IS '审计日志表'; -COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; -COMMENT ON TABLE sys_notice IS '系统公告表'; -COMMENT ON TABLE sys_user_message IS '用户消息表'; -COMMENT ON TABLE sys_file IS '文件管理表'; -COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表'; - -COMMENT ON TABLE sys_exception_log IS '异常日志表'; -COMMENT ON COLUMN sys_exception_log.id IS '主键ID'; -COMMENT ON COLUMN sys_exception_log.username IS '操作用户'; -COMMENT ON COLUMN sys_exception_log.title IS '异常标题'; -COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称'; -COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称'; -COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数'; -COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息'; -COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈'; -COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址'; -COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间'; - -COMMENT ON TABLE audit_log IS '审计日志表'; -COMMENT ON COLUMN audit_log.id IS '主键ID'; -COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)'; -COMMENT ON COLUMN audit_log.entity_id IS '实体ID'; -COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE)'; -COMMENT ON COLUMN audit_log.operator IS '操作人'; -COMMENT ON COLUMN audit_log.operation_time IS '操作时间'; -COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)'; -COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)'; -COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表'; -COMMENT ON COLUMN audit_log.ip_address IS 'IP地址'; -COMMENT ON COLUMN audit_log.description IS '操作描述'; -COMMENT ON COLUMN audit_log.created_at IS '记录创建时间'; - -COMMENT ON TABLE audit_log_archive IS '审计日志归档表'; -COMMENT ON COLUMN audit_log_archive.id IS '主键ID'; -COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)'; -COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID'; -COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE)'; -COMMENT ON COLUMN audit_log_archive.operator IS '操作人'; -COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间'; -COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)'; -COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)'; -COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表'; -COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址'; -COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理'; -COMMENT ON COLUMN audit_log_archive.description IS '操作描述'; -COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间'; -COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间'; diff --git a/gym-manage-api/gym-payment/pom.xml b/gym-manage-api/gym-payment/pom.xml deleted file mode 100644 index b8d2fcd..0000000 --- a/gym-manage-api/gym-payment/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - 4.0.0 - - - cn.novalon.gym.manage - gym-manage-api - 1.0.0 - - - gym-payment - jar - - Gym Payment - Payment Module - Integrates Huifu Payment Gateway - - - - cn.novalon.gym.manage - manage-common - ${project.version} - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - org.bouncycastle - bcprov-jdk18on - 1.78.1 - - - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - - - org.springframework.boot - spring-boot-starter-validation - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - - - - - - diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java deleted file mode 100644 index 5669d26..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/CorsConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.novalon.gym.manage.payment.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.reactive.CorsWebFilter; -import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; - -@Configuration -public class CorsConfig { - @Bean - public CorsWebFilter corsWebFilter() { - CorsConfiguration config = new CorsConfiguration(); - config.addAllowedOrigin("*"); - config.addAllowedMethod("*"); - config.addAllowedHeader("*"); - config.setAllowCredentials(false); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return new CorsWebFilter(source); - } -} \ No newline at end of file diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java deleted file mode 100644 index c9596cc..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/config/HuifuPayConfig.java +++ /dev/null @@ -1,34 +0,0 @@ - -package cn.novalon.gym.manage.payment.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Data -@Configuration -@ConfigurationProperties(prefix = "payment.huifu") -public class HuifuPayConfig { - - private String sysId; - - private String productId; - - private String huifuId; - - private String acctId; - - private String privateKey; - - private String publicKey; - - private String createUrl; - - private String queryUrl; - - private String refundUrl; - - private String notifyUrl; - - private String version = "1.0"; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java deleted file mode 100644 index b37b8a8..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/request/PaymentCreateRequest.java +++ /dev/null @@ -1,58 +0,0 @@ - -package cn.novalon.gym.manage.payment.dto.request; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentCreateRequest { - - @NotNull(message = "会员ID不能为空") - private Long memberId; - - @NotBlank(message = "订单类型不能为空") - @Size(max = 50, message = "订单类型长度不能超过50") - private String orderType; - - @NotBlank(message = "商品描述不能为空") - @Size(max = 128, message = "商品描述长度不能超过128") - private String goodsDesc; - - @NotBlank(message = "交易金额不能为空") - private String transAmt; - - @NotBlank(message = "交易类型不能为空") - @Size(max = 16, message = "交易类型长度不能超过16") - private String tradeType; - - @Size(max = 255, message = "备注长度不能超过255") - private String remark; - - @Size(max = 9, message = "账户号长度不能超过9") - private String acctId; - - private String timeExpire; - - private String delayAcctFlag; - - private Integer feeFlag; - - @Size(max = 128, message = "禁用支付方式长度不能超过128") - private String limitPayType; - - @Size(max = 32, message = "渠道号长度不能超过32") - private String channelNo; - - private String payScene; - - private String notifyUrl; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java deleted file mode 100644 index dc89b50..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentCreateResponse.java +++ /dev/null @@ -1,28 +0,0 @@ - -package cn.novalon.gym.manage.payment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentCreateResponse { - - private String orderId; - - private String tradeType; - - private String qrCode; - - private String payInfo; - - private String transAmt; - - private String payStatus; - - private String message; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java deleted file mode 100644 index b7b0b71..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentQueryResponse.java +++ /dev/null @@ -1,30 +0,0 @@ - -package cn.novalon.gym.manage.payment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentQueryResponse { - - private String orderId; - - private String tradeType; - - private String transAmt; - - private String payStatus; - - private String outTransId; - - private String qrCode; - - private String payTime; - - private String message; -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java deleted file mode 100644 index 92b3c41..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/dto/response/PaymentRefundResponse.java +++ /dev/null @@ -1,22 +0,0 @@ - -package cn.novalon.gym.manage.payment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentRefundResponse { - - private String orderId; - - private String refundAmt; - - private String refundStatus; - - private String message; -} 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 eab9746..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/handler/PaymentHandler.java +++ /dev/null @@ -1,78 +0,0 @@ - -package cn.novalon.gym.manage.payment.handler; - -import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; -import cn.novalon.gym.manage.payment.service.IPaymentService; -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 java.util.HashMap; -import java.util.Map; - -@Slf4j -@Component -@Tag(name = "支付管理", description = "斗拱聚合支付相关操作") -public class PaymentHandler { - - private final IPaymentService paymentService; - - public PaymentHandler(IPaymentService paymentService) { - this.paymentService = paymentService; - } - - @Operation(summary = "创建支付", description = "创建支付订单,支持微信、支付宝等多种支付方式") - public Mono createPayment(ServerRequest request) { - log.info("========== PaymentHandler.createPayment 被调用 =========="); - return request.bodyToMono(PaymentCreateRequest.class) - .flatMap(paymentService::createPayment) - .flatMap(response -> ServerResponse.ok().bodyValue(response)) - .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("创建支付失败: " + e.getMessage()))); - } - - @Operation(summary = "查询支付状态", description = "根据订单号查询支付状态") - public Mono queryPayment(ServerRequest request) { - String orderId = request.pathVariable("orderId"); - return paymentService.queryPayment(orderId) - .flatMap(response -> ServerResponse.ok().bodyValue(response)) - .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse(e.getMessage()))); - } - - @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")); - return paymentService.refundPayment(orderId, refundAmt); - }) - .flatMap(response -> ServerResponse.ok().bodyValue(response)) - .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("退款失败: " + e.getMessage()))); - } - - @Operation(summary = "支付回调", description = "支付成功回调通知") - public Mono handleNotify(ServerRequest request) { - return request.bodyToMono(String.class) - .flatMap(paymentService::handleNotify) - .then(ServerResponse.ok().bodyValue(buildSuccessResponse("处理成功"))) - .onErrorResume(e -> ServerResponse.badRequest().bodyValue(buildErrorResponse("处理失败: " + e.getMessage()))); - } - - private Map buildErrorResponse(String message) { - Map response = new HashMap<>(); - response.put("success", false); - response.put("message", message); - return response; - } - - private Map buildSuccessResponse(String message) { - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", message); - return response; - } -} diff --git a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java b/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java deleted file mode 100644 index d8f70a5..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/IPaymentService.java +++ /dev/null @@ -1,19 +0,0 @@ - -package cn.novalon.gym.manage.payment.service; - -import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; -import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse; -import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse; -import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse; -import reactor.core.publisher.Mono; - -public interface IPaymentService { - - Mono createPayment(PaymentCreateRequest request); - - Mono queryPayment(String orderId); - - Mono refundPayment(String orderId, String refundAmt); - - Mono handleNotify(String notifyBody); -} 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 8cbecce..0000000 --- a/gym-manage-api/gym-payment/src/main/java/cn/novalon/gym/manage/payment/service/impl/PaymentServiceImpl.java +++ /dev/null @@ -1,723 +0,0 @@ -package cn.novalon.gym.manage.payment.service.impl; - -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.novalon.gym.manage.payment.dto.request.PaymentCreateRequest; -import cn.novalon.gym.manage.payment.dto.response.PaymentCreateResponse; -import cn.novalon.gym.manage.payment.dto.response.PaymentQueryResponse; -import cn.novalon.gym.manage.payment.dto.response.PaymentRefundResponse; -import cn.novalon.gym.manage.payment.service.IPaymentService; -import lombok.extern.slf4j.Slf4j; -import okhttp3.*; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -@Slf4j -@Service -public class PaymentServiceImpl implements IPaymentService { - - private final OkHttpClient okHttpClient; - private final Map paymentCache = new ConcurrentHashMap<>(); - - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - - // ==================== 配置 ==================== - private static final String SYS_ID = "6666000207573586"; // 代理商 - private static final String PRODUCT_ID = "XLSISV"; - private static final String HUIFU_ID = "6666000207581039"; // 商户号 - private static final String ACCT_ID = "F28308086"; - private static final String ALIPAY_CHANNEL = "hlm001"; - private static final String NOTIFY_URL = "http://localhost:8084/api/payment/notify"; - - // ===== v4 接口 ===== - private static final String CREATE_URL = "https://api.huifu.com/v4/trade/payment/create"; - private static final String QUERY_URL = "https://api.huifu.com/v4/trade/payment/query"; - private static final String REFUND_URL = "https://api.huifu.com/v4/trade/payment/refund"; - private static final String CLOSE_URL = "https://api.huifu.com/v4/trade/payment/close"; - - // ==================== 密钥 ==================== - // 商户私钥(用于签名请求) - private static final String MERCHANT_PRIVATE_KEY = - "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="; - - // 汇付公钥(用于验证回调签名) - private static final String HUIFU_PUBLIC_KEY = - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqolFneAH66z3/3gaIYIaRZOIk/UYzdsXIm0RyawYBYAOu/NJ7ul8CRIrRlt5vd98HodW2yPrXA4+VHF3AS9UE4WTDpo9qV5brhqQSr/lAuZtEwMZwUWwgdnGFMkUFd9RvyGXAqY0bsQrcQgQ6zGjZHzlMljogDR3iblG0ak5ssD2TSC2W+1cxu+id+FP6onZXlXizuClTyIRh17m7CbS6rl0P3M96MlTdCzTeBw/Y54CiegBJI2wOrm2Qa6Dg6KRc+YkaJWjuRJVJkwjk8JhSyALno9oEzuDKaAXlsQlxeIhmAy4esRrZGrMV8SG0gwUZIP8lduPjQE95lCqqJ0gQIDAQAB"; - - public PaymentServiceImpl() { - this.okHttpClient = new OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) - .build(); - - // 验证私钥是否有效 - validatePrivateKey(); - } - - private void validatePrivateKey() { - try { - log.info("========== 验证私钥 =========="); - String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", ""); - log.info("私钥长度(去空格后): {}", privateKeyBase64.length()); - - byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64); - log.info("私钥解码后长度: {} bytes", privateKeyBytes.length); - - PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PrivateKey privateKey = keyFactory.generatePrivate(spec); - log.info("私钥加载成功,算法: {}", privateKey.getAlgorithm()); - - // 测试签名 - Signature signature = Signature.getInstance("SHA256withRSA"); - signature.initSign(privateKey); - signature.update("test".getBytes(StandardCharsets.UTF_8)); - signature.sign(); - log.info("私钥签名测试成功!"); - log.info("========== 私钥验证完成 =========="); - } catch (Exception e) { - log.error("========== 私钥验证失败 ==========", e); - } - } - - // ==================== 1. 创建支付订单 ==================== - @Override - public Mono createPayment(PaymentCreateRequest request) { - log.info("========== createPayment 方法被调用 =========="); - String orderId = UUID.randomUUID().toString().replace("-", ""); - String reqDate = LocalDateTime.now().format(DATE_FORMATTER); - String reqSeqId = "RQ" + System.currentTimeMillis(); - - // ===== 构建 data 参数 ===== - Map data = new HashMap<>(); - data.put("req_date", reqDate); - data.put("req_seq_id", reqSeqId); - data.put("huifu_id", HUIFU_ID); - data.put("trade_type", "ALIPAY"); - data.put("pay_type", "APP"); // APP 支付,返回 alipay_scheme - data.put("trans_amt", request.getTransAmt() != null ? request.getTransAmt() : "1"); - data.put("goods_desc", request.getGoodsDesc() != null ? request.getGoodsDesc() : "会员卡"); - data.put("acct_id", ACCT_ID); - data.put("notify_url", NOTIFY_URL); - data.put("remark", request.getRemark() != null ? request.getRemark() : ""); - - // ===== 支付宝参数 ===== - data.put("alipay_channel", ALIPAY_CHANNEL); - - String dataJson = JSONUtil.toJsonStr(data); - String sign = generateSign(dataJson); - - Map requestBody = new HashMap<>(); - requestBody.put("sys_id", SYS_ID); - requestBody.put("product_id", PRODUCT_ID); - requestBody.put("data", dataJson); - requestBody.put("sign", sign); - - return Mono.fromCallable(() -> { - String jsonBody = JSONUtil.toJsonStr(requestBody); - log.info("========== 发起支付宝支付请求 (APP) =========="); - log.info("请求URL: {}", CREATE_URL); - log.info("请求Body: {}", jsonBody); - log.info("=================================="); - - Request httpRequest = new Request.Builder() - .url(CREATE_URL) - .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) - .addHeader("Content-Type", "application/json; charset=UTF-8") - .addHeader("Accept", "application/json") - .build(); - - try (Response response = okHttpClient.newCall(httpRequest).execute()) { - int httpCode = response.code(); - String responseBody = response.body() != null ? response.body().string() : ""; - - log.info("========== 收到响应 =========="); - log.info("HTTP状态码: {}", httpCode); - log.info("响应Body: {}", responseBody); - log.info("==============================="); - - if (httpCode >= 400) { - if (responseBody != null && !responseBody.isEmpty()) { - try { - JSONObject errorJson = JSONUtil.parseObj(responseBody); - String errorMsg = errorJson.getStr("error_msg"); - String errorCode = errorJson.getStr("error_code"); - if (errorMsg != null) { - throw new RuntimeException("支付失败: " + errorMsg + " (code: " + errorCode + ")"); - } - } catch (Exception e) { - // 忽略 - } - } - throw new RuntimeException("支付请求失败: HTTP " + httpCode); - } - - if (responseBody == null || responseBody.isEmpty()) { - throw new RuntimeException("响应体为空"); - } - - return parseCreateResponse(responseBody, orderId, request); - } catch (IOException e) { - log.error("网络请求异常", e); - throw new RuntimeException("网络请求异常: " + e.getMessage(), e); - } - }); - } - - // ==================== 2. 查询支付状态 ==================== - @Override - public Mono queryPayment(String orderId) { - PaymentInfo paymentInfo = paymentCache.get(orderId); - if (paymentInfo == null) { - return Mono.error(new RuntimeException("支付记录不存在")); - } - - String reqDate = LocalDateTime.now().format(DATE_FORMATTER); - String reqSeqId = "RQ" + System.currentTimeMillis(); - - Map data = new HashMap<>(); - data.put("req_date", reqDate); - data.put("req_seq_id", reqSeqId); - data.put("huifu_id", HUIFU_ID); - data.put("out_trans_id", paymentInfo.outTransId); - - String dataJson = JSONUtil.toJsonStr(data); - String sign = generateSign(dataJson); - - Map requestBody = new HashMap<>(); - requestBody.put("sys_id", SYS_ID); - requestBody.put("product_id", PRODUCT_ID); - requestBody.put("data", dataJson); - requestBody.put("sign", sign); - - return Mono.fromCallable(() -> { - String jsonBody = JSONUtil.toJsonStr(requestBody); - log.info("查询支付状态, orderId={}", orderId); - - Request httpRequest = new Request.Builder() - .url(QUERY_URL) - .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) - .addHeader("Content-Type", "application/json; charset=UTF-8") - .addHeader("Accept", "application/json") - .build(); - - try (Response response = okHttpClient.newCall(httpRequest).execute()) { - int httpCode = response.code(); - String responseBody = response.body() != null ? response.body().string() : ""; - - log.info("查询响应: code={}, body={}", httpCode, responseBody); - - if (httpCode >= 400) { - throw new RuntimeException("查询失败: HTTP " + httpCode); - } - - if (responseBody == null || responseBody.isEmpty()) { - throw new RuntimeException("查询响应为空"); - } - - return parseQueryResponse(responseBody, orderId); - } - }); - } - - // ==================== 3. 申请退款 ==================== - @Override - public Mono refundPayment(String orderId, String refundAmt) { - PaymentInfo paymentInfo = paymentCache.get(orderId); - if (paymentInfo == null) { - return Mono.error(new RuntimeException("支付记录不存在")); - } - - String reqDate = LocalDateTime.now().format(DATE_FORMATTER); - String reqSeqId = "RQ" + System.currentTimeMillis(); - - Map data = new HashMap<>(); - data.put("req_date", reqDate); - data.put("req_seq_id", reqSeqId); - data.put("huifu_id", HUIFU_ID); - data.put("out_trans_id", paymentInfo.outTransId); - data.put("trans_amt", refundAmt); - - String dataJson = JSONUtil.toJsonStr(data); - String sign = generateSign(dataJson); - - Map requestBody = new HashMap<>(); - requestBody.put("sys_id", SYS_ID); - requestBody.put("product_id", PRODUCT_ID); - requestBody.put("data", dataJson); - requestBody.put("sign", sign); - - return Mono.fromCallable(() -> { - String jsonBody = JSONUtil.toJsonStr(requestBody); - log.info("发起退款请求, orderId={}", orderId); - - Request httpRequest = new Request.Builder() - .url(REFUND_URL) - .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) - .addHeader("Content-Type", "application/json; charset=UTF-8") - .addHeader("Accept", "application/json") - .build(); - - try (Response response = okHttpClient.newCall(httpRequest).execute()) { - int httpCode = response.code(); - String responseBody = response.body() != null ? response.body().string() : ""; - - log.info("退款响应: code={}, body={}", httpCode, responseBody); - - if (httpCode >= 400) { - throw new RuntimeException("退款失败: HTTP " + httpCode); - } - - return parseRefundResponse(responseBody, orderId, refundAmt); - } - }); - } - - // ==================== 4. 关闭订单 ==================== - public Mono closePayment(String orderId) { - PaymentInfo paymentInfo = paymentCache.get(orderId); - if (paymentInfo == null) { - return Mono.error(new RuntimeException("支付记录不存在")); - } - - String reqDate = LocalDateTime.now().format(DATE_FORMATTER); - String reqSeqId = "RQ" + System.currentTimeMillis(); - - Map data = new HashMap<>(); - data.put("req_date", reqDate); - data.put("req_seq_id", reqSeqId); - data.put("huifu_id", HUIFU_ID); - data.put("out_trans_id", paymentInfo.outTransId); - - String dataJson = JSONUtil.toJsonStr(data); - String sign = generateSign(dataJson); - - Map requestBody = new HashMap<>(); - requestBody.put("sys_id", SYS_ID); - requestBody.put("product_id", PRODUCT_ID); - requestBody.put("data", dataJson); - requestBody.put("sign", sign); - - return Mono.fromRunnable(() -> { - try { - String jsonBody = JSONUtil.toJsonStr(requestBody); - log.info("关闭订单请求, orderId={}", orderId); - - Request httpRequest = new Request.Builder() - .url(CLOSE_URL) - .post(RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"))) - .addHeader("Content-Type", "application/json; charset=UTF-8") - .addHeader("Accept", "application/json") - .build(); - - try (Response response = okHttpClient.newCall(httpRequest).execute()) { - String responseBody = response.body() != null ? response.body().string() : ""; - log.info("关闭订单响应, code={}, body={}", response.code(), responseBody); - } - } catch (Exception e) { - log.error("关闭订单异常", e); - } - }); - } - - // ==================== 5. 处理异步通知 ==================== - @Override - public Mono handleNotify(String notifyBody) { - return Mono.fromRunnable(() -> { - log.info("========== 收到支付回调通知 =========="); - log.info("回调内容: {}", notifyBody); - - try { - JSONObject notifyJson = JSONUtil.parseObj(notifyBody); - String sign = notifyJson.getStr("sign"); - String data = notifyJson.getStr("data"); - - // 直接打印回调信息,不验证签名 - log.info("签名: {}", sign); - log.info("数据: {}", data); - - if (data != null && !data.isEmpty()) { - JSONObject dataJson = JSONUtil.parseObj(data); - String respCode = dataJson.getStr("resp_code"); - String respDesc = dataJson.getStr("resp_desc"); - String outTransId = dataJson.getStr("out_trans_id"); - String transStatus = dataJson.getStr("trans_status"); - String transAmt = dataJson.getStr("trans_amt"); - String finishDate = dataJson.getStr("finish_date"); - String finishTime = dataJson.getStr("finish_time"); - - log.info("========== 回调数据解析 =========="); - log.info("响应码: {}", respCode); - log.info("响应描述: {}", respDesc); - log.info("商户订单号: {}", outTransId); - log.info("交易状态: {}", transStatus); - log.info("交易金额: {}", transAmt); - log.info("交易完成日期: {}", finishDate); - log.info("交易完成时间: {}", finishTime); - log.info("==================================="); - - // 更新本地支付状态 - if ("S".equals(transStatus) || "TRADE_SUCCESS".equals(transStatus)) { - paymentCache.values().stream() - .filter(info -> outTransId != null && outTransId.equals(info.outTransId)) - .findFirst() - .ifPresent(info -> { - info.payStatus = transStatus; - info.payTime = LocalDateTime.now(); - log.info("支付状态更新成功, orderId={}, status={}", info.orderId, transStatus); - }); - } - } - } catch (Exception e) { - log.error("处理回调异常", e); - } - }); - } - - // ==================== 签名方法 ==================== - private String generateSign(String dataJson) { - Exception originalException = null; - System.err.println(">>>>>>>>> [PaymentServiceImpl] generateSign 开始执行 <<<<<<<<<<"); - System.err.flush(); - try { - System.err.println(">>>>>>>>> 开始签名方法 <<<<<<<<<<"); - System.err.flush(); - log.error("========== 开始签名 =========="); - - // 打印密钥的前几个字节来检查格式 - String privateKeyBase64 = MERCHANT_PRIVATE_KEY.replaceAll("\\s", ""); - byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64); - System.err.println(">>>>>>>>> 密钥前20字节(hex): " + bytesToHex(keyBytes, 20) + " <<<<<<<<<<"); - System.err.println(">>>>>>>>> 密钥长度: " + keyBytes.length + " bytes <<<<<<<<<<"); - System.err.flush(); - - // 检查是否是PKCS#8格式 (应该以30 82开头) - if (keyBytes.length > 2) { - System.err.println(">>>>>>>>> 密钥前2字节(hex): " + bytesToHex(keyBytes, 2) + " <<<<<<<<<<"); - boolean startsWith30 = (keyBytes[0] & 0xFF) == 0x30; - System.err.println(">>>>>>>>> 密钥是否以0x30开头: " + startsWith30 + " <<<<<<<<<<"); - System.err.flush(); - - // 打印更多头部字节来识别密钥类型 - if (keyBytes.length > 4) { - System.err.println(">>>>>>>>> 密钥前5字节(hex): " + bytesToHex(keyBytes, 5) + " <<<<<<<<<<"); - System.err.flush(); - // RSA PKCS#8 私钥通常以 30 82 04** 开头 - // EC PKCS#8 私钥通常以 30 82 04** 开头 (和RSA一样,需要看后面的内容区分) - // 如果是 30 82,且第4个字节是 0x00 或 0x01,可能是RSA私钥 - // 如果是 30 82,且第4个字节是 0x02 或 0x03,可能是EC私钥 - } - } - - log.info("私钥长度(去空格后): {}", privateKeyBase64.length()); - - // 检查私钥格式 - if (privateKeyBase64.length() < 100) { - throw new RuntimeException("私钥长度异常: " + privateKeyBase64.length()); - } - - log.info("私钥前50字符: {}", privateKeyBase64.substring(0, 50)); - log.info("私钥后50字符: {}", privateKeyBase64.substring(privateKeyBase64.length() - 50)); - - log.info("开始解码私钥..."); - byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64); - log.info("私钥解码后长度: {} bytes", privateKeyBytes.length); - - // 直接尝试使用PKCS8EncodedKeySpec加载 - log.info("开始生成 PrivateKey 对象..."); - PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PrivateKey privateKey = keyFactory.generatePrivate(spec); - log.info("私钥生成成功,算法: {}", privateKey.getAlgorithm()); - - log.info("开始签名..."); - Signature signature = Signature.getInstance("SHA256withRSA"); - log.info("调用 initSign..."); - signature.initSign(privateKey); - log.info("initSign 完成"); - log.info("开始 update..."); - signature.update(dataJson.getBytes(StandardCharsets.UTF_8)); - log.info("update 完成,开始 sign..."); - String sign = Base64.getEncoder().encodeToString(signature.sign()); - log.info("签名生成成功, 签名长度: {}", sign.length()); - log.info("========== 签名完成 =========="); - return sign; - } catch (RuntimeException e) { - log.error("签名生成失败(RuntimeException): {} - {}", e.getClass().getName(), e.getMessage(), e); - throw e; - } catch (Exception e) { - log.error("签名生成失败(Exception): {} - {}", e.getClass().getName(), e.getMessage(), e); - throw new RuntimeException("签名生成失败: " + e.getMessage(), e); - } - } - - private boolean verifySign(String sign, String dataJson) { - try { - String publicKeyBase64 = HUIFU_PUBLIC_KEY.replaceAll("\\s", ""); - byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64); - X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey publicKey = keyFactory.generatePublic(spec); - Signature signature = Signature.getInstance("SHA256withRSA"); - signature.initVerify(publicKey); - signature.update(dataJson.getBytes(StandardCharsets.UTF_8)); - return signature.verify(Base64.getDecoder().decode(sign)); - } catch (Exception e) { - log.error("验签失败", e); - return false; - } - } - - // ==================== 响应解析方法 ==================== - - private PaymentCreateResponse parseCreateResponse(String responseBody, String orderId, PaymentCreateRequest request) { - JSONObject responseJson = JSONUtil.parseObj(responseBody); - String dataStr = responseJson.getStr("data"); - JSONObject data = JSONUtil.parseObj(dataStr); - - String respCode = data.getStr("resp_code"); - if (!"000000".equals(respCode)) { - throw new RuntimeException("支付创建失败: " + data.getStr("resp_desc") + " (code: " + respCode + ")"); - } - - // APP 支付优先取 alipay_scheme - String payUrl = data.getStr("alipay_scheme"); - if (payUrl == null) { - payUrl = data.getStr("pay_url"); - } - if (payUrl == null) { - payUrl = data.getStr("pay_info"); - } - - PaymentInfo paymentInfo = new PaymentInfo(); - paymentInfo.orderId = orderId; - paymentInfo.memberId = request.getMemberId(); - paymentInfo.orderType = request.getOrderType(); - paymentInfo.transAmt = request.getTransAmt(); - paymentInfo.goodsDesc = request.getGoodsDesc(); - paymentInfo.payStatus = "PENDING"; - paymentInfo.outTransId = data.getStr("out_trans_id"); - paymentInfo.payUrl = payUrl; - paymentInfo.remark = request.getRemark(); - paymentCache.put(orderId, paymentInfo); - - log.info("支付创建成功, orderId={}, outTransId={}, payUrl={}", orderId, paymentInfo.outTransId, payUrl); - - return PaymentCreateResponse.builder() - .orderId(orderId) - .payInfo(payUrl) - .transAmt(request.getTransAmt()) - .payStatus("PENDING") - .message("支付创建成功") - .build(); - } - - private PaymentQueryResponse parseQueryResponse(String responseBody, String orderId) { - JSONObject responseJson = JSONUtil.parseObj(responseBody); - String dataStr = responseJson.getStr("data"); - JSONObject data = JSONUtil.parseObj(dataStr); - - String respCode = data.getStr("resp_code"); - if (!"000000".equals(respCode)) { - throw new RuntimeException("查询失败: " + data.getStr("resp_desc")); - } - - PaymentInfo paymentInfo = paymentCache.get(orderId); - if (paymentInfo != null) { - paymentInfo.payStatus = data.getStr("trans_status"); - } - - return PaymentQueryResponse.builder() - .orderId(orderId) - .tradeType(data.getStr("trade_type")) - .transAmt(data.getStr("trans_amt")) - .payStatus(data.getStr("trans_status")) - .outTransId(data.getStr("out_trans_id")) - .payTime(data.getStr("end_time")) - .message("查询成功") - .build(); - } - - private PaymentRefundResponse parseRefundResponse(String responseBody, String orderId, String refundAmt) { - JSONObject responseJson = JSONUtil.parseObj(responseBody); - String dataStr = responseJson.getStr("data"); - JSONObject data = JSONUtil.parseObj(dataStr); - - String respCode = data.getStr("resp_code"); - if (!"000000".equals(respCode)) { - throw new RuntimeException("退款失败: " + data.getStr("resp_desc")); - } - - PaymentInfo paymentInfo = paymentCache.get(orderId); - if (paymentInfo != null) { - paymentInfo.payStatus = "REFUNDED"; - } - - return PaymentRefundResponse.builder() - .orderId(orderId) - .refundAmt(refundAmt) - .refundStatus(data.getStr("trans_status")) - .message("退款成功") - .build(); - } - - // ==================== 工具方法 ==================== - private static String bytesToHex(byte[] bytes, int length) { - int len = Math.min(bytes.length, length); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < len; i++) { - sb.append(String.format("%02X ", bytes[i] & 0xFF)); - } - return sb.toString(); - } - - /** - * 将PKCS#1格式的RSA私钥转换为PKCS#8格式 - * PKCS#1: SEQUENCE { version, modulus, publicExponent, privateExponent, prime1, prime2, ... } - * PKCS#8: SEQUENCE { version, algorithmIdentifier, OCTET STRING (containing PKCS#1) } - */ - private byte[] convertPKCS1ToPKCS8(byte[] pkcs1Key) { - try { - log.info("检测到PKCS#1格式私钥,开始转换为PKCS#8格式..."); - log.info("PKCS#1密钥长度: {} bytes", pkcs1Key.length); - - // RSA算法OID: 1.2.840.113549.1.1.1 - byte[] rsaOid = new byte[] { - 0x06, 0x09, 0x2A, (byte)0x86, 0x48, (byte)0x86, (byte)0xF7, 0x0D, 0x01, 0x01, 0x01 - }; - - // 构建AlgorithmIdentifier: SEQUENCE { OID, NULL } - // SEQUENCE (2 bytes: tag + length) + OID + NULL = 15 bytes total - byte[] algorithmIdentifier = new byte[15]; - int idx = 0; - algorithmIdentifier[idx++] = 0x30; // SEQUENCE tag - algorithmIdentifier[idx++] = 0x0D; // length = 13 bytes for OID + NULL - System.arraycopy(rsaOid, 0, algorithmIdentifier, idx, rsaOid.length); - idx += rsaOid.length; - algorithmIdentifier[idx++] = 0x05; // NULL tag - algorithmIdentifier[idx++] = 0x00; // NULL value - - log.info("AlgorithmIdentifier长度: {} bytes", algorithmIdentifier.length); - - // 计算 OCTET STRING 长度编码需要的字节数 - int octetStringLengthBytes = 1; // tag - if (pkcs1Key.length > 127) { - if (pkcs1Key.length > 255) { - octetStringLengthBytes += 3; // 0x82 + 2 length bytes - } else { - octetStringLengthBytes += 2; // 0x81 + 1 length byte - } - } else { - octetStringLengthBytes += 1; // 1 length byte - } - - // 计算 version INTEGER 编码 (3 bytes: tag + length + value) - int versionBytes = 3; - - // 计算 total length of inner content (version + algorithmId + octetString) - int innerContentLength = versionBytes + algorithmIdentifier.length + octetStringLengthBytes + pkcs1Key.length; - - // 计算 outer SEQUENCE 长度编码需要的字节数 - int sequenceLengthBytes = 1; // tag - if (innerContentLength > 127) { - if (innerContentLength > 255) { - sequenceLengthBytes += 3; // 0x82 + 2 length bytes - } else { - sequenceLengthBytes += 2; // 0x81 + 1 length byte - } - } else { - sequenceLengthBytes += 1; - } - - // 分配最终数组 - int totalLen = sequenceLengthBytes + innerContentLength; - byte[] pkcs8Key = new byte[totalLen]; - log.info("PKCS#8密钥分配: {} bytes", totalLen); - - int offset = 0; - - // outer SEQUENCE - pkcs8Key[offset++] = 0x30; - if (innerContentLength > 127) { - if (innerContentLength > 255) { - pkcs8Key[offset++] = (byte)0x82; - pkcs8Key[offset++] = (byte)((innerContentLength >> 8) & 0xFF); - pkcs8Key[offset++] = (byte)(innerContentLength & 0xFF); - } else { - pkcs8Key[offset++] = (byte)0x81; - pkcs8Key[offset++] = (byte)innerContentLength; - } - } else { - pkcs8Key[offset++] = (byte)innerContentLength; - } - - // version = 0 - pkcs8Key[offset++] = 0x02; - pkcs8Key[offset++] = 0x01; - pkcs8Key[offset++] = 0x00; - - // algorithmIdentifier - System.arraycopy(algorithmIdentifier, 0, pkcs8Key, offset, algorithmIdentifier.length); - offset += algorithmIdentifier.length; - - // privateKey as OCTET STRING - pkcs8Key[offset++] = 0x04; - if (pkcs1Key.length > 127) { - if (pkcs1Key.length > 255) { - pkcs8Key[offset++] = (byte)0x82; - pkcs8Key[offset++] = (byte)((pkcs1Key.length >> 8) & 0xFF); - pkcs8Key[offset++] = (byte)(pkcs1Key.length & 0xFF); - } else { - pkcs8Key[offset++] = (byte)0x81; - pkcs8Key[offset++] = (byte)pkcs1Key.length; - } - } else { - pkcs8Key[offset++] = (byte)pkcs1Key.length; - } - - // 复制 PKCS#1 私钥 - System.arraycopy(pkcs1Key, 0, pkcs8Key, offset, pkcs1Key.length); - - log.info("PKCS#1到PKCS#8转换完成,最终长度: {} bytes", pkcs8Key.length); - return pkcs8Key; - } catch (Exception e) { - log.error("PKCS#1到PKCS#8转换失败", e); - throw new RuntimeException("密钥格式转换失败: " + e.getMessage(), e); - } - } - - // ==================== 内部缓存类 ==================== - private static class PaymentInfo { - String orderId; - Long memberId; - String orderType; - String tradeType; - String transAmt; - String goodsDesc; - String payStatus; - String outTransId; - String payUrl; - String hfSeqId; - LocalDateTime payTime; - String remark; - } -} \ No newline at end of file diff --git a/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 691774a..0000000 --- a/gym-manage-api/gym-payment/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.novalon.gym.manage.payment.config.HuifuPayConfig 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 5b289e1..e66d484 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; diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 06ebb61..8746a4a 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -46,7 +46,6 @@ gym-groupCourse gym-checkIn gym-dataCount - gym-payment gym-auth diff --git a/gym-manage-api/test-jar/META-INF/MANIFEST.MF b/gym-manage-api/test-jar/META-INF/MANIFEST.MF deleted file mode 100644 index 19b83f2..0000000 --- a/gym-manage-api/test-jar/META-INF/MANIFEST.MF +++ /dev/null @@ -1,6 +0,0 @@ -Manifest-Version: 1.0 -Created-By: Maven JAR Plugin 3.4.2 -Build-Jdk-Spec: 21 -Implementation-Title: Gym Payment -Implementation-Version: 1.0.0 - diff --git a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties deleted file mode 100644 index e4cde16..0000000 --- a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=gym-payment -groupId=cn.novalon.gym.manage -version=1.0.0 diff --git a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml b/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml deleted file mode 100644 index 39709f1..0000000 --- a/gym-manage-api/test-jar/META-INF/maven/cn.novalon.gym.manage/gym-payment/pom.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - 4.0.0 - - - cn.novalon.gym.manage - gym-manage-api - 1.0.0 - - - gym-payment - jar - - Gym Payment - Payment Module - Integrates Huifu Payment Gateway - - - - cn.novalon.gym.manage - manage-common - ${project.version} - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - com.squareup.okhttp3 - okhttp - 4.12.0 - - - org.springframework.boot - spring-boot-starter-validation - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - - - - - - diff --git a/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 691774a..0000000 --- a/gym-manage-api/test-jar/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.novalon.gym.manage.payment.config.HuifuPayConfig -- 2.52.0 From a91416917b4e7b8a9ce8ff31a1536ce9ca434659 Mon Sep 17 00:00:00 2001 From: future <1360317836@qq.com> Date: Tue, 2 Jun 2026 09:56:37 +0800 Subject: [PATCH 4/6] =?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 --- .../checkIn/config/WebSocketConfig.java | 43 +++++++++++++++++++ .../manage/checkIn/entity/SignInRecord.java | 2 +- .../checkIn/handler/CheckInHandler.java | 3 +- .../checkIn/service/ICheckInService.java | 2 +- .../service/impl/CheckServiceImpl.java | 39 ++--------------- .../checkIn/websocket/MyWebSocketHandler.java | 5 +-- .../gym/manage/checkin/CheckInModuleTest.java | 1 - .../service/impl/MemberCardServiceImpl.java | 2 +- .../common/constant/RedisKeyConstants.java | 3 ++ .../filter/JwtAuthenticationFilter.java | 2 +- .../gym/manage/sys/config/SecurityConfig.java | 1 - 11 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java new file mode 100644 index 0000000..e4d6ea1 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java @@ -0,0 +1,43 @@ +package cn.novalon.gym.manage.checkIn.config; + +import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class WebSocketConfig { + + @Autowired + private MyWebSocketHandler myWebSocketHandler; + + /** + * 注册 WebSocket 路由映射 + * 路径对应前端连接的 ws://xxx/webSocket/checkIn + */ + @Bean + public HandlerMapping webSocketMapping() { + Map map = new HashMap<>(); + map.put("/webSocket/checkIn", myWebSocketHandler); + + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setUrlMap(map); + mapping.setOrder(10); // 设置优先级 + return mapping; + } + + /** + * 注册 WebSocket 处理器适配器(必须) + */ + @Bean + public WebSocketHandlerAdapter handlerAdapter() { + return new WebSocketHandlerAdapter(); + } +} \ No newline at end of file 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 4946012..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,4 +206,4 @@ public class SignInRecord { public void restore() { this.isDelete = false; } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java index ceae33f..a8a8748 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java @@ -36,6 +36,7 @@ public class CheckInHandler { public Mono checkIn(ServerRequest request) { Long memberId = authUtil.getMemberIdOrThrow(request); + return request.bodyToMono(Map.class) .flatMap(body -> { String qrContent = (String) body.get("qrContent"); @@ -190,4 +191,4 @@ public class CheckInHandler { .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of("code", 200, "message", "success", "data", stats))); } -} \ No newline at end of file +} 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 5b4894f..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,4 +78,4 @@ public interface ICheckInService { * @return 签到统计VO */ Mono getDailySignInStats(LocalDate date); -} \ No newline at end of file +} 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 ad41210..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 @@ -2,6 +2,8 @@ package cn.novalon.gym.manage.checkIn.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.json.JSONUtil; +import cn.hutool.extra.qrcode.QrCodeUtil; +import cn.hutool.extra.qrcode.QrConfig; import cn.novalon.gym.manage.checkIn.config.QRCodeConfig; import cn.novalon.gym.manage.checkIn.constant.QRRedisKey; import cn.novalon.gym.manage.checkIn.entity.SignInRecord; @@ -19,8 +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; -import cn.hutool.extra.qrcode.QrCodeUtil; -import cn.hutool.extra.qrcode.QrConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -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 6b94625..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); @@ -199,4 +198,4 @@ public class MyWebSocketHandler implements WebSocketHandler { cleanupTimeoutConnections(); return qrContentToSink.size(); } -} \ No newline at end of file +} 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/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java index 070a938..3d87017 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java @@ -1,5 +1,6 @@ package cn.novalon.gym.manage.member.service.impl; +import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.member.entity.MemberCard; import cn.novalon.gym.manage.member.entity.MemberCardRecord; import cn.novalon.gym.manage.member.entity.MemberCardTransaction; @@ -15,7 +16,6 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository; import cn.novalon.gym.manage.member.repository.MemberCardRepository; import cn.novalon.gym.manage.member.service.IMemberCardService; import cn.novalon.gym.manage.member.service.IMemberCardTransactionService; -import cn.novalon.gym.manage.common.util.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; 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 diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index b2d1a4a..b743860 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -63,7 +63,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory Date: Tue, 9 Jun 2026 09:09:12 +0800 Subject: [PATCH 5/6] =?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/handler/CheckInHandler.java | 25 +++++ .../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 ++++++++++++++++++ .../src/main/resources/application.yml | Bin 4207 -> 8754 bytes 8 files changed, 298 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/handler/CheckInHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java index a8a8748..76b7427 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java @@ -1,7 +1,12 @@ package cn.novalon.gym.manage.checkIn.handler; import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl; +<<<<<<< HEAD import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; +======= +import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO; +import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import cn.novalon.gym.manage.sys.util.AuthUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,6 +17,7 @@ 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 cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -82,7 +88,12 @@ public class CheckInHandler { * @return */ public Mono getSignInRecords(ServerRequest request) { +<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); +======= + Long memberId = 1L; +// authUtil.getMemberIdOrThrow(request); +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -128,7 +139,12 @@ public class CheckInHandler { * @return */ public Mono getSignInStatistics(ServerRequest request) { +<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); +======= + Long memberId = 1L; +// authUtil.getMemberIdOrThrow(request); +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -153,7 +169,12 @@ public class CheckInHandler { * @return */ public Mono exportSignInRecords(ServerRequest request) { +<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); +======= + Long memberId = 1L; +// authUtil.getMemberIdOrThrow(request); +>>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -191,4 +212,8 @@ public class CheckInHandler { .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of("code", 200, "message", "success", "data", stats))); } +<<<<<<< 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-已删除'; diff --git a/gym-manage-api/manage-gateway/src/main/resources/application.yml b/gym-manage-api/manage-gateway/src/main/resources/application.yml index 240c1f027f78b6e2dc98954b50f4e5feab146a9f..1d99616c7e76cc9fc3d862e5db3217c3c195c543 100644 GIT binary patch literal 8754 zcmcJVNmCm~7>4_ts{98IIVA{VW4ut60|*&ovqcDF`$B;bf*2%%BnV06uP1q*{@irW zCdr~)Q^Sm=yT5(y=D&XjuH$apt^d2?esn*$IbGMTudhS5qW4#B(cg@54>Wq?E?m!b zH6B*9wc=Dz+Q~N{UAbfTr>pt-HT~-72KK$a4fORdeF4i;VcB+T5AcqK<=S0pMq3bu zdiI4e;sbV1BS2(*R}ilR6MTH{>U&LpS=(`Udfq?4ZVPVTjYMx2UUqhN4170!74QbR zm5kPOUwD|tb)>?036f%NxJF)*(0?vEhvIm}J<$by@O`Q8ePKfrribSbTm%?A(MU(Q zgS5&iWF}PZd8wScUxf+UF7*6aSDr%jd8Qd7(J|D@GtJ};i>Z=8s*`t;`_Qdw{ZM># zq!;v<>NAfK>QV?zcOv}l2N+lmHWMvkGK7w>VsH7GNaI)|-BMnHbdf9TFZ6`xIk!{_ ziTUUW%0_w%v)4rXx;t_kdbV7{9qReX&AWZQH{A!f>FRnratCflBOi2cx-I=()89rV zCafNr;&VpThGw?}chBt#cB^uw$@7a=p6M#uc;K5S`bN`3QG*uZD)ZMXjg?6?z%bu0 zu3eU|Pg;xJwS^gtBlTxvF~JowKNa^q$pEe0ih8_!AgSU>BYguBkT3o0JnebwTlWx0 z@{o>ng+_^sJ-_y?ejf`TzL19l|C22%wez}%i)Y~@HR-Y^?$NHr*)j}ZK{q$T#Y!R| z{58ELU`IQ$t2cT!A1nemzk9EYnn!MJFENYvKe@l%JK1SVtFhAhSBNW~I^z3*wDCr` zPi55~WECG3y?>FsKf7hw_G|Z>wEJFBe@ii|Cn}Z%eb2oVm4{mSyL;=NOA8zBg&?eI z&Or9{R5UcS{$BGJ<>4Q->JLGGDQdSh|DOjbcLbT3WqgOcFy~Ub8(P2TImhcZWfeQ3 z`;{<07xZvNm|w&j(o5xxE|AN;u$AW@G;l5H4|Ls0TQ}kd4WyL37gwJo$%ZIElAlCZ zpy){NtCI1)Kr{00k;`k^kk z(v8KY5-FEP({oOg-I6VA>&CXHlR#G-_k9*B<2=$qcEkc|vIt@V`ilA}r(i?$?8^3y z`$*f0uZFPFqBc^DM7cBJA~Kyn0`s2N0$ze8JQcJeUOa%TLqsJ)o$6WUfzxn`6>baX zt|CcG81sByNQPOxVqq!g-!AJ-`C6*~AmbvZDW2ISZ1}#vWDU@y!>zn5eq&hIbXh^tD%$df*%>*FxBA2t)^Tpp43cUo6 zt-(7qeb!s>`fO~y69#fg$Hy4!wo*SK$5U;`>{iC(p>UB|8a~TVgIj)Y={r2qYf;gT zJT!8}6VDkr73lM&GmtRR&PJ_Qqa$O5^@&}th4OK1k#Tq;E2Xon7eDdep)J5PtzPI0 z9e5Rv>eZ~V-u3cp_x2@&w<|ixBG=LmPdbc_*3f~P6(D=dYV@0QUSM{wiA#2Sd}d@i&%-5<^Eibxh^`I;_|@T#gQ^S^Hx*e-FZ@a2$u=FTPA`N?K zMG3Mu1qEHRYHvcLyg)WIR>*=%1w`#Jel6y-8XO{mnT2SazGF6ZRx6-V|-wEM->vV2{aqoT4RT z8>uZ5KWu+&_Be+uTC&*C=T7b`VIrJFPfL=7WvP=wY`%-$R5MJc$gPUvPS15u2U$ zSVJ;y^&*tn&UITb=k=>69nEWPUpavKpUP|gLE^TPhz94i?p&j`!-+NFpUmXTwJ_V4 zlq+ZpyRhtmSjCLqi*;{UmzPwOA<$C3vLlSL<6)Hvav*zBBUuDN8g}AS+@I#8g#8@T zYM=y&;ex8{j#}6o%g>ILY>_^oQM+=o*7Mk)Ee|2!ENYcXpInW;vQYLtx7cMFw4gOs z!{e-2i*U;EX05Eq;dIhu)@_M=rdyr0=viq9kth1WERd?LHm}FD=F4kxI=+iKdOXZ* zFO=239j$^qED%<1CIiP=20g-7NUi^T&{v(lhM^!6-X>UXYF2d2`J3 z_%Bs8a%a!?M3HgaSpi%VVeKT8J$w2Bb{~L1T&BCbD)9s`1q)r!Y|FU(UC`apSqZ!M zK(QED6|*c?=4mME;al-7ijHMPE1s{Dx7V2{Rh9cve#&Xd@>o6xnr$D0Qxn^-uAa&4 z>8zKT=~X4eInRBy6!7v8t1P-l>aYcJMgEkbEnC6z(mlL|Oa+1sud4!HQ`fqYkv4lw eU9C>7oAUy*Vmq<=qVf3u5DS`bo@)8C2>X9J1O>DJ literal 4207 zcmb7HTX)(x5Ps)Z_@Pf-OvtTGea=1@aEKc)tZhQl7mlz6sPV0m45dB$-*+VW2IR8M zLr<~h^37=Gn-Rlwi>cAlv@E6E&~Can-Bycblq75MHAq7oi2rC>jDG4Q(PJE^bgwh= z3me*bcie+Rlw}bK5GQG(WD^v_>3SdQF-p)H>ubdE4()}-D9uA9ReV%GG>xV?$E^IN zYa}$lsZ<9C5`(~zQ(|Zv&NJieEJ_0uZBoV!0De`8WE6)2Pi;-pbZv_G=I=9yDc_-{6E7(G_aCrq10Z3WPvP$uAlG@j2#H#H{`hT~{8& z7Ye5YAZnMDh^8@)c+SMvo;g?yP2ZZEPi@a~AMC!hm^tQyX^+g_$ZF`s7$Ah8GPL~m zbmIAqGeG#9u2yAM6_OR9jQ5HYxWt$>G~`WJhu6o4D-`gQ8rsEy$`UagPLe6bTaxDN z0Yy0!zzRhS7jk9J1~|+!>|&Os3Bz|7g_u^Rj#H%4+)on@cc5Ddlp_>oB`(rM;hNWb zLvtjC+W;lQ>8`_88U+4b9v-%)q;3P0+7q=`NK0H63|F!*g{q*IZ z`2OeUCUGw5^fRAde80O|4L^nE4eOl;*7e@${(8Lp@$h*!zWNu;;-47-zoOg=)7InE zc1_=&IL6JVtFHJHg3oxbG7moKKC2r9`VBF*ODWX20(WmcEi9++K23#{#nkk?`NSO< z!i?w*sEew-06~J55k63MC%yy#Rz!Ua3{G>bv^9-D2I&?>bsdCg4;54UuGDSEhiWrN zi-Gy%8J7@8OXOH+MkFROuLl4iZfh~J$F~21>vfbaYq!#XzDlW{VMpt)+2&2%spZ;}fpN~t$T^8I5Q`q>^^Kjn^zE@VnfZpiHAq)hCh8r; z_OwP@di)b3k|qJx0~Bmlp+-OcZ8-x}GT4?XSQdBzzKXpVqG&o&rEeABK!kYts@Pnb{$rV32A zHAx!3DPDskOw}hC#7j(-B*aNzMm9ALj}CsdOD317t{#)y~Qnz zI#C>=U5)FaHCdGI;GUxBU3G6XnZuopM#=#2iR#eWLdgklfvBwvW&lJm{6#@7z+u3L z9EW<6i@VU%l|;tGp0q*HN>4MK=whV9F#PblnWWz0&K%t?CR59i7(FPd5*`slPT!Fv zgi!fDdqLS7*#p}dF6Oo~fGGW^sb%yhGY8Ucde-1YOKN31hH+%6tZrpN)K#3v$MKy8 z1HE(<#tuQbR0)QJyp@Z&2?bN-_q;0;EcT;nd{*PGkwh5gt{9rf<4$%z+)JEs*^4JC zk^y|*8c%(ga4OUTyhi~lM~5J=%93-dHPIazFK)4V$#V#QUuYfL=nj+u8pe1u&w_0imk6{H9k~j&o z6!wW?m)+r|(wA#d$_kbj^F^#3L6nD3zXjDvf`fIHl7O{wvTXy!tzq$Q3$(Dx1-LEv z0EJ;5XKhf*#D65SYMR(FbwRL(R#9dHk3yjwm56{?c6x|85DruVir@)SuDTgbm(BM< zb#r2EDm<>?Nm6YBVhh>g1ha!BzKSq|o^6*Bo@n{Ph*wkt?Q2F-)p_zF-j2DA%dR;94 z6gosbYCDau9r3(S-5 Date: Wed, 24 Jun 2026 00:43:53 +0800 Subject: [PATCH 6/6] =?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 --- .../gym/manage/auth/dto/PhoneLoginDto.java | 13 +-- .../manage/auth/handler/PhoneAuthHandler.java | 5 +- .../service/impl/PhoneAuthServiceImpl.java | 78 +++++++++++++++--- .../manage/checkIn/entity/SignInRecord.java | 4 - .../checkIn/handler/CheckInHandler.java | 22 ----- .../checkIn/service/ICheckInService.java | 4 - .../service/impl/CheckServiceImpl.java | 49 ----------- .../checkIn/websocket/MyWebSocketHandler.java | 78 ------------------ .../gym/manage/checkin/CheckInModuleTest.java | 3 - gym-manage-api/gym-groupCourse/pom.xml | 6 ++ .../gym/manage/groupcourse/util/OSSUtil.java | 6 +- .../common/constant/RedisKeyConstants.java | 3 - .../src/main/resources/application.yml | Bin 8754 -> 4227 bytes .../gym/manage/sys/config/SecurityConfig.java | 10 +-- 14 files changed, 89 insertions(+), 192 deletions(-) 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 1666761..c3d0758 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,7 +1,6 @@ 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; @@ -9,7 +8,7 @@ import lombok.NoArgsConstructor; /** * 手机号一键登录请求DTO - * uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端 + * uniapp官方一键登录流程:前端通过云函数获取access_token和openid传给后端换取手机号 */ @Data @Builder @@ -17,11 +16,13 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class PhoneLoginDto { - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; + @NotBlank(message = "access_token不能为空") + private String accessToken; + + @NotBlank(message = "openid不能为空") + private String openid; 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/handler/PhoneAuthHandler.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/handler/PhoneAuthHandler.java index af46968..562920a 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,6 +2,7 @@ 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; @@ -38,8 +39,8 @@ public class PhoneAuthHandler { public Mono sendSmsCode(ServerRequest request) { log.info("收到发送短信验证码请求"); - return request.bodyToMono(PhoneLoginDto.class) - .flatMap(dto -> phoneAuthService.sendSmsCode(dto.getPhone())) + return request.bodyToMono(SendCodeRequest.class) + .flatMap(req -> phoneAuthService.sendSmsCode(req.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/impl/PhoneAuthServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java index 2ad6588..468f5a1 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,5 +1,6 @@ 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; @@ -15,6 +16,13 @@ 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; @@ -35,6 +43,7 @@ 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; @@ -45,19 +54,63 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { @Override public Mono oneClickLogin(PhoneLoginDto request) { - log.info("手机号一键登录, phone: {}", request.getPhone()); + log.info("手机号一键登录请求, accessToken: {}, openid: {}", request.getAccessToken(), request.getOpenid()); - String encryptedPhone = encryptPhone(request.getPhone()); + return Mono.fromCallable(() -> getPhoneByToken(request.getAccessToken(), request.getOpenid())) + .flatMap(phone -> { + log.info("通过access_token获取手机号成功: {}", maskPhone(phone)); + String encryptedPhone = encryptPhone(phone); - return memberRepository.findByPhone(encryptedPhone) - .flatMap(existingMember -> { - log.info("手机号已注册,直接登录, memberId: {}", existingMember.getId()); - return doLogin(existingMember, false, request); + 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); + })); }) - .switchIfEmpty(Mono.defer(() -> { - log.info("手机号未注册,创建新会员"); - return createNewMemberAndLogin(request, encryptedPhone); - })); + .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); } @Override @@ -87,7 +140,6 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { .switchIfEmpty(Mono.defer(() -> { log.info("手机号未注册,创建新会员"); PhoneLoginDto registerRequest = new PhoneLoginDto(); - registerRequest.setPhone(request.getPhone()); return createNewMemberAndLogin(registerRequest, encryptedPhone); })); }); @@ -153,7 +205,7 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { vo.setNeedCompleteInfo(needCompleteInfo); vo.setNickname(member.getNickname()); vo.setAvatar(member.getAvatar()); - vo.setPhone(decryptPhone(member.getPhone())); + vo.setPhone(member.getPhone() != null ? decryptPhone(member.getPhone()) : null); return vo; } @@ -174,4 +226,4 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { return null; } } -} \ No newline at end of file +} 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/handler/CheckInHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java index 76b7427..16716f9 100644 --- a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java @@ -1,12 +1,9 @@ package cn.novalon.gym.manage.checkIn.handler; import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl; -<<<<<<< HEAD import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; -======= import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO; import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) import cn.novalon.gym.manage.sys.util.AuthUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -88,12 +85,7 @@ public class CheckInHandler { * @return */ public Mono getSignInRecords(ServerRequest request) { -<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); -======= - Long memberId = 1L; -// authUtil.getMemberIdOrThrow(request); ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -139,12 +131,7 @@ public class CheckInHandler { * @return */ public Mono getSignInStatistics(ServerRequest request) { -<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); -======= - Long memberId = 1L; -// authUtil.getMemberIdOrThrow(request); ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -169,12 +156,7 @@ public class CheckInHandler { * @return */ public Mono exportSignInRecords(ServerRequest request) { -<<<<<<< HEAD Long memberId = authUtil.getMemberIdOrThrow(request); -======= - Long memberId = 1L; -// authUtil.getMemberIdOrThrow(request); ->>>>>>> f52451d (feat: 添加签到模块和数据库表结构) String startDateStr = request.queryParam("startDate").orElse(null); String endDateStr = request.queryParam("endDate").orElse(null); @@ -212,8 +194,4 @@ public class CheckInHandler { .contentType(MediaType.APPLICATION_JSON) .bodyValue(Map.of("code", 200, "message", "success", "data", stats))); } -<<<<<<< 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/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index 2ef9fd2..12e0171 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -93,6 +93,12 @@ javase 3.5.3 + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.4 + diff --git a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java index c3cd21b..1318db2 100644 --- a/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java +++ b/gym-manage-api/gym-groupCourse/src/main/java/cn/novalon/gym/manage/groupcourse/util/OSSUtil.java @@ -1,8 +1,8 @@ package cn.novalon.gym.manage.groupcourse.util; -//import com.aliyun.oss.OSS; -//import com.aliyun.oss.OSSClientBuilder; -//import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.model.PutObjectRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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-gateway/src/main/resources/application.yml b/gym-manage-api/manage-gateway/src/main/resources/application.yml index 1d99616c7e76cc9fc3d862e5db3217c3c195c543..dfe6e2a4f20c406d9074e054a46f281150f04731 100644 GIT binary patch literal 4227 zcmb7ITX)(x5Ps)Z_@Pf-OvtTGea=1@aEKdji%m%S!V$IrHMZqSGL-i0f8UYh8<5L3 z4?T%Bmv2Th-;ByJ+hS%l3?rpXSjJ8Jrrl`pl#ygDzXmkKf&9fVV)WA_i5cUVvc1X4 zFKij-?O_KFQJO|1K!VUj%O)s>)Ac?!W0asZHrGht9okEa5zRs^m4DQK7zU%6z`Xd& zG)QQHQ>6|L6b6YSW5hBxLZsH&SwsUAZ73HO0KY0lQiel`r?O@krZGWc^Y2txFH` z3xiVv5RJ=H#4xx(BIELF$L=lqw(rdCr>5sjAKb39n2qcQ+a1`Qfm73oF+d1GrSJI7 z$=LI2XMpgTu2w}>HIfx!Ty*jixWt&(G}KL(hu6o4D-;OIEaT!pWr;Wr$H|1@Euk5I zKv4z-utE{Xxm=O69uBhiww<~V>XTgHdqU+4b9bGjg9;UutT7q>j@KRiF4|F!*g{q*IZ z`2OeMCK+9_$!9UY_VFFD8{Rn&oa_Cl{p<1a$HV8{@akVMi+`pB{K|6AO*@Yh zcWV3Ycx2stx@yZGDfo={I`iO@?bE73px+SZJIbKOHMo1{Y2l2z)2E5FvY6POHy=-X zmNX-K1L~q|FF=r>WrPour(<6M04uUSdX``r*4laVYDOd`Dz66sAZ}+daEGq{g6nlemz7&(z+6#grkD|`@YmG)gK-BO zD_hoio3ZVl3v7KwTX zu{){I79RfxSWx>@prCRD5@aR*2DZV;IssZ^ zs)%Z(>$&}r?a!vpN#RtDTgl|@JEBm>G#RSN;O)7hWu$Fr`bMnaPSn*jJ5bkq8RJlvHDi zjqGz8X?~~>bUTY%7=N;0WD8r@dAst6?%?jC=v{ShFrLFb4hGr)aE$KS%0j^tZ-J<+ z^kx7=vi?O*IKXSbkphQilF9oq=}IBva(CLGXl2qAC#D?pFd#qtt|zIpxU&bhi}A!6 zDU2SJSpkp8!DsGB5<;kApT3~%4cwkP>M!Q*s0UH{PZP)Lj%OoCyX`r>7cJ>E?ij|A zrJ~}s1zBrx79YoV8VvN;Q5ZV}Nvh^UM{~l0nN?q0bfM^ahwaJf282iA*QGZ;}t%Xz+EC=$$}%S9*8H?V5K zrq_*zUd&@y%cLYu!j!^}k?*%Vywv(?RiZp+emQgGDiTCl2=$xOpCUb2VHpW{6DQjy z(B2xB@uoxz>s^4Gk`GWAW^vjCrBwc+*w!%Q-f2qGwTzNRYj`vY?WjNm#IiF(EP!yJ z5>N!sl4AW$8C}-j4CT$qRVw$WsI#yoH{Mv+p2wS$tQ>I{t$*OBg?G5cSENH@;Mk`Zf@Y5t{IApY#HiV z+LEn^S&QKx8Rn90YSmNIe^0C3x^A{xU(`QeRC;S|rF~Q;a%a^Cc$8;?N*=0ztL^PI zP`WSc(2PUp4Ouz){|H2Qi}PnhkhBuLjI}f!SVqt3%=!u}W$Tts?+Q|c;i>c7Qg8oO J@$Aoq{{z#g{1*TK literal 8754 zcmcJVNmCm~7>4_ts{98IIVA{VW4ut60|*&ovqcDF`$B;bf*2%%BnV06uP1q*{@irW zCdr~)Q^Sm=yT5(y=D&XjuH$apt^d2?esn*$IbGMTudhS5qW4#B(cg@54>Wq?E?m!b zH6B*9wc=Dz+Q~N{UAbfTr>pt-HT~-72KK$a4fORdeF4i;VcB+T5AcqK<=S0pMq3bu zdiI4e;sbV1BS2(*R}ilR6MTH{>U&LpS=(`Udfq?4ZVPVTjYMx2UUqhN4170!74QbR zm5kPOUwD|tb)>?036f%NxJF)*(0?vEhvIm}J<$by@O`Q8ePKfrribSbTm%?A(MU(Q zgS5&iWF}PZd8wScUxf+UF7*6aSDr%jd8Qd7(J|D@GtJ};i>Z=8s*`t;`_Qdw{ZM># zq!;v<>NAfK>QV?zcOv}l2N+lmHWMvkGK7w>VsH7GNaI)|-BMnHbdf9TFZ6`xIk!{_ ziTUUW%0_w%v)4rXx;t_kdbV7{9qReX&AWZQH{A!f>FRnratCflBOi2cx-I=()89rV zCafNr;&VpThGw?}chBt#cB^uw$@7a=p6M#uc;K5S`bN`3QG*uZD)ZMXjg?6?z%bu0 zu3eU|Pg;xJwS^gtBlTxvF~JowKNa^q$pEe0ih8_!AgSU>BYguBkT3o0JnebwTlWx0 z@{o>ng+_^sJ-_y?ejf`TzL19l|C22%wez}%i)Y~@HR-Y^?$NHr*)j}ZK{q$T#Y!R| z{58ELU`IQ$t2cT!A1nemzk9EYnn!MJFENYvKe@l%JK1SVtFhAhSBNW~I^z3*wDCr` zPi55~WECG3y?>FsKf7hw_G|Z>wEJFBe@ii|Cn}Z%eb2oVm4{mSyL;=NOA8zBg&?eI z&Or9{R5UcS{$BGJ<>4Q->JLGGDQdSh|DOjbcLbT3WqgOcFy~Ub8(P2TImhcZWfeQ3 z`;{<07xZvNm|w&j(o5xxE|AN;u$AW@G;l5H4|Ls0TQ}kd4WyL37gwJo$%ZIElAlCZ zpy){NtCI1)Kr{00k;`k^kk z(v8KY5-FEP({oOg-I6VA>&CXHlR#G-_k9*B<2=$qcEkc|vIt@V`ilA}r(i?$?8^3y z`$*f0uZFPFqBc^DM7cBJA~Kyn0`s2N0$ze8JQcJeUOa%TLqsJ)o$6WUfzxn`6>baX zt|CcG81sByNQPOxVqq!g-!AJ-`C6*~AmbvZDW2ISZ1}#vWDU@y!>zn5eq&hIbXh^tD%$df*%>*FxBA2t)^Tpp43cUo6 zt-(7qeb!s>`fO~y69#fg$Hy4!wo*SK$5U;`>{iC(p>UB|8a~TVgIj)Y={r2qYf;gT zJT!8}6VDkr73lM&GmtRR&PJ_Qqa$O5^@&}th4OK1k#Tq;E2Xon7eDdep)J5PtzPI0 z9e5Rv>eZ~V-u3cp_x2@&w<|ixBG=LmPdbc_*3f~P6(D=dYV@0QUSM{wiA#2Sd}d@i&%-5<^Eibxh^`I;_|@T#gQ^S^Hx*e-FZ@a2$u=FTPA`N?K zMG3Mu1qEHRYHvcLyg)WIR>*=%1w`#Jel6y-8XO{mnT2SazGF6ZRx6-V|-wEM->vV2{aqoT4RT z8>uZ5KWu+&_Be+uTC&*C=T7b`VIrJFPfL=7WvP=wY`%-$R5MJc$gPUvPS15u2U$ zSVJ;y^&*tn&UITb=k=>69nEWPUpavKpUP|gLE^TPhz94i?p&j`!-+NFpUmXTwJ_V4 zlq+ZpyRhtmSjCLqi*;{UmzPwOA<$C3vLlSL<6)Hvav*zBBUuDN8g}AS+@I#8g#8@T zYM=y&;ex8{j#}6o%g>ILY>_^oQM+=o*7Mk)Ee|2!ENYcXpInW;vQYLtx7cMFw4gOs z!{e-2i*U;EX05Eq;dIhu)@_M=rdyr0=viq9kth1WERd?LHm}FD=F4kxI=+iKdOXZ* zFO=239j$^qED%<1CIiP=20g-7NUi^T&{v(lhM^!6-X>UXYF2d2`J3 z_%Bs8a%a!?M3HgaSpi%VVeKT8J$w2Bb{~L1T&BCbD)9s`1q)r!Y|FU(UC`apSqZ!M zK(QED6|*c?=4mME;al-7ijHMPE1s{Dx7V2{Rh9cve#&Xd@>o6xnr$D0Qxn^-uAa&4 z>8zKT=~X4eInRBy6!7v8t1P-l>aYcJMgEkbEnC6z(mlL|Oa+1sud4!HQ`fqYkv4lw eU9C>7oAUy*Vmq<=qVf3u5DS`bo@)8C2>X9J1O>DJ 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 2a39369..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; @@ -21,7 +21,7 @@ public class SecurityConfig { private final OperationLogWebFilter operationLogWebFilter; private final Environment environment; - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, OperationLogWebFilter operationLogWebFilter, Environment environment) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; @@ -33,11 +33,11 @@ public class SecurityConfig { public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { String[] activeProfiles = environment.getActiveProfiles(); final boolean isDevOrTest; - + isDevOrTest = java.util.Arrays.stream(activeProfiles) .anyMatch(profile -> "dev".equals(profile) || "test".equals(profile) || "h2-test".equals(profile)); - - logger.info("SecurityConfig初始化: 当前环境={}, Swagger启用状态={}", + + logger.info("SecurityConfig初始化: 当前环境={}, Swagger启用状态={}", activeProfiles.length > 0 ? String.join(",", activeProfiles) : "default", isDevOrTest); http -- 2.52.0