diff --git a/authcenterapp/build.gradle b/authcenterapp/build.gradle index 52e5cad..8424de6 100644 --- a/authcenterapp/build.gradle +++ b/authcenterapp/build.gradle @@ -43,6 +43,7 @@ android { } dependencies { + api project(':library') // 网络连接类库 api 'com.squareup.okhttp3:okhttp:4.4.1' // Html 解析 diff --git a/authcenterapp/build.properties b/authcenterapp/build.properties index fff8528..8c15561 100644 --- a/authcenterapp/build.properties +++ b/authcenterapp/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Jan 14 12:40:12 GMT 2026 +#Thu Jan 15 02:32:25 GMT 2026 stageCount=0 -libraryProject= +libraryProject=library baseVersion=15.0 publishVersion=15.0.0 -buildCount=44 +buildCount=47 baseBetaVersion=15.0.1 diff --git a/authcenterapp/src/main/java/cc/winboll/StorageUtils.java b/authcenterapp/src/main/java/cc/winboll/StorageUtils.java new file mode 100644 index 0000000..9dd1c38 --- /dev/null +++ b/authcenterapp/src/main/java/cc/winboll/StorageUtils.java @@ -0,0 +1,74 @@ +package cc.winboll; + +import android.content.Context; +import android.content.SharedPreferences; +import cc.winboll.LogUtils; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 10:10 + * 本地存储工具类,负责加密私钥等核心数据存储 + * 存储于应用私有SharedPreferences,仅本应用可访问,适配Android API30 + * @Author 豆包&ZhanGSKen + */ +public class StorageUtils { + private static final String TAG = "StorageUtils"; + private static final String SP_NAME = "AuthCenter_Storage"; + private static final String KEY_ENCRYPT_PRIVATE_KEY = "encrypt_private_key"; + private static SharedPreferences sp; + + /** + * 初始化存储工具 + */ + public static void init(Context context) { + if (sp == null && context != null) { + sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + } + } + + /** + * 存储加密后的应用私钥 + */ + public static boolean saveEncryptPrivateKey(String encryptKey) { + if (sp == null || encryptKey == null) { + LogUtils.w(TAG, "存储加密私钥参数无效"); + return false; + } + try { + sp.edit().putString(KEY_ENCRYPT_PRIVATE_KEY, encryptKey).commit(); + return true; + } catch (Exception e) { + LogUtils.e(TAG, "存储加密私钥失败", e); + return false; + } + } + + /** + * 获取存储的加密应用私钥 + */ + public static String getEncryptPrivateKey() { + if (sp == null) { + LogUtils.w(TAG, "存储工具未初始化"); + return null; + } + return sp.getString(KEY_ENCRYPT_PRIVATE_KEY, null); + } + + /** + * 校验存储的加密私钥与目标数据一致 + */ + public static boolean checkEncryptKeyConsistent(String targetKey) { + String saveKey = getEncryptPrivateKey(); + return targetKey != null && targetKey.equals(saveKey); + } + + /** + * 清除所有存储数据 + */ + public static void clearAllData() { + if (sp != null) { + sp.edit().clear().commit(); + } + } +} + diff --git a/authcenterapp/src/main/java/cc/winboll/manager/HeartbeatManager.java b/authcenterapp/src/main/java/cc/winboll/manager/HeartbeatManager.java new file mode 100644 index 0000000..95f88ae --- /dev/null +++ b/authcenterapp/src/main/java/cc/winboll/manager/HeartbeatManager.java @@ -0,0 +1,112 @@ +package cc.winboll.manager; + +import cc.winboll.LogUtils; +import cc.winboll.model.AuthDataModel; +import cc.winboll.utils.AuthUtils; +import cc.winboll.utils.ServerUtils; +import java.util.Timer; +import java.util.TimerTask; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 10:14 + * @Describe 心跳保活管理类,负责定时发送PING请求,处理PONG响应 + * 适配Java8,每分钟发送1次心跳,支持异常重试 + * @Author 豆包&ZhanGSKen + */ +public class HeartbeatManager { + private static final String TAG = "HeartbeatManager"; + private static final long HEARTBEAT_INTERVAL = 60 * 1000; // 1分钟 + private static Timer heartbeatTimer; + private static String serverPublicKey; // 服务器公钥,用于加密PING数据 + + /** + * 初始化心跳管理器 + */ + public static void init(String serverPubKey) { + serverPublicKey = serverPubKey; + LogUtils.d(TAG, "心跳管理器初始化完成,间隔:" + HEARTBEAT_INTERVAL + "ms"); + } + + /** + * 启动心跳任务 + */ + public static void startHeartbeat() { + stopHeartbeat(); // 先停止已有任务,避免重复 + heartbeatTimer = new Timer(); + heartbeatTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + sendHeartbeatPing(); + } + }, 0, HEARTBEAT_INTERVAL); + LogUtils.i(TAG, "心跳保活任务已启动"); + } + + /** + * 停止心跳任务 + */ + public static void stopHeartbeat() { + if (heartbeatTimer != null) { + heartbeatTimer.cancel(); + heartbeatTimer = null; + LogUtils.i(TAG, "心跳保活任务已停止"); + } + } + + /** + * 发送心跳PING请求 + */ + private static void sendHeartbeatPing() { + if (!AuthUtils.isLogin() || serverPublicKey == null) { + LogUtils.w(TAG, "未登录或服务器公钥未初始化,跳过心跳"); + return; + } + // 组装PING数据 + AuthDataModel pingModel = new AuthDataModel(); + pingModel.setAppId(AuthUtils.getAppId()); + pingModel.setShortToken(AuthUtils.getShortToken()); + pingModel.setLoginFlag(true); + String pingJson = pingModel.buildPingJson(); + if (pingJson == null) { + LogUtils.w(TAG, "PING数据组装失败"); + return; + } + // 服务器公钥加密PING数据 + String encryptPing = AuthUtils.encryptByPublicKey(pingJson, serverPublicKey); + if (encryptPing == null) { + LogUtils.w(TAG, "PING数据加密失败"); + return; + } + // 发送请求 + String response = ServerUtils.sendPingRequest(encryptPing); + if (response != null) { + handlePongResponse(response); + } else { + LogUtils.w(TAG, "心跳PING请求失败,下次重试"); + } + } + + /** + * 处理PONG响应 + */ + private static void handlePongResponse(String encryptPongData) { + // 临时私钥解密PONG数据(需确保临时私钥已存在) + String pongJson = AuthUtils.decryptByPrivateKey(encryptPongData, AuthUtils.getTempPrivateKey().toString()); + AuthDataModel pongModel = AuthDataModel.parsePongJson(pongJson); + if (pongModel == null) { + LogUtils.w(TAG, "PONG数据解析失败"); + return; + } + // 更新Token + if (pongModel.isLoginFlag() && pongModel.getShortToken() != null) { + AuthUtils.setShortToken(pongModel.getShortToken()); + LogUtils.d(TAG, "心跳保活成功,Token已更新"); + } else { + LogUtils.w(TAG, "登录态失效,停止心跳"); + AuthUtils.setLoginStatus(false); + stopHeartbeat(); + } + } +} + diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..b11c04e --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply from: '../.winboll/winboll_lib_build.gradle' +apply from: '../.winboll/winboll_lint_build.gradle' + +android { + // 适配MIUI12 + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 30 + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + api fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/library/build.properties b/library/build.properties new file mode 100644 index 0000000..8c15561 --- /dev/null +++ b/library/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Thu Jan 15 02:32:25 GMT 2026 +stageCount=0 +libraryProject=library +baseVersion=15.0 +publishVersion=15.0.0 +buildCount=47 +baseBetaVersion=15.0.1 diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..536058a --- /dev/null +++ b/library/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dbac478 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/library/src/main/java/cc/winboll/LogUtils.java b/library/src/main/java/cc/winboll/LogUtils.java new file mode 100644 index 0000000..0a3d5a1 --- /dev/null +++ b/library/src/main/java/cc/winboll/LogUtils.java @@ -0,0 +1,91 @@ +package cc.winboll; + +import java.util.Date; +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * 日志工具类,对接Java原生java.util.logging,简化分级调用+统一格式化输出 + * 适配Java8语法,兼容Android API30,支持异常堆栈打印 + * @Author 豆包&ZhanGSKen + * @Date 2026/01/14 00:00:00 + * @LastEditTime 2026/01/15 02:42:35 + */ +public class LogUtils { + // 全局日志实例,绑定当前工具类名 + private static final Logger LOGGER = Logger.getLogger(LogUtils.class.getName()); + + // 静态代码块初始化日志配置(仅执行一次,全局生效) + static { + // 关闭父处理器,避免日志重复输出 + LOGGER.setUseParentHandlers(false); + // 自定义控制台输出处理器,绑定格式化规则 + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(new CustomLogFormatter()); + LOGGER.addHandler(consoleHandler); + // 默认日志级别(全量输出,可按需调整屏蔽低级别日志) + LOGGER.setLevel(Level.ALL); + consoleHandler.setLevel(Level.ALL); + } + + // 私有化构造方法,禁止外部实例化 + private LogUtils() {} + + // 调试日志(细粒度开发调试信息,上线可屏蔽) + public static void d(String tag, String msg) { + LOGGER.fine(String.format("[%s] %s", tag, msg)); + } + + // 调试日志-重载版(带异常堆栈,便于调试阶段排查问题) + public static void d(String tag, String msg, Throwable throwable) { + LOGGER.log(Level.FINE, String.format("[%s] %s", tag, msg), throwable); + } + + // 信息日志(常规运行状态、流程节点信息) + public static void i(String tag, String msg) { + LOGGER.info(String.format("[%s] %s", tag, msg)); + } + + // 信息日志-重载版(带异常堆栈,关联信息类场景的异常链路) + public static void i(String tag, String msg, Throwable throwable) { + LOGGER.log(Level.INFO, String.format("[%s] %s", tag, msg), throwable); + } + + // 警告日志-基础版(非致命异常、潜在风险提示) + public static void w(String tag, String msg) { + LOGGER.warning(String.format("[%s] %s", tag, msg)); + } + + // 警告日志-重载版(带异常堆栈,便于排查警告根源) + public static void w(String tag, String msg, Throwable throwable) { + LOGGER.log(Level.WARNING, String.format("[%s] %s", tag, msg), throwable); + } + + // 错误日志-基础版(致命异常、核心流程错误提示) + public static void e(String tag, String msg) { + LOGGER.severe(String.format("[%s] %s", tag, msg)); + } + + // 错误日志-重载版(带异常堆栈,定位错误完整链路) + public static void e(String tag, String msg, Throwable throwable) { + LOGGER.log(Level.SEVERE, String.format("[%s] %s", tag, msg), throwable); + } + + // 自定义日志格式化器,统一输出格式,提升可读性 + static class CustomLogFormatter extends Formatter { + @Override + public String format(LogRecord record) { + // 输出格式:[时间戳] [日志级别] [线程名] [日志源] - 日志内容 + return String.format("[%1$tF %1$tT] [%2$s] [%3$s] %4$s - %5$s%n", + new Date(record.getMillis()), + record.getLevel().getName(), + Thread.currentThread().getName(), + record.getLoggerName(), + formatMessage(record)); + } + } +} + diff --git a/library/src/main/java/cc/winboll/model/AuthDataModel.java b/library/src/main/java/cc/winboll/model/AuthDataModel.java new file mode 100644 index 0000000..ce79df7 --- /dev/null +++ b/library/src/main/java/cc/winboll/model/AuthDataModel.java @@ -0,0 +1,64 @@ +package cc.winboll.model; + +import org.json.JSONObject; +import cc.winboll.LogUtils; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 10:11 + * @Describe 鉴权核心数据模型,封装PING/PONG JSON数据,负责序列化与反序列化 + * 适配Java8,支持核心字段解析与组装 + * @Author 豆包&ZhanGSKen + */ +public class AuthDataModel { + private static final String TAG = "AuthDataModel"; + // PING/PONG 核心字段 + private String appId; + private String shortToken; + private boolean loginFlag; + + public String getAppId() { return appId; } + public void setAppId(String appId) { this.appId = appId; } + public String getShortToken() { return shortToken; } + public void setShortToken(String shortToken) { this.shortToken = shortToken; } + public boolean isLoginFlag() { return loginFlag; } + public void setLoginFlag(boolean loginFlag) { this.loginFlag = loginFlag; } + + /** + * 组装PING JSON字符串 + */ + public String buildPingJson() { + try { + JSONObject json = new JSONObject(); + json.put("appId", appId); + json.put("shortToken", shortToken); + json.put("loginFlag", loginFlag); + return json.toString(); + } catch (Exception e) { + LogUtils.e(TAG, "组装PING JSON失败", e); + return null; + } + } + + /** + * 解析PONG JSON字符串 + */ + public static AuthDataModel parsePongJson(String jsonStr) { + if (jsonStr == null || jsonStr.isEmpty()) { + LogUtils.w(TAG, "PONG JSON字符串为空"); + return null; + } + try { + JSONObject json = new JSONObject(jsonStr); + AuthDataModel model = new AuthDataModel(); + model.setAppId(json.optString("appId")); + model.setShortToken(json.optString("shortToken")); + model.setLoginFlag(json.optBoolean("loginFlag")); + return model; + } catch (Exception e) { + LogUtils.e(TAG, "解析PONG JSON失败", e); + return null; + } + } +} + diff --git a/library/src/main/java/cc/winboll/utils/AuthUtils.java b/library/src/main/java/cc/winboll/utils/AuthUtils.java new file mode 100644 index 0000000..1af9910 --- /dev/null +++ b/library/src/main/java/cc/winboll/utils/AuthUtils.java @@ -0,0 +1,111 @@ +package cc.winboll.utils; + +import cc.winboll.LogUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import javax.crypto.Cipher; + +/** + * 鉴权核心工具类,负责密钥生成、加解密、登录态管理 + * 适配Java8,非对称加密采用RSA 2048位 + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 09:56:00 + */ +public class AuthUtils { + private static final String TAG = "AuthUtils"; + private static final String ALGORITHM = "RSA"; + private static final int KEY_SIZE = 2048; + // 临时存储明文私钥,使用后立即置空 + private static PrivateKey tempPrivateKey; + // 登录态核心参数 + private static String appId; + private static String shortToken; + private static boolean loginStatus = false; + + /** + * 生成RSA密钥对(公钥+私钥) + */ + public static KeyPair generateRsaKeyPair() { + try { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM); + keyGen.initialize(KEY_SIZE); + return keyGen.generateKeyPair(); + } catch (Exception e) { + LogUtils.e(TAG, "生成RSA密钥对失败", e); + return null; + } + } + + /** + * 公钥加密数据 + */ + public static String encryptByPublicKey(String data, String publicKeyStr) { + try { + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes); + PublicKey publicKey = java.security.KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encryptBytes = cipher.doFinal(data.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(encryptBytes); + } catch (Exception e) { + LogUtils.e(TAG, "公钥加密失败", e); + return null; + } + } + + /** + * 私钥解密数据 + */ + public static String decryptByPrivateKey(String data, String privateKeyStr) { + try { + byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyStr); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + PrivateKey privateKey = java.security.KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(data)); + return new String(decryptBytes, "UTF-8"); + } catch (Exception e) { + LogUtils.e(TAG, "私钥解密失败", e); + return null; + } + } + + /** + * 获取临时明文私钥 + */ + public static PrivateKey getTempPrivateKey() { + return tempPrivateKey; + } + + /** + * 临时存储明文私钥,用完需手动置空 + */ + public static void setTempPrivateKey(PrivateKey privateKey) { + tempPrivateKey = privateKey; + } + + /** + * 清空临时私钥,保障安全 + */ + public static void clearTempPrivateKey() { + tempPrivateKey = null; + } + + // 登录态相关get/set方法 + public static String getAppId() { return appId; } + public static void setAppId(String appId) { AuthUtils.appId = appId; } + public static String getShortToken() { return shortToken; } + public static void setShortToken(String shortToken) { AuthUtils.shortToken = shortToken; } + public static boolean isLogin() { return loginStatus; } + public static void setLoginStatus(boolean status) { loginStatus = status; } +} + diff --git a/library/src/main/java/cc/winboll/utils/NFCUtils.java b/library/src/main/java/cc/winboll/utils/NFCUtils.java new file mode 100644 index 0000000..437d32f --- /dev/null +++ b/library/src/main/java/cc/winboll/utils/NFCUtils.java @@ -0,0 +1,124 @@ +package cc.winboll.utils; + +import android.content.Context; +import android.nfc.FormatException; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.Ndef; +import android.nfc.tech.NdefFormatable; +import cc.winboll.LogUtils; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 10:06 + * NFC操作工具类,负责NFC功能检测、卡连接、数据读写 + * 适配Android API30,仅支持空白NFC卡操作 + * @Author 豆包&ZhanGSKen + */ +public class NFCUtils { + private static final String TAG = "NFCUtils"; + + /** + * 检测手机NFC功能是否开启 + */ + public static boolean isNfcEnabled(Context context) { + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(context); + return nfcAdapter != null && nfcAdapter.isEnabled(); + } + + /** + * 检测是否为空白NFC卡(无NDEF数据) + */ + public static boolean isBlankNfcCard(Tag tag) { + try { + Ndef ndef = Ndef.get(tag); + if (ndef != null) { + return ndef.getCachedNdefMessage() == null || ndef.getCachedNdefMessage().getRecords().length == 0; + } + NdefFormatable ndefFormatable = NdefFormatable.get(tag); + return ndefFormatable != null; + } catch (Exception e) { + LogUtils.w(TAG, "检测NFC卡是否空白失败", e); + return false; + } + } + + /** + * 向NFC卡写入加密数据(仅支持NDEF格式) + */ + public static boolean writeDataToNfc(Tag tag, String encryptData) { + if (tag == null || encryptData == null || encryptData.isEmpty()) { + LogUtils.w(TAG, "NFC写入参数无效"); + return false; + } + try { + Ndef ndef = Ndef.get(tag); + if (ndef != null) { + ndef.connect(); + if (!ndef.isWritable()) { + LogUtils.w(TAG, "NFC卡不可写入"); + return false; + } + android.nfc.NdefRecord record = new android.nfc.NdefRecord( + android.nfc.NdefRecord.TNF_WELL_KNOWN, + android.nfc.NdefRecord.RTD_TEXT, + new byte[0], + encryptData.getBytes(StandardCharsets.UTF_8) + ); + android.nfc.NdefMessage message = new android.nfc.NdefMessage(new android.nfc.NdefRecord[]{record}); + int size = message.toByteArray().length; + if (ndef.getMaxSize() < size) { + LogUtils.w(TAG, "NFC卡存储空间不足"); + return false; + } + ndef.writeNdefMessage(message); + ndef.close(); + return true; + } else { + NdefFormatable ndefFormatable = NdefFormatable.get(tag); + if (ndefFormatable != null) { + ndefFormatable.connect(); + android.nfc.NdefRecord record = new android.nfc.NdefRecord( + android.nfc.NdefRecord.TNF_WELL_KNOWN, + android.nfc.NdefRecord.RTD_TEXT, + new byte[0], + encryptData.getBytes(StandardCharsets.UTF_8) + ); + android.nfc.NdefMessage message = new android.nfc.NdefMessage(new android.nfc.NdefRecord[]{record}); + ndefFormatable.format(message); + ndefFormatable.close(); + return true; + } + } + } catch (FormatException|IOException e) { + LogUtils.e(TAG, "NFC卡写入失败", e); + } + return false; + } + + /** + * 从NFC卡读取数据 + */ + public static String readDataFromNfc(Tag tag) { + if (tag == null) { + return null; + } + try { + Ndef ndef = Ndef.get(tag); + if (ndef != null) { + ndef.connect(); + android.nfc.NdefMessage message = ndef.getCachedNdefMessage(); + ndef.close(); + if (message != null && message.getRecords().length > 0) { + return new String(message.getRecords()[0].getPayload(), StandardCharsets.UTF_8); + } + } + } catch (IOException e) { + LogUtils.e(TAG, "NFC卡读取失败", e); + } + return null; + } +} + diff --git a/library/src/main/java/cc/winboll/utils/ServerUtils.java b/library/src/main/java/cc/winboll/utils/ServerUtils.java new file mode 100644 index 0000000..8f22b56 --- /dev/null +++ b/library/src/main/java/cc/winboll/utils/ServerUtils.java @@ -0,0 +1,129 @@ +package cc.winboll.utils; + +import cc.winboll.LogUtils; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/15 10:08 + * 服务器交互工具类,负责请求封装、接口调用、响应处理 + * 适配Java8,支持GET/POST请求,兼容Android与标准JVM + * @Author 豆包&ZhanGSKen + */ +public class ServerUtils { + private static final String TAG = "ServerUtils"; + private static final int CONNECT_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + private static String serverBaseUrl; // 服务器基础地址 + + /** + * 初始化服务器基础地址 + */ + public static void initServerUrl(String url) { + serverBaseUrl = url; + LogUtils.d(TAG, "服务器地址初始化完成:" + serverBaseUrl); + } + + /** + * 发送验证码请求(POST) + */ + public static String sendVerifyCode(String email) { + String url = serverBaseUrl + "/api/sendVerifyCode"; + Map params = new HashMap<>(); + params.put("email", email); + return sendPostRequest(url, params); + } + + /** + * 校验验证码请求(POST) + */ + public static String verifyCode(String email, String code) { + String url = serverBaseUrl + "/api/verifyCode"; + Map params = new HashMap<>(); + params.put("email", email); + params.put("code", code); + return sendPostRequest(url, params); + } + + /** + * 提交应用公钥请求(POST) + */ + public static String submitAppPublicKey(String email, String publicKey) { + String url = serverBaseUrl + "/api/submitPublicKey"; + Map params = new HashMap<>(); + params.put("email", email); + params.put("appPublicKey", publicKey); + return sendPostRequest(url, params); + } + + /** + * 发送心跳PING请求(POST,加密数据) + */ + public static String sendPingRequest(String encryptPingData) { + String url = serverBaseUrl + "/api/heartbeat/ping"; + Map params = new HashMap<>(); + params.put("encryptData", encryptPingData); + return sendPostRequest(url, params); + } + + /** + * 通用POST请求封装 + */ + private static String sendPostRequest(String urlStr, Map params) { + if (urlStr == null || params == null) { + LogUtils.w(TAG, "POST请求参数为空"); + return null; + } + HttpURLConnection conn = null; + try { + URL url = new URL(urlStr); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + // 拼接参数 + StringBuilder paramStr = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + paramStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + if (paramStr.length() > 0) { + paramStr.deleteCharAt(paramStr.length() - 1); + } + + OutputStream os = conn.getOutputStream(); + os.write(paramStr.toString().getBytes("UTF-8")); + os.flush(); + os.close(); + + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + br.close(); + return response.toString(); + } else { + LogUtils.w(TAG, "POST请求失败,响应码:" + conn.getResponseCode()); + } + } catch (Exception e) { + LogUtils.e(TAG, "POST请求异常", e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } +} + diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml new file mode 100644 index 0000000..4a3cc35 --- /dev/null +++ b/library/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + library + Hello world! + + diff --git a/library/src/main/res/values/styles.xml b/library/src/main/res/values/styles.xml new file mode 100644 index 0000000..8d78246 --- /dev/null +++ b/library/src/main/res/values/styles.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/settings.gradle-demo b/settings.gradle-demo index 8a2f38b..66d359a 100644 --- a/settings.gradle-demo +++ b/settings.gradle-demo @@ -32,6 +32,7 @@ // AuthCenterAPP 项目编译设置 //include ':authcenterapp' +//include ':library' //rootProject.name = "authcenterapp" // Contacts 项目编译设置 diff --git a/winboll/build.properties b/winboll/build.properties index 3427cb7..5926fb4 100644 --- a/winboll/build.properties +++ b/winboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Tue Jan 13 16:53:24 HKT 2026 +#Tue Jan 13 18:37:44 GMT 2026 stageCount=11 libraryProject= baseVersion=15.11 publishVersion=15.11.10 -buildCount=0 +buildCount=1 baseBetaVersion=15.11.11 diff --git a/winboll/src/main/res/xml/network_security_config.xml b/winboll/src/main/res/xml/network_security_config.xml index e0ea145..84e403f 100644 --- a/winboll/src/main/res/xml/network_security_config.xml +++ b/winboll/src/main/res/xml/network_security_config.xml @@ -11,5 +11,11 @@ + + + 127.0.0.1 + localhost + +