JSONArray.fromObject不执行且不报错问题的解决

本文记录了一次在Java Web项目中使用JSON-lib进行JSON格式转换时遇到的问题及解决过程。问题表现为部分JavaBean对象无法被正确转换为JSON格式,且未出现明显的错误提示。通过逐步排查,最终发现是由于使用的commons-lang3版本与程序需求不符导致。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    今天在写javaweb工程的时候需要向前台传json格式的数据,用到了json-lib-2.4-jdk15.jar等一系列包,然而却出现如下状况:


    CityBean是一个javaBean,我们看到,控制台只打印出了list的内容,而下面的两个却没能打印,这说明程序执行到JSON.Array.fromObject()时终止,而控制台没有报任何错误,也没有异常抛出,特别奇怪。

    下面是有关JSON导入的包:


    首先包的数量是正确的,而且都是我才从官网上下载好的,都是最新的版本,所以一开始我并没有将重心放到jar包上。   

    后来看到网上说List中的javaBean包含其它实体属性,打印出来是个内存值,在转换json的过程中异常,将实体属性转换成字符串重新填充即可,于是我在CityBean中重写了toString()方法:


    


    可惜JSONArray.fromObject()仍然不执行。。。。。。

    后来写了个test用JUnit调试了一下,终于报异常了:



    显示找不到lang下面的类????我明明导入了lang包啊。

    据说tomcat是在它自己的lib目录下找jar包,如果jar包没有存放在tomcat的lib目录下,那么就会找不到。可是我放入了之后依旧报同样的错误。

    最后我终于发现,我导入的是commons-lang3-3.7.jar,而程序需要的是commons-lang的jar包,后来去官网看了以前的版本,果然2.x的包lang后面没有紧跟着数字,同理commons-collections4-4.2.jar也应替换到之前的版本。


    demo通过:


    程序正常执行:



    
package com.videogo.ui.login; import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.DatePicker; import android.widget.ImageButton; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.videogo.openapi.EZOpenSDK; import ezviz.ezopensdk.R; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class FanHui extends AppCompatActivity { private static final String TAG = "EZPlayback"; private String mAppKey; private String mDeviceSerial; private String mVerifyCode; private String mAccessToken; private int mCameraNo; private TextView mDateTextView; private int mSelectedYear, mSelectedMonth, mSelectedDay; private static final String KEY_APPKEY = "appkey"; private static final String KEY_SERIAL = "serial"; private static final String KEY_VERIFYCODE = "VerifyCode"; private static final String KEY_ACCESSTOKEN = "accessToken"; private static final String KEY_CAMERANO = "cameraNo"; // 回放录像相关 private static final String VIDEO_BY_TIME_URL = "https://open.ys7.com/api/lapp/video/by/time"; private ExecutorService mExecutorService; private ListView mListView; private PlaybackAdapter mAdapter; private List<VideoInfo> mVideoList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.ez_playback_list_page); // 创建线程池 mExecutorService = Executors.newFixedThreadPool(2); extractParametersFromIntent(); final Calendar calendar = Calendar.getInstance(); mSelectedYear = calendar.get(Calendar.YEAR); mSelectedMonth = calendar.get(Calendar.MONTH); mSelectedDay = calendar.get(Calendar.DAY_OF_MONTH); // 初始化视图 initViews(); // 设置日期显示模块 setupDatePicker(); // 默认加载当天的录像 loadVideosForSelectedDate(); } private void initViews() { // 查找ListView mListView = findViewById(R.id.listView); if (mListView == null) { Log.e(TAG, "ListView not found with ID listView"); return; } // 初始化适配器 mAdapter = new PlaybackAdapter(); mListView.setAdapter(mAdapter); // 设置点击事件 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { VideoInfo video = mVideoList.get(position); playVideo(video); } }); } private void setupDatePicker() { mDateTextView = findViewById(R.id.date_text); ImageButton datePickerButton = findViewById(R.id.date_picker_button); updateDateDisplay(); datePickerButton.setOnClickListener(v -> showDatePickerDialog()); } private void updateDateDisplay() { String formattedDate = String.format(Locale.getDefault(), "%d年%02d月%02d日", mSelectedYear, mSelectedMonth + 1, // 月份需要+1 mSelectedDay); mDateTextView.setText(formattedDate); } private void showDatePickerDialog() { final AlertDialog dlg = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog).create(); dlg.show(); Window window = dlg.getWindow(); window.setContentView(R.layout.datepicker_layout); // 设置对话框宽度 WindowManager.LayoutParams lp = window.getAttributes(); lp.width = WindowManager.LayoutParams.MATCH_PARENT; window.setAttributes(lp); // 获取并初始化 DatePicker DatePicker dpPicker = window.findViewById(R.id.dpPicker); // 隐藏需要的视图 ViewGroup rootView = (ViewGroup) dpPicker.getChildAt(0); if (rootView != null) { ViewGroup childView = (ViewGroup) rootView.getChildAt(0); if (childView != null) { childView.getChildAt(2).setVisibility(View.VISIBLE); // 确保月选择器可见 childView.getChildAt(1).setVisibility(View.VISIBLE); } } dpPicker.init(mSelectedYear, mSelectedMonth, mSelectedDay, null); // 设置按钮事件 RelativeLayout yesButton = window.findViewById(R.id.YES); RelativeLayout noButton = window.findViewById(R.id.NO); yesButton.setOnClickListener(v -> { mSelectedYear = dpPicker.getYear(); mSelectedMonth = dpPicker.getMonth(); mSelectedDay = dpPicker.getDayOfMonth(); updateDateDisplay(); dlg.dismiss(); // 加载新选择的日期的录像 loadVideosForSelectedDate(); }); noButton.setOnClickListener(v -> dlg.dismiss()); } private void extractParametersFromIntent() { Bundle extras = getIntent().getExtras(); if (extras != null) { mAppKey = extras.getString(KEY_APPKEY, ""); mDeviceSerial = extras.getString(KEY_SERIAL, ""); mVerifyCode = extras.getString(KEY_VERIFYCODE, ""); mAccessToken = extras.getString(KEY_ACCESSTOKEN, ""); mCameraNo = extras.getInt(KEY_CAMERANO, 0); Log.d(TAG, "Received parameters:"); Log.d(TAG, "AppKey: " + mAppKey); Log.d(TAG, "DeviceSerial: " + mDeviceSerial); Log.d(TAG, "VerifyCode: " + mVerifyCode); Log.d(TAG, "AccessToken: " + mAccessToken); Log.d(TAG, "CameraNo: " + mCameraNo); } else { Log.e(TAG, "No parameters received from intent"); } } private void loadVideosForSelectedDate() { // 计算开始和结束时间戳 Calendar cal = Calendar.getInstance(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 0, 0, 0); long startTime = cal.getTimeInMillis(); cal.set(mSelectedYear, mSelectedMonth, mSelectedDay, 23, 59, 59); long endTime = cal.getTimeInMillis(); // 发起网络请求获取录像 fetchVideosByTime(startTime, endTime); } private void fetchVideosByTime(long startTime, long endTime) { mExecutorService.execute(() -> { try { URL url = new URL(VIDEO_BY_TIME_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setDoOutput(true); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // 构建POST数据 StringBuilder postData = new StringBuilder(); postData.append("accessToken=").append(URLEncoder.encode(mAccessToken, "UTF-8")); postData.append("&deviceSerial=").append(URLEncoder.encode(mDeviceSerial, "UTF-8")); postData.append("&channelNo=").append(mCameraNo); postData.append("&startTime=").append(startTime); postData.append("&endTime=").append(endTime); postData.append("&recType=").append(0); // 系统自动选择 // 发送请求 OutputStream os = conn.getOutputStream(); os.write(postData.toString().getBytes("UTF-8")); os.flush(); os.close(); // 处理响应 int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // 读取响应内容 InputStream is = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); // 解析JSON响应 JSONObject json = new JSONObject(response.toString()); String code = json.optString("code", "0"); if ("200".equals(code)) { JSONArray data = json.getJSONArray("data"); List<VideoInfo> videos = parseVideoData(data); runOnUiThread(() -> { mVideoList.clear(); mVideoList.addAll(videos); mAdapter.notifyDataSetChanged(); }); } else { String msg = json.optString("msg", "未知错误"); Log.e(TAG, "获取录像失败: " + msg); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像失败: " + msg, Toast.LENGTH_SHORT).show()); } } else { Log.e(TAG, "HTTP错误: " + responseCode); runOnUiThread(() -> Toast.makeText(FanHui.this, "网络请求失败: " + responseCode, Toast.LENGTH_SHORT).show()); } conn.disconnect(); } catch (Exception e) { Log.e(TAG, "获取录像异常", e); runOnUiThread(() -> Toast.makeText(FanHui.this, "获取录像出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } }); } private List<VideoInfo> parseVideoData(JSONArray data) throws JSONException { List<VideoInfo> videos = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); for (int i = 0; i < data.length(); i++) { JSONObject videoObj = data.getJSONObject(i); VideoInfo video = new VideoInfo(); video.id = videoObj.optString("id"); video.startTime = videoObj.optLong("startTime"); video.endTime = videoObj.optLong("endTime"); video.recType = videoObj.optInt("recType"); // 格式化时间显示 Date startDate = new Date(video.startTime); Date endDate = new Date(video.endTime); video.timeRange = sdf.format(startDate) + " - " + sdf.format(endDate); videos.add(video); } return videos; } private void playVideo(VideoInfo video) { // 使用EZOpenSDK播放回放录像 try { // 创建播放器实例 EZPlayer player = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); // 配置播放器 player.setHandler(new Handler()); player.setPlayVerifyCode(mVerifyCode); // 开始回放 player.startPlayback(video.startTime, video.endTime); // 这里需要将播放器与界面中的播放窗口关联 // 通常需要获取TextureView或SurfaceView的holder // 例如:player.setSurfaceHolder(surfaceHolder); Toast.makeText(this, "开始播放录像: " + video.timeRange, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e(TAG, "播放录像失败", e); Toast.makeText(this, "播放录像失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } @Override protected void onDestroy() { super.onDestroy(); if (mExecutorService != null) { mExecutorService.shutdown(); } } // 录像信息数据结构 private static class VideoInfo { String id; long startTime; long endTime; int recType; String timeRange; } // 列表适配器 private class PlaybackAdapter extends BaseAdapter { @Override public int getCount() { return mVideoList.size(); } @Override public Object getItem(int position) { return mVideoList.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = getLayoutInflater().inflate(R.layout.video_item_layout, parent, false); holder = new ViewHolder(); holder.timeTextView = convertView.findViewById(R.id.time_text); holder.durationTextView = convertView.findViewById(R.id.duration_text); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } VideoInfo video = mVideoList.get(position); // 计算持续时间(分钟) long durationMinutes = (video.endTime - video.startTime) / (1000 * 60); holder.timeTextView.setText(video.timeRange); holder.durationTextView.setText(durationMinutes + "分钟"); return convertView; } class ViewHolder { TextView timeTextView; TextView durationTextView; } } // 简单的Handler实现 private static class Handler implements android.os.Handler.Callback { @Override public boolean handleMessage(Message msg) { // 处理播放器回调 return false; } } } 依据上述代码解决这7个报错:Cannot resolve symbol 'EZPlayer' Cannot resolve method 'setHandler(com.videogo.ui.login.FanHui.Handler)' Cannot resolve method 'setPlayVerifyCode(java.lang.String)' Cannot resolve method 'startPlayback(long, long)' Class 'Handler' must either be declared abstract or implement abstract method 'handleMessage(Message)' in 'Callback' Method does not override method from its superclass Cannot resolve symbol 'Message'
06-26
package com.isa.navi.library.map; import android.inputmethodservice.Keyboard; import android.os.Environment; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import androidx.annotation.NonNull; import com.isa.navi.library.constant.Const; import com.isa.navi.library.manager.BaseProxy; import com.isa.navi.library.map.DataCheckInfo; import com.isa.navi.library.map.UpdateEnvironment; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; /** * @author haoj * @description description * @date 2023-06-20 */ public class MapProxy extends BaseProxy<IMapProxyAIDL> implements IMapProxy { private static final String TAG = Const.ISA_NAVI + "MapProxy"; private List<IMapListener.IMapChangeListener> mIUpdateCallbackListeners = new CopyOnWriteArrayList<>(); private IMapDownloadCallbackAIDL mIMapDownloadCallBackAIDL; private volatile static MapProxy mInstance; public static MapProxy getInstance() { if (mInstance == null) { synchronized (MapProxy.class) { if (mInstance == null) { mInstance = new MapProxy(); } } } return mInstance; } @Override protected String getProxyName() { return Const.SVR_MAP_REPOSITORY; } @Override protected IMapProxyAIDL getProxyService(IBinder iBinder) { return IMapProxyAIDL.Stub.asInterface(iBinder); } @Override public void updateMap() { try { if (isAvailable()) { mService.updateMap(); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void suspendUpdate(boolean suspend) { try { Log.d(TAG, "suspendUpdateMap suspend : " + suspend + ", isAvailable : " + isAvailable()); if (isAvailable()) { mService.suspendUpdate(suspend); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void cancelUpdateMap() { try { Log.d(TAG, "cancelUpdateMap : " + ", isAvailable : " + isAvailable()); if (isAvailable()) { mService.cancelUpdate(); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void addMapChangeListener(IMapListener.IMapChangeListener listener) { Log.d(TAG, "addDataChangeListener callback:" + listener + ", isAvailable : " + isAvailable()); try { if (isAvailable()) { if ((listener != null) && !mIUpdateCallbackListeners.contains(listener)) { mIUpdateCallbackListeners.add(listener); } if (mIUpdateCallbackListeners.size() == 1) { mIMapDownloadCallBackAIDL = new IMapDownloadCallbackAIDL.Stub() { @Override public void onMapDownloadStatusChange(List<UpdateInfoBean> bean) throws RemoteException { Log.d(TAG, "onMapDownloadStatusChange:" + bean + " ,size:" + mIUpdateCallbackListeners.size()); try { Iterator<IMapListener.IMapChangeListener> iterator = mIUpdateCallbackListeners.iterator(); while (iterator.hasNext()) { IMapListener.IMapChangeListener l = iterator.next(); l.onMapChange(bean); } } catch (Exception e) { e.printStackTrace(); } } }; mService.registerMapDownloadListener(mIMapDownloadCallBackAIDL); } } } catch (RemoteException e) { e.printStackTrace(); } } @Override public void removeMapChangeListener(IMapListener.IMapChangeListener listener) { Log.d(TAG, "removeDataChangeListener callback:" + listener + ", isAvailable : " + isAvailable()); try { if (isAvailable()) { if ((listener != null) && mIUpdateCallbackListeners.contains(listener)) { mIUpdateCallbackListeners.remove(listener); } if (mIUpdateCallbackListeners.size() == 0 && mIMapDownloadCallBackAIDL != null) { mService.unregisterMapDownloadListener(mIMapDownloadCallBackAIDL); } mIMapDownloadCallBackAIDL = null; } } catch (RemoteException e) { e.printStackTrace(); } } private static String calculateMD5(String filePath) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); FileInputStream fis = new FileInputStream(filePath); byte[] buffer = new byte[8192]; int read = 0; while ((read = fis.read(buffer)) > 0) { digest.update(buffer, 0, read); } byte[] md5sum = digest.digest(); StringBuffer hexString = new StringBuffer(); for (int i = 0; i < md5sum.length; i++) { String hex = Integer.toHexString(0xff & md5sum[i]); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } return hexString.toString(); } catch (Exception e) { e.printStackTrace(); return ""; } } private Map<String, String> getDetectionInfo(String md5_file_path) throws JSONException { Map<String, String> dataMap = new HashMap<>(); boolean isUsbReady = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); if(!isUsbReady) { return dataMap; } JSONObject jsonObject = getJsonObject(md5_file_path); JSONArray jsonArray = jsonObject.getJSONArray("ISAData"); for(int i = 0; i< jsonArray.length(); i++) { JSONObject jsonObject1 = jsonArray.getJSONObject(i); String dataName = jsonObject1.getString("data_name"); String md5 = jsonObject1.getString("md5"); dataMap.put(dataName, md5); } return dataMap; } @NonNull private static JSONObject getJsonObject(String md5_file_path) throws JSONException { File file = new File(md5_file_path); StringBuilder content = new StringBuilder(); try{ BufferedReader reader = new BufferedReader(new FileReader(file)); String line; while ((line = reader.readLine()) != null) { content.append(line).append("\n"); } reader.close(); } catch (IOException e) { throw new RuntimeException(e); } JSONObject jsonObject = new JSONObject(content.toString()); return jsonObject; } public DataCheckInfo checkISAData(String md5_file_path) { DataCheckInfo info = new DataCheckInfo(); md5_file_path += "/ISADataCheckInfo.txt"; Log.d(TAG, "checkISAData md5_file_path = " + md5_file_path); //获取文件信息,用于校验数据 Map<String, String> md5_file_map = null; try { md5_file_map = getDetectionInfo(md5_file_path); } catch (JSONException e) { e.printStackTrace(); } //如果文件中的信息足32个则返回报错信息 if(md5_file_map.isEmpty() || md5_file_map.size() != 32) { info.setIs_result(false); info.setFailure(2); info.setReasom_chinese("ISADataCheckInfo.txt文件信息缺失"); info.setReasom_engine("Insufficient information in ISADataCheckInfo.txt"); Log.d(TAG, "checkISAData md5_file_map 信息足"); return info; } // public String get_Global_m_data_address() { // return mHmiProxyJNI.get_Global_m_data_address(); // } //在数据分区下,检索数据 String data_path = "/map/"; File directory = new File(data_path); Map<String, String> data_MD5_map = new HashMap<>(); if(directory.exists() && directory.isDirectory()) { File[] files = directory.listFiles(); if (files != null) { for(File file :files) { if(file.isFile()) { String file_name = file.getName(); if(file_name.indexOf("CarDBBIN") != -1 || file_name.indexOf("Country_CarDB") != -1) { String MD5 = ""; MD5 = calculateMD5(data_path+file_name); if(MD5 == "") { info.setIs_result(false); info.setFailure(4); info.setReasom_chinese("无法计算分区内数据MD5值"); info.setReasom_engine("Unable to calculate MD5 of data within the partition"); return info; } Log.d(TAG, "checkISAData file_name:" + file_name); Log.d(TAG, "checkISAData MD5:" + MD5); data_MD5_map.put(file_name, MD5); } } } } else { info.setIs_result(false); info.setFailure(3); info.setReasom_chinese("无法访问数据分区"); info.setReasom_engine("Unable to read data partition"); Log.d(TAG, "checkISAData 无法读取数据分区目录"); return info; } } else { info.setIs_result(false); info.setFailure(8); info.setReasom_chinese("数据分区存在"); info.setReasom_engine("The data partition does not exist"); Log.d(TAG, "checkISAData 数据分区目录存在"); return info; } Log.d(TAG, "data_MD5_map 组织完毕!"); //如果数据足32个,则返回报错 if(data_MD5_map.size() != 32) { info.setIs_result(false); info.setFailure(5); info.setReasom_chinese("数据分区目录下数据缺失"); info.setReasom_engine("Data is missing in the data partition directory"); Log.d(TAG, "checkISAData 数据分区目录目录下数据全"); return info; } //遍历data_MD5_map集合,跟校验信息进行判断 for(Map.Entry<String, String> entry: data_MD5_map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); String md5_file_map_value = md5_file_map.get(key); if(md5_file_map_value != null) { Log.d(TAG, "value:" + value); Log.d(TAG, "md5_file_map_value:" + md5_file_map_value); if(!value.equals(md5_file_map_value)) { Log.d(TAG, "value size:" + value.length()); Log.d(TAG, "md5_file_map_value size:" + md5_file_map_value.length()); info.setIs_result(false); info.setFailure(6); info.setReasom_chinese(key + " 该国家数据校验通过"); info.setReasom_engine(key + " verification failed"); Log.d(TAG, "checkISAData " + key + "该国家数据校验通过"); } } else { info.setIs_result(false); info.setFailure(7); info.setReasom_chinese(key + " 无法获取分区内数据MD5值"); info.setReasom_engine(key + " MD5 value calculation failed"); Log.d(TAG, "checkISAData MD5值计算失败"); return info; } } info.setIs_result(true); return info; } public String getInfoAfterUpdates() { String result = ""; // try { // if (isAvailable()) { // result = mService.getInfoAfterUpdates(); // } // } catch (RemoteException e) { // e.printStackTrace(); // } return result; } public void startUSBDetection() { try { Log.d(TAG, "getVersion : " + ", isAvailable : " + isAvailable()); if (isAvailable()) { mService.startUSBDetection(); } } catch (RemoteException e) { e.printStackTrace(); } } @Override public String getMapInfo() { String result = ""; try { if (isAvailable()) { result = mService.getMapInfo(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public String getUpdateVersion() { String result = ""; try { if (isAvailable()) { result = mService.getUpdateVersion(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public UpdateEnvironment checkUpdateEnvironment() { UpdateEnvironment result = null; try { if (isAvailable()) { result = mService.checkUpdateEnvironment(); Log.i(TAG, "result.getResult()" + result.getResult()); Log.i(TAG, "result.getReason()" + result.getReason()); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public String getUpdatePurpose() { String result = ""; try { if (isAvailable()) { result = mService.getUpdatePurpose(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public String getUpdateContent() { String result = ""; try { if (isAvailable()) { result = mService.getUpdateContent(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public String getUserManual() { String result = ""; try { if (isAvailable()) { result = mService.getUserManual(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public String getUpdatedContentDescription() { String result = ""; try { if (isAvailable()) { result = mService.getUpdatedContentDescription(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public int getEstimatedTime() { int result = 0; try { if (isAvailable()) { result = mService.getEstimatedTime(); } } catch (RemoteException e) { e.printStackTrace(); } return result; } @Override public boolean validatePassword(String inputPassword) { try { if (isAvailable()) { return mService.validatePassword(inputPassword); } } catch (Exception e) { e.printStackTrace(); } return false; } } 我要在这个类导入package com.isa.navi.jni.hmi; import android.content.Context; import android.os.Message; import android.os.StatFs; import android.util.Log; import androidx.annotation.NonNull; import com.isa.navi.jni.hmi.bean.UpdateList; import com.isa.navi.jni.hmi.update.USBListener; import com.isa.navi.library.map.UpdateEnvironment; import com.isa.navi.library.map.UpdateInfoBean; import com.isa.navi.manager.CallBackManager; import com.isa.navi.manager.CarManager; import com.isa.navi.manager.base.BaseHandlerManager; import com.isa.navi.receiver.USBReceiver; import com.isa.navi.utils.GsonUtils; import com.isa.navi.utils.LogUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; public class HmiJNIImpl extends BaseHandlerManager implements IHmiJNI { private static final String TAG = "UPDATE"; private static final int MSG_DOWNLOAD_STATUS_CHANGE = 1; private final HmiProxyJNI mHmiProxyJNI; private final Format mFormat; private static final String PASSWORD_PREFIX = "DfCs"; private static final String PASSWORD_SUFFIX = "#@"; private static final int LAST_CHARS_LENGTH = 4; private static String updateUSBpath = ""; /** * 车辆管理实例,用于管理车辆相关的功能。 */ private CarManager mCarManager; private volatile static HmiJNIImpl mInstance; private boolean is_run = false; private final Set<String> USBPathSet = Collections.synchronizedSet(new HashSet<>()); public static HmiJNIImpl getInstance() { if (mInstance == null) { synchronized (HmiJNIImpl.class) { if (mInstance == null) { mInstance = new HmiJNIImpl(); } } } return mInstance; } private HmiJNIImpl() { super(true); mHmiProxyJNI = new HmiProxyJNI(); mFormat = Format.getInstance(); mCarManager = CarManager.getInstance(); } @Override public void initialize() { super.initialize(); LogUtils.d(TAG, "initialize : "); DfCert.getInstance().getService(); CallBackManager.getInstance().registerUSBEventChangeListener(new USBListener() { @Override public void path(String mountPath) { USBPathSet.add(mountPath); LogUtils.i(TAG,"USBPathSet:" + mountPath); } }); // 初始化C++模块 mHmiProxyJNI.InitializeCpp(); InstanceEngine(); } private int[] getCurrentYearAndQuarter() { int[] result = new int[2]; LocalDate currentDate = LocalDate.now(); // 获取当前年份 int currentYear = currentDate.getYear(); // 获取当前月份 Month currentMonth = currentDate.getMonth(); // 根据月份确定当前季度 int currentQuarter; switch (currentMonth) { case JANUARY: case FEBRUARY: case MARCH: currentQuarter = 1; break; case APRIL: case MAY: case JUNE: currentQuarter = 2; break; case JULY: case AUGUST: case SEPTEMBER: currentQuarter = 3; break; case OCTOBER: case NOVEMBER: case DECEMBER: currentQuarter = 4; break; default: currentQuarter = 0; } result[0] = currentYear; result[1] = currentQuarter; return result; } private int[] parseMapInfo(String mapInfo) { int[] result = new int[2]; // 检查输入字符串的长度是否为6(4位年份 + 2位季度) if (mapInfo == null || mapInfo.length() != 6) { LogUtils.e(TAG, "Input string must be in the format YYYYMM where YYYY is the year and MM is the quarter as a two-digit number."); return result; } // 提取年份(前4位) String yearStr = mapInfo.substring(0, 4); result[0] = Integer.parseInt(yearStr); // 提取季度(后2位),并验证它在1到4的范围内 String quarterStr = mapInfo.substring(4, 6); int quarter = Integer.parseInt(quarterStr); if (quarter < 1 || quarter > 4) { LogUtils.e(TAG, "Quarter must be between 1 and 4."); return result; } result[1] = quarter; return result; } public void dataVersionCheck() { Thread dataVersionCheckThread = new Thread(new Runnable() { @Override public void run() { try { UpdateInfoBean bean = new UpdateInfoBean(); String localVersion = mHmiProxyJNI.getMapInfo(); if(localVersion.isEmpty()) { bean.setUpdateStatus(4); handleDownloadStatusChange(bean); LogUtils.i(TAG, "Detected no updates for a long time"); return; } int[] currentYearAndQuarter = getCurrentYearAndQuarter(); int[] localYearAndQuarter = parseMapInfo(localVersion); int yearDifference = currentYearAndQuarter[0] - localYearAndQuarter[0]; int QuarterDifference = currentYearAndQuarter[1] - localYearAndQuarter[1]; if(yearDifference * 4 + QuarterDifference > 3) { bean.setUpdateStatus(4); handleDownloadStatusChange(bean); LogUtils.i(TAG, "Detected no updates for a long time"); } } catch (Exception e) { e.printStackTrace(); } } }); // 设置线程名称 dataVersionCheckThread.setName("ISA_NAVI_dataVersionCheck"); // 启动线程 dataVersionCheckThread.start(); } public void setContext(Context context) { if(mContext == null) { mContext = context; } language.getInstance().setContext(mContext); } @Override protected void handleMessage(Message msg) { switch (msg.what) { case MSG_DOWNLOAD_STATUS_CHANGE: handleDownloadStatusChange((String) msg.obj); break; default: break; } } private List<String> JsonArrayToListExample(JSONArray jsonArray) { List<String> stringList = new ArrayList<>(); try { // 遍历JSONArray for (int i = 0; i < jsonArray.length(); i++) { // 由于我们假设JSONArray只包含字符串,所以我们可以直接getString String item = jsonArray.getString(i); // 将字符串添加到列表中 stringList.add(item); } } catch (JSONException e) { e.printStackTrace(); } return stringList; } private UpdateInfoBean jsonToUpdateInfoBean(String json) { UpdateInfoBean result = null; try { // 解析整个JSON字符串为一个JSONObject JSONObject rootObject = new JSONObject(json); // 解析mUpdateMessage对象 JSONObject mUpdateMessage = rootObject.getJSONObject("mUpdateMessage"); String country = mUpdateMessage.getString("country"); double downloadSize = mUpdateMessage.getDouble("downloadSize"); boolean isCurrentCountry = mUpdateMessage.getBoolean("isCurrentCountry"); double progress = mUpdateMessage.getDouble("progress"); double totalSize = mUpdateMessage.getDouble("totalSize"); int updateStatus = mUpdateMessage.getInt("updateStatus"); String version = mUpdateMessage.getString("version"); // 解析mUpdateNotification对象 JSONObject mUpdateNotification = rootObject.getJSONObject("mUpdateNotification"); JSONArray updateException = mUpdateNotification.getJSONArray("UpdateException"); JSONArray updateFailedList = mUpdateNotification.getJSONArray("UpdateFailedList"); boolean updateResult = mUpdateNotification.getBoolean("UpdateResult"); JSONArray updateSuccessfulList = mUpdateNotification.getJSONArray("UpdateSuccessfulList"); // 解析mapStatus int mapStatus = rootObject.getInt("mapStatus"); UpdateInfoBean.UpdateNotification temp1 = new UpdateInfoBean.UpdateNotification(updateResult,JsonArrayToListExample(updateException),JsonArrayToListExample(updateSuccessfulList),JsonArrayToListExample(updateFailedList)); UpdateInfoBean.UpdateMessage temp2 = new UpdateInfoBean.UpdateMessage(country, version, totalSize, (int) progress, isCurrentCountry); temp2.setUpdateStatus(updateStatus); temp2.setDownloadSize(downloadSize); result = new UpdateInfoBean(mapStatus, temp1, temp2); } catch (Exception e) { LogUtils.e(TAG, "JSON parsing failed:" + e.toString()); } return result; } private void handleDownloadStatusChange(String json) { UpdateInfoBean bean = jsonToUpdateInfoBean(json); LogUtils.i(TAG, "handleDownloadStatusChange : " + bean); if (bean != null) { // 仅当status=8时检查is_run,其他状态直接放行 if(bean.getUpdateStatus() != 8 || is_run) { List<UpdateInfoBean> list = new ArrayList<>(); list.add(bean); CallBackManager.getInstance().notifyUpdateStatus(list); } } } private void handleDownloadStatusChange(UpdateInfoBean bean) { LogUtils.i(TAG, "handleDownloadStatusChange : " + bean); if (bean != null) { // 仅当status=8时检查is_run,其他状态直接放行 if(bean.getUpdateStatus() != 8 || is_run) { List<UpdateInfoBean> list = new ArrayList<>(); list.add(bean); CallBackManager.getInstance().notifyUpdateStatus(list); } } else { LogUtils.e(TAG, "UpdateInfoBean is null!"); } } public void sendUpdateMessage(String json) { LogUtils.d(TAG, "sendUpdateMessage : " + json); Message message = Message.obtain(); message.what = MSG_DOWNLOAD_STATUS_CHANGE; message.obj = json; mHandler.sendMessage(message); } @Override public void updateMap() { LogUtils.d(TAG, "activeUpdateStart : "); // 检查更新过程是否已经在运行,防止重复创建线程 if(is_run) { LogUtils.i(TAG, "更新程序已经在进行"); return; }else { is_run = true; } // 创建一个新线程来执行主动更新 Thread updateMapThread = new Thread(new Runnable() { @Override public void run() { try { // 调用JNI方法开始主动更新 mHmiProxyJNI.updateMap(); LogUtils.d(TAG, "activeUpdateStart end: "); } catch (Exception e) { e.printStackTrace(); } } }); // 设置线程名称 updateMapThread.setName("ISA_NAVI_UpdateMapThread"); // 启动线程 updateMapThread.start(); } public void updateMapend() { is_run = false; } @Override public void suspendUpdate(boolean suspend) { mHmiProxyJNI.suspendUpdate(suspend); } @Override public void cancelUpdate() { mHmiProxyJNI.cancelUpdate(); } /** * 启动USB检测线程,用于检测插入的U盘并判断是否有可用的升级文件。 */ public void startUSBDetection() { LogUtils.i(TAG, "startUSBDetection START"); // setUSBPath("/data/KVM_EU_202404_T1.2.zip"); Thread USBDetectionThread = new Thread(new Runnable(){ @Override public void run() { UpdateInfoBean bean = new UpdateInfoBean(); boolean isHaveUSBData = false; //遍历检测到的U盘路径 if(USBPathSet.isEmpty()) { //未检测到U盘,触发提示弹窗 LogUtils.e(TAG, "No USB drive detected!"); bean.setUpdateStatus(2); handleDownloadStatusChange(bean); return; } int set_num = 0; for(String usbPath : USBPathSet ) { //判断U盘路径是否真实存在 set_num ++; if(USBReceiver.isMounted(usbPath)) { File usbFile = new File(usbPath); File[] listFiles = usbFile.listFiles(); if(listFiles == null) { continue; } File newfile = null; for(File file : listFiles) { //判断U盘内是否含有升级文件 if(!file.isDirectory() && mFormat.isUpgradeFileNamingFormat(file.getName())) { isHaveUSBData = true; if(newfile == null) { newfile = file; } else { if(Format.getInstance().isNewerVersion(file.getName().replaceAll("\\.\\w+$", ""), newfile.getName().replaceAll("\\.\\w+$", ""))) { newfile = file; } } } } //未检测到U盘指定路径下是否有数据,触发提示弹窗 if(!isHaveUSBData || newfile == null) { LogUtils.e(TAG, "The USB drive does not contain an upgrade package!"); bean.setUpdateStatus(3); handleDownloadStatusChange(bean); return; }else{ // 从文件名中提取版本号 String localDataPath = Format.getInstance().extractVersion(getCountryDbPath()); LogUtils.i(TAG, "file name:" + newfile.getName()); LogUtils.i(TAG, "localDataPath:" + localDataPath); if(localDataPath.isEmpty()) { String mapinfo = getMapInfo(); if(!mapinfo.isEmpty()) { String[] newVersionParts = new String[2]; Format.getInstance().extractVersion(newfile.getName().replaceAll("\\.\\w+$", ""), newVersionParts); String newUpdateVersion = newVersionParts[0]; String newReleasesVersion = newVersionParts[1]; LogUtils.i(TAG, "localDataPath.isEmpty() newUpdateVersion: " + newUpdateVersion); LogUtils.i(TAG, "localDataPath.isEmpty() newReleasesVersion: " + newReleasesVersion); if(Integer.parseInt(newUpdateVersion) > Integer.parseInt(mapinfo) || (Integer.parseInt(newUpdateVersion) == Integer.parseInt(mapinfo) && Double.parseDouble(newReleasesVersion) > 1)) { LogUtils.i(TAG,"Trigger USB drive update!"); bean.setUpdateStatus(0); handleDownloadStatusChange(bean); //设置U盘更新路径 updateUSBpath = usbPath; setUSBPath(newfile.getAbsolutePath()); LogUtils.i(TAG, "filePath:" + newfile.getAbsolutePath()); } else { LogUtils.e(TAG, "The upgrade package version on the USB drive is less than or equal to the vehicle's version!"); bean.setUpdateStatus(1); handleDownloadStatusChange(bean); } } else { LogUtils.i(TAG,"Trigger USB drive update!"); bean.setUpdateStatus(0); handleDownloadStatusChange(bean); //设置U盘更新路径 updateUSBpath = usbPath; setUSBPath(newfile.getAbsolutePath()); LogUtils.i(TAG, "filePath:" + newfile.getAbsolutePath()); } return; } // 判断U盘内数据版本是否大于车机数据版本 if(Format.getInstance().isNewerVersion(newfile.getName().replaceAll("\\.\\w+$", ""), localDataPath)) { LogUtils.i(TAG,"Trigger USB drive update!"); bean.setUpdateStatus(0); handleDownloadStatusChange(bean); //设置U盘更新路径 updateUSBpath = usbPath; setUSBPath(newfile.getAbsolutePath()); LogUtils.i(TAG, "filePath:" + newfile.getAbsolutePath()); } else { LogUtils.e(TAG, "The upgrade package version on the USB drive is less than or equal to the vehicle's version!"); bean.setUpdateStatus(1); handleDownloadStatusChange(bean); } return; } } else { if(set_num == USBPathSet.size()) { //未检测到U盘,触发提示弹窗 LogUtils.e(TAG, "No USB drive detected!"); bean.setUpdateStatus(2); handleDownloadStatusChange(bean); return; } } } } }); USBDetectionThread.setName("ISA_NAVI_USBDetectionThread"); USBDetectionThread.start(); } public String getMapInfo() { String result = mHmiProxyJNI.getMapInfo(); LogUtils.i(TAG,"MapInfo:" + result); return result; } public String getUpdateVersion() { String result = mHmiProxyJNI.getUpdateVersion(); LogUtils.i(TAG,"UpdateVersion:" + result); return result; } public UpdateEnvironment checkUpdateEnvironment() { String json = mHmiProxyJNI.checkUpdateEnvironment(); LogUtils.d(TAG,"checkUpdateEnvironment JSON:" + json); UpdateEnvironment result = GsonUtils.fromJson(json, UpdateEnvironment.class); if(result == null) { LogUtils.e(TAG, "GsonUtils.fromJson(json, UpdateEnvironment.class) Fail!"); return null; } if (!result.getResult()) { List<String> temp_list = new ArrayList<>(); for (int i : result.getInt_reason()) { String str = language.getInstance().getEnvironment(i); if(str != null && !str.isEmpty()) { temp_list.add(str); } } // 更新temp对象中的reason字段 result.setReason(temp_list); } LogUtils.i(TAG,"checkUpdateEnvironment :" + result); return result; } public String getUpdatePurpose() { return language.getInstance().getUpdatePurpose(); } public String getUpdateContent() { return language.getInstance().getUpdateContent(); } public String getUserManual() { return language.getInstance().getUserManual(); } public String getUpdatedContentDescription() { return language.getInstance().getUpdatedContentDescription(); } public int getEstimatedTime() { return 30; } private void InstanceEngine() { Thread InstanceEngine = new Thread(new Runnable(){ @Override public void run() { LogUtils.d("ISA_NAVI", "InstanceEngine start!"); mHmiProxyJNI.instanceEngine(); } }); InstanceEngine.setName("ISA_NAVI_InstanceEngine"); InstanceEngine.start(); } // 获取后四位字符 @NonNull private static String getLastFourChars(String mapInfo) { if (mapInfo == null || mapInfo.isEmpty()) { LogUtils.e(TAG, "mapInfo cannot be null or empty"); return "0000"; } return mapInfo.length() >= LAST_CHARS_LENGTH ? mapInfo.substring(mapInfo.length() - LAST_CHARS_LENGTH) : "0000"; } // 生成密码 @NonNull private String generatePassword() { String mapInfo = getMapInfo(); String lastFourChars = getLastFourChars(mapInfo); return PASSWORD_PREFIX + lastFourChars + PASSWORD_SUFFIX; } // 校验密码 public boolean validatePassword(String inputPassword) { if (inputPassword == null) { return false; } String generatedPassword = generatePassword(); LogUtils.i(TAG, "generatedPassword:" + generatedPassword); LogUtils.i(TAG, "inputPassword:" + inputPassword); return inputPassword.equals(generatedPassword); } public void setUSBPath(String USBPath) { mHmiProxyJNI.setUSBPath(USBPath); } public boolean unzipFile(String zipFilePath, String destinationDirPath) { return ZipUtils.getInstance().unzip(zipFilePath, destinationDirPath); } public long getFileSize(String filePath) { File file = new File(filePath); if(!file.isFile() || !file.exists()) { return 0; } LogUtils.d(TAG, "getFileSize:" + file.length()); return file.length(); } public long getAvailableSpace(String filePath) { StatFs stat = new StatFs(filePath); long availableBlocks = stat.getAvailableBlocksLong(); long blockSize = stat.getBlockSizeLong(); LogUtils.i(TAG, "[AvailableSpace]:" + availableBlocks * blockSize + "字节"); return availableBlocks * blockSize; } public String getFail(int i) { return language.getInstance().getFail(i); } public boolean decryptFile(String encryptedFilePath, String decryptedFilePath) { return new DecryptUtil().decryptFile(encryptedFilePath, decryptedFilePath, DfCert.getInstance().getKEY_AES_256()); } public boolean verifySignature(String filePath, String signFilePath) { try { return DecryptUtil.verifySignature(filePath, signFilePath, DfCert.getInstance().getPUBLIC_KEY()); } catch (Exception e) { return false; } } /** * 获取高压电池电量百分比。 * * <p>此方法通过调用`mCarManager.getHVPercent`来获取高压电池的电量百分比。如果调用成功,则返回电量百分比值; * 如果调用失败或发生异常,则捕获异常并打印堆栈跟踪,最后返回`0`。</p> * * @return 一个浮点数,表示高压电池的电量百分比(0-100);如果获取失败或发生异常,则返回`0`。 */ public float getHVPercent() { float HVPercent = 0; try { HVPercent = mCarManager.getHVPercent(); if(HVPercent > 100 || HVPercent < 0) { HVPercent = 0; } } catch (Exception e) { e.printStackTrace(); } return HVPercent; } boolean isMounted() { return USBReceiver.isMounted(updateUSBpath); } public String getCountryDbPath() { return mHmiProxyJNI.getCountryDbPath(); } public String get_Global_m_data_address() { return mHmiProxyJNI.get_Global_m_data_address(); } public boolean dataVerification(String verifyDataPath) { LogUtils.i(TAG, "dataVerification[verifyDataPath]:" + verifyDataPath); String signPath = get_Global_m_data_address() + "sign/"; if(!mapControl()) { return false; } //解压加密数据 LogUtils.i(TAG, "verifyDataPath[" + verifyDataPath + "] -> signPath[" + signPath + "]"); if(!unzipFile(verifyDataPath, signPath)) { //删除解密验签中间文件 if(!deleteDirectory(signPath)) { LogUtils.e(TAG, "无法删除解密验签中间文件!"); } return false; } if(!mapControl()) { return false; } //提取签名文件以及加密文件的绝对路径 String[] temp = ZipUtils.getInstance().findEncryptedZipFiles(signPath); String aesDaraPath = temp[1]; String signDataPtah = temp[0]; LogUtils.i(TAG, "aesDaraPath:" + aesDaraPath); LogUtils.i(TAG, "signDataPtah" + signDataPtah); if(!mapControl()) { return false; } //验证签名 if(!verifySignature(aesDaraPath, signDataPtah)) { LogUtils.e(TAG, "verifySignature fail!"); //删除解密验签中间文件 if(!deleteDirectory(signPath)) { LogUtils.e(TAG, "无法删除解密验签中间文件!"); } return false; } if(!mapControl()) { return false; } //对称解密Zip if(!decryptFile(aesDaraPath, verifyDataPath)) { LogUtils.e(TAG, "decryptFile fail!"); //删除解密验签中间文件 if(!deleteDirectory(signPath)) { LogUtils.e(TAG, "无法删除解密验签中间文件!"); } return false; } //删除解密验签中间文件 if(!deleteDirectory(signPath)) { LogUtils.e(TAG, "无法删除解密验签中间文件!"); } LogUtils.i(TAG, "dataVerification success"); return true; } /** * 删除指定目录及其所有内容 * * @param directory 指定的目录路径 * @return 如果删除成功返回 true,否则返回 false */ public boolean deleteDirectory(String directory) { File dir = new File(directory); if (!dir.exists()) { LogUtils.e("目录存在: " + directory); return false; // 目录存在,视为删除失败 } if (!dir.isDirectory()) { LogUtils.e("指定路径是一个目录: " + directory); return false; // 路径是目录,无法删除 } return deleteRecursive(dir); // 递归删除目录 } /** * 递归删除文件或目录 * * @param file 文件或目录 * @return 如果删除成功返回 true,否则返回 false */ private boolean deleteRecursive(File file) { if (file.isDirectory()) { // 如果是目录,递归删除其所有子文件和子目录 File[] files = file.listFiles(); if (files != null) { for (File subFile : files) { if (!deleteRecursive(subFile)) { return false; // 如果某个子文件或子目录删除失败,则整体失败 } } } } // 删除当前文件或空目录 if (!file.delete()) { LogUtils.e("无法删除文件或目录: " + file.getAbsolutePath()); return false; // 删除失败 } LogUtils.i("已成功删除: " + file.getAbsolutePath()); return true; // 删除成功 } public boolean mapControl() { LogUtils.d(TAG, "mapControl JAVA start"); return mHmiProxyJNI.mapControl(); } public boolean zipDbFiles(String sourceDir, String outputZipPath) { return ZipUtils.getInstance().zipDbFiles(sourceDir, outputZipPath); } public boolean zip(String sourcePath, String outputZipPath) { return ZipUtils.getInstance().zip(sourcePath, outputZipPath, true); } // Used to load the 'native-lib' library on application startup. static { LogUtils.d(TAG, "loadLibrary IsaEngineJni " + System.currentTimeMillis()); System.loadLibrary("IsaEngineJni"); } }
最新发布
08-14
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隐秀_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值