ByteBuffer.flip()注意细节

本文详细介绍了ByteBuffer中的flip()方法的工作原理及其应用场景。通过具体的代码示例,解释了如何使用此方法来实现数据从一个地方到另一个地方的有效传输,并讨论了多次调用此方法可能产生的效果。

ByteBuffer.flip()

反转此缓冲区。首先将限制设置为当前位置,然后将位置设置为 0。如果已定义了标记,则丢弃该标记。 在一系列通道读取或放置 操作之后,调用此方法为一系列通道写入或相对获取 操作做好准备。

例如:

buf.put(magic);// Prepend header

in.read(buf); // Read data into rest of buffer

buf.flip(); // Flip buffer

out.write(buf);

// Write header + data to channel当将数据从一个地方传输到另一个地方时,经常将此方法与 compact 方法一起使用

同时调用两次的结果是:

假设第一次 position=0 limit=13

第二次 position=0 limit=0

这个问题搞了我一上午,郁闷了。

package com.depower; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; /** * 测试多数据源 * */ @RunWith(SpringRunner.class) @SpringBootTest @Slf4j public class DataRTest { @Test public void dataReciveTest() { // 示例数据2 String hexData = "0,100,31,2,226,16,6,0,6,0,0,0,0,0,0,0,0,0,0,0,0,2,0,6,0,0,0,5,0,0,13,31,13,33,13,35,13,33,13,34,13,37,13,36,13,29,13,26,13,32,13,25,13,31,13,33,13,34,13,30,13,29,13,31,13,30,13,25,13,27,13,31,13,27,0,0,12,233,0,0,7,96,27,28,28,27,6,38,221,10,38,213,38,212,0,129,3,132,79,12,116,114,97,107,1,97,0,49,164,57,111,153,1,29,0,0,0,0,0,0,0,0,0,0,0,0,56,57,56,54,48,52,68,50,49,57,50,53,68,48,49,52,54,49,54,57,56,54,52,50,51,57,48,54,52,55,51,52,57,49,53,212,235,204,104,0,0,0,0,208,208,234,8,96,4,0,0,0,0,0,0,113,40,0,0,129,1,0,0,20,5,0,0,56,0,0,0,231,127"; Map<String,Object> contents=new HashMap<String,Object>(); BmsTrackerParser(hexData,contents); } public void BmsTrackerParser(String hexData,Map<String,Object> contents) { log.info("原始数据:{}",hexData); String[] parts = hexData.substring(0, hexData.length()).split(","); byte[] bytes = new byte[parts.length]; for (int i = 0; i < parts.length; i++) { bytes[i] = (byte) Integer.parseInt(parts[i].trim()); } ByteBuffer buffer = ByteBuffer.wrap(bytes); // 解析 BMS 大端数据 dataParse(buffer,contents); // 设置小端模式解析 Tracker 数据 trackerParse(buffer,contents); } private static void trackerParse(ByteBuffer buffer, Map<String, Object> mapinfo) { log.info("=== Tracker 小端数据解析 ==="); byte[] header = new byte[4]; buffer.get(header); log.info("包头: " + new String(header)); mapinfo.put("header", new String(header)); byte cmd = buffer.get(); log.info("命令: " + String.format("0x%02X", cmd)); mapinfo.put("cmd", String.format("0x%02X", cmd)); short length = buffer.getShort(); log.info("数据长度: " + length); mapinfo.put("length", length); byte[] ts = new byte[6]; buffer.get(ts); long timestamp = 0; for (byte b : ts) { timestamp = (timestamp << 8) | (b & 0xFF); } Date date = new Date(); date.setTime(timestamp); String uploadtime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(date); log.info("上报时间戳: " + timestamp); log.info("上报时间: " + uploadtime); mapinfo.put("upload_time", uploadtime); if (String.format("0x%02X", cmd).equals("0x02")) { short crc = buffer.getShort(); log.info("CRC校验: " + String.format("0x%04X", crc)); } if (String.format("0x%02X", cmd).equals("0x01")) { byte csq = buffer.get(); log.info("4G信号强度: " + csq); mapinfo.put("gps_signal", csq); byte gpsMaxSignal = buffer.get(); log.info("GPS最大信号强度: " + gpsMaxSignal); mapinfo.put("gps_max_signal", gpsMaxSignal); int longitude = buffer.getInt(); log.info("GPS经度: " + longitude / 1e6); mapinfo.put("longitude", longitude / 1e6); int latitude = buffer.getInt(); log.info("GPS纬度: " + latitude / 1e6); mapinfo.put("latitude", latitude / 1e6); byte speed = buffer.get(); log.info("GPS速度: " + speed); mapinfo.put("gps_speed", speed); short angle = buffer.getShort(); log.info("GPS对地角度: " + angle); mapinfo.put("gps_cog", angle); byte[] iccid = new byte[20]; buffer.get(iccid); log.info("ICCID: " + new String(iccid).trim()); mapinfo.put("iccid", new String(iccid).trim()); byte[] imei = new byte[15]; buffer.get(imei); log.info("IMEI: " + new String(imei).trim()); mapinfo.put("imei", new String(imei).trim()); int timestamp10 = buffer.getInt(); log.info("10位时间戳版本: " + timestamp10); Date date2 = new Date(); date2.setTime(timestamp10); String timestamp10Str = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date2); mapinfo.put("tracker_software_version", timestamp10Str); log.info("10位时间版本: " + timestamp10Str); // 小区信息 JSONArray cellList = new JSONArray(); // 创建JSON数组 for (int i = 0; i < 10; i++) { int flag = buffer.getInt(); int cid = buffer.getInt(); int mcc = buffer.getInt(); int mnc = buffer.getInt(); int tac = buffer.getInt(); int pci = buffer.getInt(); int earfcn = buffer.getInt(); int rssi = buffer.getInt(); if (flag == 0 && cid == 0 && mcc == 0 && mnc == 0 && tac == 0 && pci == 0 && earfcn == 0 && rssi == 0) { break; } // 创建单个小区的JSON对象 JSONObject cell = new JSONObject(); cell.put("flag", flag); cell.put("cid", cid); cell.put("mcc", mcc); cell.put("mnc", mnc); cell.put("tac", tac); cell.put("pci", pci); cell.put("earfcn", earfcn); cell.put("rssi", rssi); cellList.add(cell); // 添加到JSON数组 log.info("小区信息[{}] - flag: {}, cid: {}, mcc: {}, mnc: {}, tac: {}, pci: {}, earfcn: {}, rssi: {}\n", i, flag, cid, mcc, mnc, tac, pci, earfcn, rssi); } mapinfo.put("cellList", cellList.toString()); short crc = buffer.getShort(); log.info("CRC校验: " + String.format("0x%04X", crc)); } } private static void dataParse(ByteBuffer buffer, Map<String, Object> contents) { log.info("=== Data 大端数据解析 ==="); // 解析动态数据 byte dataType = buffer.get(); // 数据类型 log.info("数据类型: {}", Integer.toHexString(dataType & 0xFF).equals("0") ? "动态数据" : "静态数据"); contents.put("data_type", Integer.toHexString(dataType & 0xFF)); if (Integer.toHexString(dataType & 0xFF).equals("0")) { // 2. SOC (U8) Integer soc = Byte.toUnsignedInt(buffer.get()); log.info("SOC: {}%", soc); contents.put("battery_soc", soc); // 低电量告警 Integer lowBatteryWarn = soc < 30 ? 1 : 0; contents.put("low_battery_warn", lowBatteryWarn); // 3. MOS温度 (S8) Integer mosTemp = Integer.valueOf(buffer.get()); log.info("MOS温度: {}℃", mosTemp); contents.put("mos_temp", mosTemp); // 4. 整包电压 (U16) Integer packVoltage = Short.toUnsignedInt(buffer.getShort()); log.info("电池整包电压: {}mV", packVoltage * 100); contents.put("battery_voltage", packVoltage * 100); // 5. 电池包状态 (U8) Integer packStatus = Byte.toUnsignedInt(buffer.get()); log.info("电池包状态: {} - {}", packStatus, resolvePackStatus(packStatus)); contents.put("battery_work_mode", packStatus); // 6. MOS管状态 (U8) Integer mosStatus = Byte.toUnsignedInt(buffer.get()); log.info("MOS管状态: {}", parseMosStatus(mosStatus)); contents.put("mos_status", mosStatus); // 7. 当前电流 (S16) int current = buffer.getShort(); log.info("当前电流: {}mA", Integer.valueOf(current * 100)); contents.put("battery_current", Integer.valueOf(current * 100)); // 8. 故障码 (U32*3) long[] faultCodes = new long[3]; for (int i = 0; i < 3; i++) { faultCodes[i] = Integer.toUnsignedLong(buffer.getInt()); } log.info("故障码原始值: {}", Arrays.toString(faultCodes)); // 转换为24字符的十六进制字符串 StringBuilder hexBuilder = new StringBuilder(24); // 24字符 = 3 * // 8个十六进制字符 for (long code : faultCodes) { // 格式化为8位十六进制,不足8位自动补前导0 hexBuilder.append(String.format("%08x", code)); } String errorCodeHex = hexBuilder.toString(); contents.put("battery_error_code", errorCodeHex); log.info("故障码十六进制: {}", errorCodeHex); // 9. 充电模式 (U8) Integer chargeMode = Byte.toUnsignedInt(buffer.get()); log.info("充电模式: {} - {}", chargeMode, resolveChargeMode(chargeMode)); contents.put("charge_mode", chargeMode); // 10. 最大充电电流 (U16) Integer maxChargeCurrent = Short.toUnsignedInt(buffer.getShort()); log.info("最大充电电流: {}mA", maxChargeCurrent * 100); contents.put("charge_current_max", maxChargeCurrent * 100); // 11. 最大放电电流 (U16) Integer maxDischargeCurrent = Short.toUnsignedInt(buffer.getShort()); log.info("最大放电电流: {}mmA", Integer.valueOf(maxDischargeCurrent * 100)); contents.put("discharge_current_max", Integer.valueOf(maxDischargeCurrent * 100)); // 12. 平均充电电流 (U16) Integer avgChargeCurrent = Short.toUnsignedInt(buffer.getShort()); log.info("平均充电电流: {}mA", Integer.valueOf(avgChargeCurrent * 100)); contents.put("charge_current_avg", Integer.valueOf(avgChargeCurrent * 100)); // 13. 平均放电电流 (U16) int avgDischargeCurrent = Short.toUnsignedInt(buffer.getShort()); log.info("平均放电电流: {}mA", Integer.valueOf(avgDischargeCurrent * 100)); contents.put("discharge_current_avg", Integer.valueOf(avgDischargeCurrent * 100)); // 14. 单体电压 (22节电芯) List<Integer> cellVoltages = new ArrayList<>(); for (int i = 0; i < 22; i++) { cellVoltages.add(Short.toUnsignedInt(buffer.getShort())); } String result = cellVoltages.stream().map(String::valueOf) // 将整数转为字符串 .collect(Collectors.joining("#")); // 用 # 拼接 log.info("单体电压: {} mV", result); contents.put("cell_voltage", result); // 单体电压最高 contents.put("battery_voltage_max", Collections.max(cellVoltages)); log.info("单体电压最高: {} mV", Collections.max(cellVoltages)); // 单体电压最低 contents.put("battery_voltage_min", Collections.min(cellVoltages)); log.info("单体电压最低: {} mV", Collections.min(cellVoltages)); // 15. 历史充电容量 (U32) Long chargeCapacity = Integer.toUnsignedLong(buffer.getInt()); log.info("历史充电容量: {}mah", chargeCapacity); contents.put("charge_capacity_history", chargeCapacity); // 16. 历史放电容量 (U32) Long dischargeCapacity = Integer.toUnsignedLong(buffer.getInt()); log.info("历史放电容量: {}mah", dischargeCapacity); contents.put("discharge_capacity_history", dischargeCapacity); // 17-20. NTC温度 (S8) List<Integer> temps = new ArrayList<>(); for (int i = 1; i <= 4; i++) { int ntcTemp = buffer.get(); log.info("NTC{}温度: {}℃", i, ntcTemp); contents.put("ntc" + i, ntcTemp); temps.add(ntcTemp); } // 计算温度指标 Integer maxTemp = Collections.max(temps); Integer minTemp = Collections.min(temps); Double avgTemp = temps.stream().mapToInt(Integer::intValue).average().orElse(0.0); // 输出结果(实际使用中可能需要存储或返回这些值) log.info("最高温度: {}℃", maxTemp); log.info("最低温度: {}℃", minTemp); log.info("平均温度: {}℃", avgTemp); // 保留两位小数 // 最高温度 contents.put("battery_temperature_max", maxTemp); // 最低温度 contents.put("battery_temperature_min", minTemp); // 平均温度 contents.put("battery_temperature_avg", avgTemp); // 21. tracker通讯状态 (U8) int trackerStatus = Byte.toUnsignedInt(buffer.get()); log.info("Tracker通讯状态: {}", parseTrackerStatus(trackerStatus)); contents.put("tracker_communication_status", parseTrackerStatus(trackerStatus)); // 22-28. 扩展字段 Integer dsoc = Short.toUnsignedInt(buffer.getShort()); log.info("DSOC: {}", dsoc); contents.put("dsoc", dsoc); Integer correctionRate = Byte.toUnsignedInt(buffer.get()); log.info("修正速率: {}", correctionRate); contents.put("correction_rate", correctionRate); Integer tsoc = Integer.valueOf(buffer.getShort()); log.info("TSOC: {}", tsoc); contents.put("tsoc", tsoc); Integer bsoc = Short.toUnsignedInt(buffer.getShort()); log.info("BSOC: {}", bsoc); contents.put("bsoc", bsoc); int chargeSop = Short.toUnsignedInt(buffer.getShort()); log.info("充电SOP: {}mA", Integer.valueOf(chargeSop * 100)); contents.put("charge_sop", Integer.valueOf(chargeSop * 100)); int dischargeSop = Short.toUnsignedInt(buffer.getShort()); log.info("放电SOP: {}mA", Integer.valueOf(dischargeSop * 100)); contents.put("discharge_sop", Integer.valueOf(dischargeSop * 100)); // 29. 校验码 CRC16 (U16) Integer checksum = Short.toUnsignedInt(buffer.getShort()); log.info("校验码: {}", checksum); contents.put("check_code", checksum); } else if (Integer.toHexString(dataType & 0xFF).equals("1")) { // 2. 未满充计数 (U16) Integer unchargedCount = Short.toUnsignedInt(buffer.getShort()); log.info("未满充计数: {}", unchargedCount); contents.put("empty_count", unchargedCount); // 3. 延时10S计数 (U16) int delay10sCount = Short.toUnsignedInt(buffer.getShort()); log.info("延时10S计数: {} s", Integer.valueOf(delay10sCount * 10)); contents.put("task_max_delay_10ms", Integer.valueOf(delay10sCount * 10)); // 4. BMS软件版本号 (u32) int version = buffer.getInt(); String softwareVersion = String.format("V%d.%d&&TV%d.%d", (version >> 24) & 0xFF, (version >> 16) & 0xFF, (version >> 8) & 0xFF, version & 0xFF); log.info("BMS软件版本号: {}", softwareVersion); contents.put("battery_soft_version", softwareVersion); // 5. BMS硬件版本号 (U16) int hwVersion = Short.toUnsignedInt(buffer.getShort()); String hardwareVersion = String.format("V%d.%d", (hwVersion >> 8) & 0xFF, hwVersion & 0xFF); log.info("BMS硬件版本号: {}", hardwareVersion); contents.put("battery_hard_version", hardwareVersion); // 6. SN码 (ASCII码) byte[] snBytes = new byte[32]; buffer.get(snBytes); String serialNumber = new String(snBytes).trim(); log.info("SN码: {}", serialNumber); contents.put("sn", serialNumber); // 7. 历史充电次数 (U32) Long chargeCycleCount = buffer.getInt() & 0xFFFFFFFFL; log.info("历史充电次数: {} 次", chargeCycleCount); contents.put("charge_cycles_history", chargeCycleCount); // 8. 循环次数 (U16) Integer cycleCount = Short.toUnsignedInt(buffer.getShort()); log.info("循环次数: {} 次", cycleCount); contents.put("battery_cycle_times", cycleCount); // 9. SOH (U8) Integer soh = Byte.toUnsignedInt(buffer.get()); log.info("SOH: {}", soh); contents.put("soh", soh); // 10. 真实SOH (U16) Integer realSoh = Short.toUnsignedInt(buffer.getShort()); log.info("真实SOH: {}", realSoh); contents.put("actual_soh", realSoh); // 11. 历史充电总能量 (U32) Long totalChargeEnergy = buffer.getInt() & 0xFFFFFFFFL; log.info("历史充电总能量: {} kwh", totalChargeEnergy); contents.put("total_charge_energy", totalChargeEnergy); // 12. FCC (U32) Long fcc = buffer.getInt() & 0xFFFFFFFFL; log.info("FCC: {} mah", fcc); contents.put("fcc", fcc); // 13. 校验码 (U16) Integer checksum = Short.toUnsignedInt(buffer.getShort()); log.info("校验码: {}", checksum); contents.put("check_code", checksum); } } private static String resolvePackStatus(int code) { switch (code) { case 0x01: return "放电模式"; case 0x10: return "充电模式"; case 0x11: return "充电准备"; case 0x21: return "保护模式"; case 0x30: return "待机无输出"; case 0x31: return "待机预放电"; case 0xFF: return "故障需返厂"; default: return "未知状态"; } } private static String parseMosStatus(int status) { return String.format("放电MOS:%s 充电MOS:%s 预放电MOS:%s 均衡管:%s", (status & 0x02) != 0 ? "开" : "关", (status & 0x04) != 0 ? "开" : "关", (status & 0x08) != 0 ? "开" : "关", (status & 0x10) != 0 ? "开" : "关"); } private static String resolveChargeMode(int mode) { switch (mode) { case 1: return "标准充"; case 2: return "快充"; case 3: return "盲充"; default: return "未知模式"; } } private static String parseTrackerStatus(int status) { if ((status & 0x01) != 0) { return "未收到"; } else if ((status & 0x02) != 0) { return "未回复"; } else if ((status & 0x04) != 0) { return "GPS异常"; } else { return "正常"; } } // 大端模式CRC校验(无长度字段) public static boolean checkBigEndianCRC(ByteBuffer buffer) { if (buffer.remaining() < 2) { log.warn("大端CRC校验失败:缓冲区不足"); return false; } // 保存原始位置 int originalPosition = buffer.position(); try { // 获取存储的CRC值(大端) int expectedCRC = Short.toUnsignedInt(buffer.getShort(buffer.limit() - 2)); log.info("大端CRC期望值: 0x" + Integer.toHexString(expectedCRC)); // 计算缓冲区实际数据的CRC(不包括最后2字节的CRC) byte[] data = new byte[buffer.limit() - 2]; buffer.position(0); buffer.get(data); short calculatedCRC = calculateCRC16(data, 0, data.length); // 比较校验结果 if (Short.toUnsignedInt(calculatedCRC) == expectedCRC) { log.info("大端CRC校验通过"); return true; } log.warn("大端CRC校验失败:计算值0x" + Integer.toHexString(Short.toUnsignedInt(calculatedCRC)) + " != 期望值0x" + Integer.toHexString(expectedCRC)); return false; } finally { buffer.position(originalPosition); } } // 小端模式CRC校验(有长度字段) public static boolean checkLittleEndianCRC(ByteBuffer buffer) { if (buffer.remaining() < 4) { // 至少需要长度字段(2) + CRC(2) log.warn("小端CRC校验失败:缓冲区不足"); return false; } // 保存原始位置和字节序 int originalPosition = buffer.position(); ByteOrder originalOrder = buffer.order(); try { // 取长度字段(假设为大端) buffer.position(0); int dataLength = Short.toUnsignedInt(buffer.getShort()); // 检查数据完整性 if (buffer.limit() < 2 + dataLength + 2) { log.warn("小端CRC校验失败:数据不完整,需要长度: " + (2 + dataLength + 2) + ", 实际长度: " + buffer.limit()); return false; } // 获取存储的CRC值(小端) buffer.position(2 + dataLength); buffer.order(ByteOrder.LITTLE_ENDIAN); int expectedCRC = Short.toUnsignedInt(buffer.getShort()); log.info("小端CRC期望值: 0x" + Integer.toHexString(expectedCRC)); // 计算实际数据的CRC(长度字段 + 数据) byte[] data = new byte[2 + dataLength]; buffer.position(0); buffer.get(data); short calculatedCRC = calculateCRC16(data, 0, data.length); // 比较校验结果 if (Short.toUnsignedInt(calculatedCRC) == expectedCRC) { log.info("小端CRC校验通过"); return true; } log.warn("小端CRC校验失败:计算值0x" + Integer.toHexString(Short.toUnsignedInt(calculatedCRC)) + " != 期望值0x" + Integer.toHexString(expectedCRC)); return false; } finally { buffer.position(originalPosition); buffer.order(originalOrder); } } // CRC-16/MODBUS计算实现 public static short calculateCRC16(byte[] data, int offset, int length) { int crc = 0xFFFF; int polynomial = 0xA001; // MODBUS多项式 for (int i = offset; i < offset + length; i++) { crc ^= (data[i] & 0xFF); for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) { crc = (crc >> 1) ^ polynomial; } else { crc = crc >> 1; } } } return (short) crc; } // 测试方法 public static void main(String[] args) { // 测试大端模式 System.out.println("===== 测试大端模式 ====="); String bigEndianData = "大端模式测试数据"; ByteBuffer bigEndianBuffer = ByteBuffer.allocate(bigEndianData.getBytes().length + 2); bigEndianBuffer.put(bigEndianData.getBytes()); short bigEndianCRC = calculateCRC16(bigEndianData.getBytes(), 0, bigEndianData.getBytes().length); bigEndianBuffer.putShort(bigEndianCRC); bigEndianBuffer.flip(); boolean bigEndianResult = checkBigEndianCRC(bigEndianBuffer); System.out.println("大端校验结果: " + bigEndianResult); // 测试小端模式 System.out.println("\n===== 测试小端模式 ====="); String littleEndianPayload = "小端模式测试数据"; byte[] payloadBytes = littleEndianPayload.getBytes(); ByteBuffer littleEndianBuffer = ByteBuffer.allocate(2 + payloadBytes.length + 2); // 写入长度字段(大端) littleEndianBuffer.putShort((short) payloadBytes.length); // 写入实际数据 littleEndianBuffer.put(payloadBytes); // 计算CRC(长度字段+数据) byte[] dataForCRC = new byte[2 + payloadBytes.length]; littleEndianBuffer.position(0); littleEndianBuffer.get(dataForCRC); short littleEndianCRC = calculateCRC16(dataForCRC, 0, dataForCRC.length); // 以小端模式写入CRC littleEndianBuffer.order(ByteOrder.LITTLE_ENDIAN); littleEndianBuffer.putShort(littleEndianCRC); littleEndianBuffer.flip(); boolean littleEndianResult = checkLittleEndianCRC(littleEndianBuffer); System.out.println("小端校验结果: " + littleEndianResult); // 测试无效数据 System.out.println("\n===== 测试无效数据 ====="); ByteBuffer invalidBuffer = ByteBuffer.allocate(10); invalidBuffer.put("无效数据".getBytes()); invalidBuffer.putShort((short) 0x1234); // 随机CRC invalidBuffer.flip(); boolean invalidBigEndian = checkBigEndianCRC(invalidBuffer); System.out.println("无效数据大端校验结果: " + invalidBigEndian); // 测试小端模式无效数据 ByteBuffer invalidLittleBuffer = ByteBuffer.allocate(10); invalidLittleBuffer.putShort((short) 5); // 长度字段 invalidLittleBuffer.put("123".getBytes()); // 数据不足 invalidLittleBuffer.order(ByteOrder.LITTLE_ENDIAN); invalidLittleBuffer.putShort((short) 0x5678); invalidLittleBuffer.flip(); boolean invalidLittleEndian = checkLittleEndianCRC(invalidLittleBuffer); System.out.println("无效数据小端校验结果: " + invalidLittleEndian); } } 这是原始代码,分析代码是否正确
09-23
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值