diff --git a/gym-manage-api/KeyAnalyzer.java b/gym-manage-api/KeyAnalyzer.java deleted file mode 100644 index e460fe7..0000000 --- a/gym-manage-api/KeyAnalyzer.java +++ /dev/null @@ -1,202 +0,0 @@ -import java.util.Base64; -import java.math.BigInteger; -import java.io.ByteArrayInputStream; - -public class KeyAnalyzer { - public static void main(String[] args) { - String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcxAwrhZF+izz1gxQKOr4jnC05obIBHbl0DzHOcd4CaaDV7Kv1NRJKi33JhdDsW4JGgu16e5Rtzq1VzU5VWz5EKgGL46maOFwCkngJUTP/LC3JVf/wtJmYCm8xNE8C7GNNIqKzYEvUOEfXqagpVvVrGsQ9FpzFp2rP9hBmHY3yizVyPX/uT9S+af5TRxiaItj3SSJGgloaEMrnKOpb/EH7JwPSS0liAyT/NxPfOyyZHc22AvaAIOE5y+0PMUIKPuIdfpOrej3LVpO1Arc2hSgmdB+YIPSiBVYPXa6AuAmil9mpbtSikQJ7Uu7lX4JyTW4QxQ06rPFKnFWVKkzivAElAgMBAAECggEAJd0AJ37iTlMpDQ90xqe7hvRQxAu256gbQ9nrqLY97g0/KIw6WEZSPakFX6gvdvb/NzKmUyAIEKGLoh6tXdZk6qfOqc/6BeK47nIcBfwT9/zerjNUVvn34w4aHyNINieMMHQ+Id8PUZmqWH+Euz9ilVTosuyEPwUZulLvUQqwXzU5VnwVghURbUhDd+ecBJACWgemRun6d5241PQXNYAdH1k7cETd8GfIi3qclhhJrxi7tu5tq4YGCXQIoz7HCLim7GIvT0M+FRgSw2EOrHnAQNFeQ/vQbP71ttLoTxehL6Se9dfWrV5OI+Y/T7vR2F84Qt0iNbaxyJGir7siKDFIwQKBgQDXDzinx3/TasplM78pR/0CtuuKr1Ch02LOPrTosJ1qf1OohxQThowhOTxMsBlgYSKu9s1QRffUUXEqYXxd2B6lzDKfggwO6U2XxIcxWeNow0xoFfqcXYSg7Ga2sCr9uhdwxIdFQNF7SNBpT8ht4fJrRX6mWY1nHybpyTDQ4xoQNQKBgQC6m+yiOoi5JD3zVSSJq/iq5DJPA5B4aoP+t5u9lp2Q7iVO0QI5ilBlEKGE4VOU0glnXlDTfuqEYooMY85ekl4WGb3AOT0PhLL2i+gO2nlWBzf4HPzB/hibjfyPyniRM03cHkG3HXucL7Sne6FwERcfjEqjUd2cdP1l89PNrq4rMQKBgQCMmjABSWYh8/y1I5rEQ4OAJdVjC3GdC1Xa35ZpVybjvLEWSpHunhW5lvD8dllw8LC7UTI0XDpGPqTM/4VO2YBYB2PFc0Gs8g0/v0ZgFpOeJ6kpl80MM/wFNemFYTIKRoMSv/psZY9PmfBgGcBBTuquBXZjDcNr+yr2yAm5V/DvTQKBgHqRi94KoF8q5N39IKCkqhJlDH5FkxDktYoKw2rFkPzuzuZz9gghRyj6wXxsG9/2DWMt2dzw0czehFoa/CO188KEadPmRKr6uCmkP2nyKhxNZX+8WnB5G2Sg4DD6BjMpBYz8+qDx5ozx8LDJTYI0V4HLPgMD9JGdbgsXGhlREOkhAoGALr6IQOXnviWNAhCdc7rrsaMLMPbLZ1wqzWtQUG1JxDobbpzEP4CW/mvW5pMn58mSBg5qbXhyDI4fFP0CPb98QIz2tGnIzYyFzdKmF5Z1N7X1OF9O+tsSqASoBZzTqB4fr/o4mz0s9JCeriBR2LWjsbsDU13DTLsfQpWbtOnIy70="; - - System.out.println("========== 密钥分析开始 =========="); - System.out.println("密钥长度: " + privateKeyBase64.length()); - - byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64); - System.out.println("解码后长度: " + keyBytes.length + " bytes"); - - // 打印前30字节 - System.out.print("前30字节(hex): "); - for (int i = 0; i < 30 && i < keyBytes.length; i++) { - System.out.printf("%02X ", keyBytes[i]); - } - System.out.println(); - - // 解析ASN.1结构 - try { - parseASN1(keyBytes); - } catch (Exception e) { - System.out.println("解析失败: " + e.getMessage()); - e.printStackTrace(); - } - } - - static void parseASN1(byte[] keyBytes) throws Exception { - ByteArrayInputStream bis = new ByteArrayInputStream(keyBytes); - - // 读取SEQUENCE - int tag = bis.read(); - System.out.println("第一个tag: 0x" + Integer.toHexString(tag)); - - if (tag == 0x30) { - System.out.println("这是SEQUENCE"); - - // 读取长度 - int len = readASN1Length(bis); - System.out.println("SEQUENCE长度: " + len); - - // 检查下一个tag - int nextTag = bis.read(); - System.out.println("下一个tag: 0x" + Integer.toHexString(nextTag)); - - if (nextTag == 0x02) { - // INTEGER - int intLen = readASN1Length(bis); - byte[] versionBytes = new byte[intLen]; - bis.read(versionBytes); - int version = new BigInteger(versionBytes).intValue(); - System.out.println("版本INTEGER值: " + version); - - // 检查这是PKCS#8还是PKCS#1 - // PKCS#8: version=0后是SEQUENCE(算法标识符) - // PKCS#1: version=0后是INTEGER(modulus) - int afterVersionTag = bis.read(); - System.out.println("版本后的tag: 0x" + Integer.toHexString(afterVersionTag)); - - if (afterVersionTag == 0x30) { - System.out.println(">>> 这是PKCS#8格式 (version后是SEQUENCE)"); - - // 解析PKCS#8 - // AlgorithmIdentifier: SEQUENCE { OID, NULL } - int algLen = readASN1Length(bis); - System.out.println("AlgorithmIdentifier长度: " + algLen); - - // 跳过AlgorithmIdentifier内容 - byte[] algBytes = new byte[algLen]; - bis.read(algBytes); - - // 打印OID - System.out.print("OID bytes: "); - for (int i = 0; i < algBytes.length; i++) { - System.out.printf("%02X ", algBytes[i]); - } - System.out.println(); - - // 下一个应该是OCTET STRING (包含私钥) - int octetTag = bis.read(); - System.out.println("下一个tag: 0x" + Integer.toHexString(octetTag)); - - if (octetTag == 0x04) { - System.out.println("这是OCTET STRING (包含私钥数据)"); - int octetLen = readASN1Length(bis); - System.out.println("OCTET STRING长度: " + octetLen); - - // OCTET STRING内容是PKCS#1私钥 - byte[] pkcs1Bytes = new byte[octetLen]; - bis.read(pkcs1Bytes); - - System.out.print("PKCS#1私钥前20字节: "); - for (int i = 0; i < 20; i++) { - System.out.printf("%02X ", pkcs1Bytes[i]); - } - System.out.println(); - - // 解析PKCS#1私钥 - ByteArrayInputStream pkcs1Stream = new ByteArrayInputStream(pkcs1Bytes); - int pkcs1Tag = pkcs1Stream.read(); - System.out.println("PKCS#1第一个tag: 0x" + Integer.toHexString(pkcs1Tag)); - - if (pkcs1Tag == 0x30) { - int pkcs1Len = readASN1Length(pkcs1Stream); - System.out.println("PKCS#1 SEQUENCE长度: " + pkcs1Len); - - // version - int vTag = pkcs1Stream.read(); - int vLen = readASN1Length(pkcs1Stream); - byte[] vBytes = new byte[vLen]; - pkcs1Stream.read(vBytes); - System.out.println("PKCS#1版本: " + new BigInteger(vBytes)); - - // 解析私钥参数 - parsePKCS1(pkcs1Stream); - } - } - } else if (afterVersionTag == 0x02) { - System.out.println(">>> 这是PKCS#1格式 (version后是INTEGER)"); - - // 继续解析PKCS#1 - parsePKCS1(bis); - } else { - System.out.println(">>> 未知格式 (tag: 0x" + Integer.toHexString(afterVersionTag) + ")"); - } - } - } - } - - static void parsePKCS1(ByteArrayInputStream bis) throws Exception { - System.out.println("\n========== 解析PKCS#1私钥参数 =========="); - - // modulus - BigInteger modulus = readASN1Integer(bis); - System.out.println("modulus长度: " + modulus.bitLength() + " bits"); - - // publicExponent - BigInteger publicExponent = readASN1Integer(bis); - System.out.println("publicExponent: " + publicExponent); - - // privateExponent - BigInteger privateExponent = readASN1Integer(bis); - System.out.println("privateExponent长度: " + privateExponent.bitLength() + " bits"); - - // prime1 - BigInteger prime1 = readASN1Integer(bis); - System.out.println("prime1长度: " + prime1.bitLength() + " bits"); - - // prime2 - BigInteger prime2 = readASN1Integer(bis); - System.out.println("prime2长度: " + prime2.bitLength() + " bits"); - - // 验证 prime1 * prime2 == modulus - BigInteger calculatedModulus = prime1.multiply(prime2); - boolean valid = calculatedModulus.equals(modulus); - System.out.println("\n验证 prime1 * prime2 == modulus: " + valid); - - if (!valid) { - System.out.println(">>> 密钥数学关系不正确!这是无效的RSA私钥"); - System.out.println("计算得到的modulus长度: " + calculatedModulus.bitLength() + " bits"); - } - - // exponent1 - BigInteger exponent1 = readASN1Integer(bis); - System.out.println("exponent1长度: " + exponent1.bitLength() + " bits"); - - // exponent2 - BigInteger exponent2 = readASN1Integer(bis); - System.out.println("exponent2长度: " + exponent2.bitLength() + " bits"); - - // coefficient - BigInteger coefficient = readASN1Integer(bis); - System.out.println("coefficient长度: " + coefficient.bitLength() + " bits"); - - System.out.println("\n========== 解析完成 =========="); - } - - static int readASN1Length(ByteArrayInputStream bis) throws Exception { - int firstByte = bis.read(); - if ((firstByte & 0x80) == 0) { - return firstByte; - } - int numBytes = firstByte & 0x7F; - int length = 0; - for (int i = 0; i < numBytes; i++) { - length = (length << 8) | (bis.read() & 0xFF); - } - return length; - } - - static BigInteger readASN1Integer(ByteArrayInputStream bis) throws Exception { - int tag = bis.read(); - if (tag != 0x02) throw new Exception("期望INTEGER tag: 0x02, 实际: 0x" + Integer.toHexString(tag)); - int len = readASN1Length(bis); - byte[] data = new byte[len]; - bis.read(data); - return new BigInteger(1, data); - } -} \ No newline at end of file diff --git a/gym-manage-api/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-auth/pom.xml b/gym-manage-api/gym-auth/pom.xml index 42aa5be..23e8943 100644 --- a/gym-manage-api/gym-auth/pom.xml +++ b/gym-manage-api/gym-auth/pom.xml @@ -10,11 +10,12 @@ 1.0.0 + cn.novalon.gym.manage gym-auth jar Gym Auth - Phone Authentication Module - Phone Number Login Services + Authentication module for Gym Management System @@ -29,93 +30,46 @@ cn.novalon.gym.manage - manage-sys + gym-member ${project.version} cn.novalon.gym.manage - gym-member + manage-sys ${project.version} + org.springframework.boot spring-boot-starter-webflux - - org.springdoc - springdoc-openapi-starter-webflux-ui - org.springframework.boot - spring-boot-starter-security - - - org.springframework.data - spring-data-commons - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - cn.hutool - hutool-all - 5.8.25 - - - - com.aliyun - aliyun-java-sdk-dypnsapi - 1.2.12 + spring-boot-starter-validation + com.aliyun aliyun-java-sdk-core - 4.6.0 + 4.6.3 + - com.aliyun - dysmsapi20170525 - 2.0.0 + org.projectlombok + lombok + true - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - default-jar - package - - jar - - - - org.apache.maven.plugins maven-compiler-plugin 3.11.0 - 21 - 21 - - - org.projectlombok - lombok - ${lombok.version} - - + ${java.version} + ${java.version} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java deleted file mode 100644 index d8d842d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/AuthConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package cn.novalon.gym.manage.auth.config; - -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AuthConfig { -} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java index fe9e35c..31551f3 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/config/SmsProperties.java @@ -4,44 +4,20 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -/** - * 阿里云短信配置属性类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Data @Configuration @ConfigurationProperties(prefix = "alibaba.cloud.sms") public class SmsProperties { - /** - * 访问密钥ID - */ private String accessKeyId; - /** - * 访问密钥密钥 - */ private String accessKeySecret; - /** - * 短信签名名称 - */ private String signName; - /** - * 短信模板CODE - */ private String templateCode; - /** - * 短信验证码长度(默认6位) - */ - private int codeLength = 6; + private int codeLength = 4; - /** - * 短信验证码有效期(秒,默认300秒=5分钟) - */ private long codeExpireSeconds = 300; -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java index 4d21283..033da34 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneCodeLoginDto.java @@ -7,9 +7,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号验证码登录请求DTO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java index 1666761..2cc0743 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/PhoneLoginDto.java @@ -7,10 +7,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录请求DTO - * uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端 - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java deleted file mode 100644 index c16602d..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SendCodeRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 发送短信验证码请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SendCodeRequest { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java deleted file mode 100644 index d97cff5..0000000 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/dto/SmsLoginDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.novalon.gym.manage.auth.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 短信验证码登录请求DTO - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SmsLoginDto { - - @NotBlank(message = "手机号不能为空") - @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") - private String phone; - - @NotBlank(message = "验证码不能为空") - private String code; -} diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java index 2407a9b..cd2848e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/PhoneAuthService.java @@ -1,37 +1,15 @@ package cn.novalon.gym.manage.auth.service; -import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto; +import cn.novalon.gym.manage.auth.dto.PhoneLoginDto; import cn.novalon.gym.manage.auth.vo.PhoneLoginVO; import reactor.core.publisher.Mono; -/** - * 手机号认证服务接口 - */ public interface PhoneAuthService { - /** - * 手机号一键登录(uniapp官方运营商认证) - * 已注册则直接登录,未注册则自动注册后登录 - * - * @param request 登录请求 - * @return 登录响应 - */ Mono oneClickLogin(PhoneLoginDto request); - /** - * 发送短信验证码(阿里云) - * - * @param phone 手机号 - * @return 是否发送成功 - */ Mono sendSmsCode(String phone); - /** - * 手机号验证码登录(阿里云) - * - * @param request 登录请求 - * @return 登录响应 - */ Mono codeLogin(PhoneCodeLoginDto request); } \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java index 914f4ff..585074e 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/SmsService.java @@ -1,35 +1,12 @@ package cn.novalon.gym.manage.auth.service; -/** - * 短信服务接口 - * - * @author auto-generated - * @date 2026-06-20 - */ +import reactor.core.publisher.Mono; + public interface SmsService { - /** - * 发送短信验证码 - * - * @param phone 手机号 - * @return 发送结果 - */ - boolean sendVerificationCode(String phone); + Mono sendVerificationCode(String phone); - /** - * 验证短信验证码 - * - * @param phone 手机号 - * @param code 验证码 - * @return 验证结果 - */ - boolean verifyCode(String phone, String code); + Mono verifyCode(String phone, String code); - /** - * 获取验证码(用于测试或特殊场景) - * - * @param phone 手机号 - * @return 验证码 - */ - String getVerificationCode(String phone); -} + Mono getVerificationCode(String phone); +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java index 2ad6588..fe96c7f 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/PhoneAuthServiceImpl.java @@ -63,14 +63,14 @@ public class PhoneAuthServiceImpl implements PhoneAuthService { @Override public Mono sendSmsCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - return Mono.fromCallable(() -> smsService.sendVerificationCode(phone)); + return smsService.sendVerificationCode(phone); } @Override public Mono codeLogin(PhoneCodeLoginDto request) { log.info("手机号验证码登录, phone: {}", request.getPhone()); - return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode())) + return smsService.verifyCode(request.getPhone(), request.getCode()) .flatMap(verified -> { if (!verified) { log.warn("验证码验证失败, phone: {}", request.getPhone()); diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java index 2162e13..2a8b630 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/service/impl/SmsServiceImpl.java @@ -1,148 +1,198 @@ package cn.novalon.gym.manage.auth.service.impl; -import cn.hutool.core.util.RandomUtil; import cn.novalon.gym.manage.auth.config.SmsProperties; import cn.novalon.gym.manage.auth.service.SmsService; import cn.novalon.gym.manage.common.constant.RedisKeyConstants; import cn.novalon.gym.manage.common.util.RedisUtil; -import com.aliyun.dysmsapi20170525.Client; -import com.aliyun.dysmsapi20170525.models.SendSmsRequest; -import com.aliyun.dysmsapi20170525.models.SendSmsResponse; -import com.aliyun.teaopenapi.models.Config; +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; -/** - * 短信服务实现类 - * - * @author auto-generated - * @date 2026-06-20 - */ @Slf4j @Service @RequiredArgsConstructor public class SmsServiceImpl implements SmsService { + private static final long SEND_INTERVAL_SECONDS = 60; + private static final long CODE_EXPIRE_SECONDS = 300; + private final SmsProperties smsProperties; private final RedisUtil redisUtil; + private final ObjectMapper objectMapper; @Override - public boolean sendVerificationCode(String phone) { + public Mono sendVerificationCode(String phone) { log.info("发送短信验证码, phone: {}", phone); - try { - // 生成验证码 - String code = generateCode(); + String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit"; - // 发送短信 - boolean sent = sendSms(phone, code); + return redisUtil.get(rateLimitKey, Long.class) + .defaultIfEmpty(0L) + .flatMap(lastSendTime -> { + long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); - if (sent) { - // 将验证码存入Redis,5分钟过期 - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds()) - .doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s", - phone, code, smsProperties.getCodeExpireSeconds())) - .block(); + if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) { + long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime); + log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds); + return Mono.just(false); + } - log.info("短信验证码发送成功, phone: {}", phone); - return true; - } + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - log.warn("短信验证码发送失败, phone: {}", phone); - return false; + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("SendSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("SignName", smsProperties.getSignName()); + request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode()); + request.putQueryParameter("TemplateParam", "{\"code\":\"##code##\",\"min\":\"5\"}"); + request.putQueryParameter("ReturnVerifyCode", "true"); - } catch (Exception e) { - log.error("发送短信验证码异常, phone: {}", phone, e); - return false; - } + log.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}", + smsProperties.getSignName(), + smsProperties.getTemplateCode(), + "{\"code\":\"##code##\",\"min\":\"5\"}"); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String code = codeNode.asText(); + + if ("OK".equals(code)) { + JsonNode requestIdNode = jsonNode.get("RequestId"); + String requestId = requestIdNode != null ? requestIdNode.asText() : "unknown"; + log.info("短信验证码发送成功, phone: {}, requestId: {}", phone, requestId); + + // 提取验证码并存入Redis + JsonNode modelNode = jsonNode.get("Model"); + if (modelNode != null) { + JsonNode verifyCodeNode = modelNode.get("VerifyCode"); + if (verifyCodeNode != null) { + String verifyCode = verifyCodeNode.asText(); + String smsCodeKey = RedisKeyConstants.SMS_CODE + phone; + redisUtil.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe(); + log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS); + } else { + log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData); + } + } else { + log.warn("响应中未找到Model字段, 原始响应: {}", responseData); + } + + redisUtil.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe(); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.error("短信验证码发送失败, phone: {}, code: {}, message: {}", phone, code, message); + return false; + + } catch (Exception e) { + log.error("发送短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); + return false; + } + }); + }); } @Override - public boolean verifyCode(String phone, String code) { + public Mono verifyCode(String phone, String code) { log.info("验证短信验证码, phone: {}", phone); - try { - String cacheKey = RedisKeyConstants.SMS_CODE + phone; - String cachedCode = redisUtil.get(cacheKey, String.class).block(); + return Mono.fromCallable(() -> { + try { + IAcsClient client = createClient(); - if (cachedCode == null) { - log.warn("验证码已过期或不存在, phone: {}", phone); + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain("dypnsapi.aliyuncs.com"); + request.setSysVersion("2017-05-25"); + request.setSysAction("CheckSmsVerifyCode"); + request.putQueryParameter("PhoneNumber", phone); + request.putQueryParameter("VerifyCode", code); + + log.info("阿里云号码认证核验参数 - phone: {}, code: {}", phone, code); + + CommonResponse response = client.getCommonResponse(request); + String responseData = response.getData(); + log.info("阿里云号码认证核验原始响应: {}", responseData); + + JsonNode jsonNode = objectMapper.readTree(responseData); + + JsonNode codeNode = jsonNode.get("Code"); + if (codeNode == null) { + log.error("阿里云核验响应中找不到Code字段, 原始响应: {}", responseData); + return false; + } + + String responseCode = codeNode.asText(); + JsonNode modelNode = jsonNode.get("Model"); + JsonNode verifyResultNode = modelNode != null ? modelNode.get("VerifyResult") : null; + boolean verifyResult = verifyResultNode != null && "PASS".equals(verifyResultNode.asText()); + + if ("OK".equals(responseCode) && verifyResult) { + log.info("验证码验证成功, phone: {}", phone); + return true; + } + + JsonNode messageNode = jsonNode.get("Message"); + String message = messageNode != null ? messageNode.asText() : "unknown"; + log.warn("验证码验证失败, phone: {}, code: {}, message: {}, result: {}", + phone, responseCode, message, verifyResult); + return false; + + } catch (Exception e) { + log.error("验证短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e); return false; } - - if (cachedCode.equals(code)) { - // 验证成功后删除验证码 - redisUtil.delete(cacheKey).block(); - log.info("验证码验证成功, phone: {}", phone); - return true; - } - - log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode); - return false; - - } catch (Exception e) { - log.error("验证短信验证码异常, phone: {}", phone, e); - return false; - } + }); } @Override - public String getVerificationCode(String phone) { + public Mono getVerificationCode(String phone) { try { String cacheKey = RedisKeyConstants.SMS_CODE + phone; - return redisUtil.get(cacheKey, String.class).block(); + return redisUtil.get(cacheKey, String.class); } catch (Exception e) { log.error("获取验证码异常, phone: {}", phone, e); - return null; + return Mono.empty(); } } - /** - * 生成验证码 - */ - private String generateCode() { - return RandomUtil.randomNumbers(smsProperties.getCodeLength()); + private IAcsClient createClient() throws ClientException { + IClientProfile profile = DefaultProfile.getProfile( + "cn-hangzhou", + smsProperties.getAccessKeyId(), + smsProperties.getAccessKeySecret()); + DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", "Dypnsapi", "dypnsapi.aliyuncs.com"); + return new DefaultAcsClient(profile); } - - /** - * 发送短信 - */ - private boolean sendSms(String phone, String code) throws Exception { - log.info("调用阿里云短信API发送短信, phone: {}, code: {}", phone, code); - - // 创建阿里云短信客户端 - Config config = new Config() - .setAccessKeyId(smsProperties.getAccessKeyId()) - .setAccessKeySecret(smsProperties.getAccessKeySecret()) - .setEndpoint("dysmsapi.aliyuncs.com"); - - Client client = new Client(config); - - // 构建发送短信请求 - SendSmsRequest request = new SendSmsRequest() - .setPhoneNumbers(phone) - .setSignName(smsProperties.getSignName()) - .setTemplateCode(smsProperties.getTemplateCode()) - .setTemplateParam("{\"code\":\"" + code + "\"}"); - - // 发送短信 - SendSmsResponse response = client.sendSms(request); - - log.info("阿里云短信API响应, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - - // 判断发送结果 - if ("OK".equals(response.getBody().getCode())) { - return true; - } - - log.error("阿里云短信发送失败, phone: {}, code: {}, message: {}", - phone, response.getBody().getCode(), response.getBody().getMessage()); - return false; - } -} +} \ No newline at end of file diff --git a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java index f5f4651..795c103 100644 --- a/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java +++ b/gym-manage-api/gym-auth/src/main/java/cn/novalon/gym/manage/auth/vo/PhoneLoginVO.java @@ -5,9 +5,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * 手机号一键登录响应VO - */ @Data @Builder @NoArgsConstructor diff --git a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 216a5f9..beb4996 100644 --- a/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/gym-manage-api/gym-auth/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1 @@ -cn.novalon.gym.manage.auth.handler.PhoneAuthHandler -cn.novalon.gym.manage.auth.service.impl.PhoneAuthServiceImpl -cn.novalon.gym.manage.auth.service.impl.SmsServiceImpl -cn.novalon.gym.manage.auth.config.AuthConfig cn.novalon.gym.manage.auth.config.SmsProperties \ No newline at end of file diff --git a/gym-manage-api/gym-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..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 @@ -2,6 +2,8 @@ package cn.novalon.gym.manage.checkIn.handler; import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl; import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; +import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO; +import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO; import cn.novalon.gym.manage.sys.util.AuthUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,6 +14,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; @@ -36,6 +39,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 +194,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..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,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; 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..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 @@ -199,4 +199,4 @@ public class MyWebSocketHandler implements WebSocketHandler { cleanupTimeoutConnections(); return qrContentToSink.size(); } -} \ No newline at end of file +} diff --git a/gym-manage-api/gym-groupCourse/pom.xml b/gym-manage-api/gym-groupCourse/pom.xml index 2ef9fd2..bfafbbb 100644 --- a/gym-manage-api/gym-groupCourse/pom.xml +++ b/gym-manage-api/gym-groupCourse/pom.xml @@ -35,6 +35,11 @@ manage-common ${project.version} + + cn.novalon.gym.manage + manage-sys + ${project.version} + cn.novalon.gym.manage manage-db @@ -81,8 +86,8 @@ gym-member ${project.version} - - + + com.google.zxing core @@ -93,14 +98,20 @@ javase 3.5.3 + + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.1 + - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 + org.springframework.boot + spring-boot-maven-plugin diff --git a/gym-manage-api/gym-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/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java index 8983e20..5c96f8f 100644 --- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java +++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/entity/Member.java @@ -71,6 +71,22 @@ public class Member extends BaseEntity { @Column("official_open_id") private String officialOpenId; + // 阿里云号码认证OpenID + @Column("dypns_open_id") + private String dypnsOpenId; + + // 身份证号码(AES加密存储) + @Column("id_card") + private String idCard; + + // 真实姓名(AES加密存储) + @Column("real_name") + private String realName; + + // 注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权 + @Column("register_channel") + private String registerChannel; + // 软删除 @Column("is_deleted") private Boolean isDeleted; diff --git a/gym-manage-api/gym-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/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-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 6e95ea5..7a15e65 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -58,7 +58,7 @@ gym-auth ${project.version} - + org.springframework.boot spring-boot-starter-webflux diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index 482d931..2a4da75 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -1,10 +1,11 @@ package cn.novalon.gym.manage.app.config; +import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler; -import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler; + import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler; diff --git a/gym-manage-api/manage-app/src/main/resources/application-dev.yml b/gym-manage-api/manage-app/src/main/resources/application-dev.yml index 46b6a66..d14149a 100644 --- a/gym-manage-api/manage-app/src/main/resources/application-dev.yml +++ b/gym-manage-api/manage-app/src/main/resources/application-dev.yml @@ -36,3 +36,11 @@ logging: cn.novalon.manage: DEBUG org.springframework.r2dbc: DEBUG org.springframework.web: TRACE + +alibaba: + cloud: + sms: + access-key-id: LTAI5t8GhorWLu5WkEx8MDZz + access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz + sign-name: 云渚科技验证平台 + template-code: 100001 diff --git a/gym-manage-api/manage-app/src/main/resources/member-config.yml b/gym-manage-api/manage-app/src/main/resources/member-config.yml index 05ce7cb..5338737 100644 --- a/gym-manage-api/manage-app/src/main/resources/member-config.yml +++ b/gym-manage-api/manage-app/src/main/resources/member-config.yml @@ -16,8 +16,8 @@ wechat: # 手机号加密配置 phone-encryption: - secret-key: ${PHONE_ENCRYPTION_SECRET_KEY} - iv: ${PHONE_ENCRYPTION_IV} + secret-key: ${PHONE_ENCRYPTION_SECRET_KEY:P8539ANjWJWsRbVHZKhM8Q==} + iv: ${PHONE_ENCRYPTION_IV:3tHp07uMRYh1xKsIXvYJMA==} spring: elasticsearch: diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java new file mode 100644 index 0000000..d4995e8 --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/AuthConfig.java @@ -0,0 +1,19 @@ +package cn.novalon.gym.manage.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "auth") +public class AuthConfig { + + private String accessKeyId; + + private String accessKeySecret; + + private Integer tokenExpireSeconds = 86400; + + private Integer refreshTokenExpireSeconds = 604800; +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java new file mode 100644 index 0000000..7b0efbc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/response/Result.java @@ -0,0 +1,54 @@ +package cn.novalon.gym.manage.common.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + private Integer code; + + private String message; + + private T data; + + private Long timestamp; + + public static Result success(T data) { + return Result.builder() + .code(200) + .message("success") + .data(data) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result success(String message) { + return Result.builder() + .code(200) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(Integer code, String message) { + return Result.builder() + .code(code) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } + + public static Result fail(String message) { + return Result.builder() + .code(500) + .message(message) + .timestamp(System.currentTimeMillis()) + .build(); + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java new file mode 100644 index 0000000..bae13cc --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/JwtUtil.java @@ -0,0 +1,86 @@ +package cn.novalon.gym.manage.common.util; + +import cn.novalon.gym.manage.common.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtil { + + private final JwtProperties jwtProperties; + + private SecretKey getSigningKey() { + String secret = jwtProperties.getSecret(); + if (secret.length() < 32) { + StringBuilder sb = new StringBuilder(secret); + while (sb.length() < 32) { + sb.append(secret); + } + secret = sb.substring(0, 32); + } + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String userId, String phone) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration()); + + Map claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("phone", phone); + + return Jwts.builder() + .setClaims(claims) + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public String generateRefreshToken(String userId) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration() * 7); + + return Jwts.builder() + .setSubject(userId) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey()) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String getUserIdFromToken(String token) { + return parseToken(token).getSubject(); + } + + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (Exception e) { + log.warn("JWT token validation failed: {}", e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql new file mode 100644 index 0000000..ca01bc4 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_SignInRecord_table.sql @@ -0,0 +1,99 @@ +-- ============================================ +-- 签到记录表(sign_in_record) +-- ============================================ + +-- Step 1: 创建 sign_in_record 表 +CREATE TABLE IF NOT EXISTS sign_in_record ( + -- ========== 主键和基础字段 ========== + id BIGSERIAL PRIMARY KEY, -- 自增主键 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录更新时间 + + -- ========== 会员相关字段 ========== + member_id BIGINT NOT NULL, -- 会员ID,关联member_user表 + member_card_id BIGINT, -- 签到时使用的会员卡ID + + -- ========== 签到核心字段 ========== + sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间 + sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别 + sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败 + + -- ========== 验证和错误信息 ========== + verification_details TEXT, -- JSONB格式,存储会员卡验证时的快照数据 + fail_reason VARCHAR(500), -- 失败时的具体原因文案 + + -- ========== 操作人信息 ========== + operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL + operator_name VARCHAR(100), -- 操作人姓名冗余 + + -- ========== 设备和环境信息 ========== + device_info VARCHAR(200), -- 签到设备标识或型号 + ip_address VARCHAR(50), -- 客户端IP地址 + source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端 + + -- ========== 软删除字段 ========== + is_delete BOOLEAN DEFAULT FALSE -- 软删除标识:false-未删除,true-已删除 +); + +-- Step 2: 创建索引 +-- 会员ID索引(加速按会员查询签到记录) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id); + +-- 签到时间索引(加速按时间范围查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time); + +-- 签到状态索引(加速按状态筛选) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status); + +-- 会员卡ID索引(加速按会员卡查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id); + +-- 操作人ID索引(加速按操作人查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id); + +-- 签到来源索引(加速按来源统计) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source); + +-- 软删除索引(加速查询未删除的记录) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete); + +-- 复合索引:会员ID + 签到时间(加速会员签到历史查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time); + +-- 复合索引:签到状态 + 签到时间(加速统计数据查询) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time); + +-- 复合索引:签到日期(用于每日统计,使用表达式索引) +CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_date ON sign_in_record(DATE(sign_in_time)); + +-- Step 3: 添加外键约束(可选,根据业务需求决定) +-- 注意:如果member_user表使用了软删除,外键约束可能需要谨慎使用 +-- ALTER TABLE sign_in_record +-- ADD CONSTRAINT fk_sign_in_record_member +-- FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE SET NULL; + +-- Step 4: 添加注释 +COMMENT ON TABLE sign_in_record IS '会员到店签到记录表'; + +COMMENT ON COLUMN sign_in_record.id IS '自增主键'; +COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间'; +COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间'; + +COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member_user表'; +COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID'; + +COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间'; +COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别'; +COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败'; + +COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据(包含卡类型、剩余次数/金额、有效期等)'; +COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案'; + +COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL'; +COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余(避免关联查询)'; + +COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号'; +COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址'; +COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端'; + +COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除'; diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql new file mode 100644 index 0000000..05765d1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V20__Update_Member_User_For_One_Click_Login.sql @@ -0,0 +1,21 @@ +-- ============================================ +-- V20: 更新 member_user 表 - 添加阿里云号码认证字段 +-- 支持一键登录功能 +-- ============================================ + +-- 添加阿里云号码认证相关字段 +ALTER TABLE IF EXISTS member_user + ADD COLUMN IF NOT EXISTS dypns_open_id VARCHAR(100), + ADD COLUMN IF NOT EXISTS id_card VARCHAR(50), + ADD COLUMN IF NOT EXISTS real_name VARCHAR(50), + ADD COLUMN IF NOT EXISTS register_channel VARCHAR(50) DEFAULT 'SMS'; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_member_user_dypns_open_id ON member_user(dypns_open_id); +CREATE INDEX IF NOT EXISTS idx_member_user_register_channel ON member_user(register_channel); + +-- 添加字段注释 +COMMENT ON COLUMN member_user.dypns_open_id IS '阿里云号码认证OpenID(一键登录用户唯一标识)'; +COMMENT ON COLUMN member_user.id_card IS '身份证号码(AES加密存储)'; +COMMENT ON COLUMN member_user.real_name IS '真实姓名(AES加密存储)'; +COMMENT ON COLUMN member_user.register_channel IS '注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权'; diff --git a/gym-manage-api/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 "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 @@ -66,7 +66,6 @@ public class SecurityConfig { .pathMatchers("/api/payment/notify").permitAll() .pathMatchers("/api/payment/refund").permitAll(); - if (isDevOrTest) { spec.pathMatchers("/swagger-ui.html").permitAll() .pathMatchers("/swagger-ui/**").permitAll() 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