小程序蓝牙打印探索与实践(中)

四、小程序 BLE 打印设备连接与生命周期管理

在小程序中,通过 BLE 连接便携式打印机,本质上是对一系列异步系统能力的编排过程,而不仅仅是 API 的顺序调用。

在真实设备环境中,连接过程会受到权限、系统蓝牙状态、设备广播稳定性、信号强度等多种因素影响,因此整个流程需要以"状态机"的方式进行设计,而不是简单的线性调用。

蓝牙能力初始化

在调用任何 BLE 相关 API 之前,必须先初始化蓝牙模块。

复制代码
const openBluetoothAdapter = () => {
  return new Promise((resolve, reject) => {
    uni.openBluetoothAdapter({
      success() {
        resolve()
      },
      fail(err) {
        if (err.errCode === 10001) {
          uni.showToast({
            title: "蓝牙未开启",
            icon: "none"
          })
        }
        reject(err)
      }
    })
  })
}

说明

  • openBluetoothAdapter 是所有 BLE 能力的前置条件
  • 未初始化时调用其他 BLE API 会直接报错
  • 10001 表示系统蓝牙不可用或未开启

同时可以监听蓝牙状态变化:onBluetoothAdapterStateChange 监听手机蓝牙状态的改变。

复制代码
onMounted(() => {
  uni.onBluetoothAdapterStateChange(res => {
    console.log("uni.onBluetoothAdapterStateChange", res)
    // available: 蓝牙模块是否可用(需支持 BLE 且蓝牙已开启)
    // discovering: 蓝牙模块是否处于搜索状态
    const { available } = res
    if (!available) {
      uni.showModal({
        title: "提示",
        content: "系统蓝牙未开启"
      })
    }
  })

BLE 设备扫描

在完成蓝牙模块初始化后,即可开始扫描附近的 BLE 外围设备。

BLE 设备扫描本质上是一个基于广播包的实时发现机制,系统并不会返回"设备列表",而是通过持续监听广播信号来逐步构建可见设备集合。

在小程序中,可以通过 startBluetoothDevicesDiscovery 开始扫描:

复制代码
const startBluetoothDevicesDiscovery = () => {
  return new Promise((resolve, reject) => {
    uni.startBluetoothDevicesDiscovery({
      allowDuplicatesKey: false, // 是否允许重复上报同一设备, true 表示允许
      interval: 0, // 搜索间隔 (1000ms ~ 120000ms)
      success() {
        timeoutId = setTimeout(() => {
          resolve()
          stopBluetoothDevicesDiscovery()
          clearTimeout(timeoutId)
        }, timeout)
      },
      fail(err) {
        reject(err)
      }
    })
  })
}

扫描过程中,通过 onBluetoothDeviceFound 监听设备广播信息:

复制代码
// 搜索到的蓝牙设备列表
const blueDeviceList = ref([])
onMounted(() => {
  uni.onBluetoothDeviceFound(res => {
    res.devices.forEach(device => {
      device.name && blueDeviceList.value.push(device)
    })
  })
})

在实际工程中,BLE 设备通常通过以下字段进行识别:

  • name
  • localName
  • 厂商自定义广播数据(manufacturerData)

建议在扫描阶段就完成设备过滤,而不是在连接阶段再做判断,以减少无效连接尝试。

扫描超时控制

BLE 扫描默认是一个持续过程,如果不主动停止,会一直占用系统资源,并可能影响后续连接操作。因此,在工程实践中,通常需要为扫描设置一个合理的超时时间。

常见做法是:在调用 startBluetoothDevicesDiscovery 后,通过定时器在指定时间后自动停止扫描。

复制代码
let timeoutId = null
const timeout = 5000

const startBluetoothDevicesDiscovery = () => {
  return new Promise((resolve, reject) => {
    uni.startBluetoothDevicesDiscovery({
      allowDuplicatesKey: false, // 是否允许重复上报同一设备, true 表示允许
      interval: 0, // 搜索间隔 (1000ms ~ 120000ms)
      success() {
        timeoutId = setTimeout(() => {
          resolve()
          stopBluetoothDevicesDiscovery()
          clearTimeout(timeoutId)
        }, timeout)
      },
      fail(err) {
        reject(err)
      }
    })
  })
}

const stopBluetoothDevicesDiscovery = () => {
  return new Promise((resolve, reject) => {
    uni.stopBluetoothDevicesDiscovery({
      success() {
        resolve()
      },
      fail(err) {
        reject(err)
      }
    })
  })
}
扫描异常与设备发现不完整问题

在真实设备环境中(尤其是 iOS 与 Android),BLE 扫描可能出现以下异常行为:

  • 已扫描到的设备不会重复上报
  • 某些设备在重新扫描后无法再次被发现
  • 明确存在广播的设备,但扫描结果为空
  • 多次调用扫描 API 后仍无法恢复历史设备显示

该问题通常并非设备异常,而是由于系统层 BLE 扫描状态、缓存机制或扫描会话未完全释放导致。

✔ 解决方案:重置蓝牙适配器状态

在小程序中,可以通过重启蓝牙适配器来清理系统扫描上下文,从而恢复设备发现能力:

复制代码
const closeBluetoothAdapter = () => {
  return new Promise((resolve, reject) => {
    uni.closeBluetoothAdapter({
      success() {
        resolve()
      },
      fail(err) {
        reject(err)
      }
    })
  })
}
const resetBluetoothAdapter = async () => {
  // 清空搜索到的蓝牙设备列表
  blueDeviceList.value = []
  // 清除定时器
  clearTimeout(timeoutId)
  // 停止搜索附近蓝牙设备
  await stopBluetoothDevicesDiscovery()
  // 关闭蓝牙模块
  await closeBluetoothAdapter()
  // 等待系统释放资源(非常重要)
  await new Promise(resolve => setTimeout(resolve, 300))

  // 重新初始化蓝牙模块
  await openBluetoothAdapter()

  uni.showToast({
    title: "蓝牙已重置",
    icon: "success"
  })
}

由于 closeBluetoothAdapter → openBluetoothAdapter 会带来系统层状态重建开销,因此不建议频繁调用。

建立蓝牙连接

建立连接

在确定目标打印机后,使用 deviceId 建立连接。

复制代码
const connectBluetooth = async deviceId => {
  uni.createBLEConnection({
    deviceId,
    success() {
      console.log("设备连接成功")
      // 连接成功后立即停止扫描,避免干扰后续操作
      stopBluetoothDevicesDiscovery()
      // 获取服务列表
      getDeviceServices(deviceId)
    },
    fail(err) {
      console.error("连接失败", err)
    }
  })
}

说明

  • 若设备已连接,再次连接通常会直接返回成功
  • 建议连接成功后立即停止扫描,避免资源竞争
  • 若"可发现但无法连接",优先检查是否仍在扫描状
获取 Services

连接成功后,需要获取设备暴露的 Service 列表。

复制代码
function getDeviceServices(deviceId) {
  uni.getBLEDeviceServices({
    deviceId,
    success(res) {
      console.log("Services列表:", res.services)
      // 通常打印服务使用自定义 UUID
      const services = res.services.sort((a, b) => getServicePriority(a.uuid) - getServicePriority(b.uuid))
      const printService = services.find(service => service.uuid.includes("FF00") || service.isPrimary)
      if (printService) {
        getServiceCharacteristics(deviceId, printService.uuid)
      }
    },
    fail(err) {
      console.error("获取服务失败", err)
    }
  })
}

在实际设备中(尤其是打印机),往往会同时暴露多个 Service,其中只有极少数才是真正用于数据传输(打印)的通道。因此,需要建立一套合理的过滤与优先级策略,用于筛选候选 Service。

Service 的筛选可以遵循以下经验规则:

  1. 排除通用标准服务

    • 1800 / 1801 / 180A / 180F
    • 这些服务几乎不可能承载打印数据
  2. 优先选择 FFxx 类服务

    • FFF0 / FFE0 / FF00
    • 通常为串口透传服务(Serial over BLE)
    • 大多数打印机使用该通道接收 ESC/POS 指令
  3. 谨慎对待厂商自定义 UUID

    • 49535343-xxxx
    • 可能是蓝牙模块内部服务,而非打印通道
  4. 其余 Service 作为候选补充

    • 不能完全忽略,但优先级较低

    const getServicePriority = uuid => {
    const u = uuid.toLowerCase()

    // 提取短 UUID(如 0000fff0)
    const shortUUID = u.startsWith("0000") ? u.slice(4, 8) : ""

    // 1. 标准服务(最低优先级)
    const standardServices = ["1800", "1801", "180a", "180f"]
    if (standardServices.includes(shortUUID)) {
    return 100
    }

    // 2. 打印透传服务(最高优先级)
    if (shortUUID.startsWith("ff")) {
    return 0
    }

    // 3. 纯 128 位厂商 UUID(中优先级,需验证)
    if (!u.includes("0000-1000-8000-00805f9b34fb")) {
    return 50
    }

    // 4. 其他标准扩展服务
    return 60
    }

在获取 Service 列表后,可结合排序使用:

复制代码
      const services = res.services.sort((a, b) => getServicePriority(a.uuid) - getServicePriority(b.uuid))
      const printService = services.find(service => service.uuid.includes("FF00") || service.isPrimary)
获取 Characteristics

确定目标 Service 后,需要进一步获取其下的 Characteristic。示例代码如下:

复制代码
function getServiceCharacteristics(deviceId, serviceId) {
  console.log("deviceId", deviceId)
  console.log("serviceId", serviceId)
  uni.getBLEDeviceCharacteristics({
    deviceId,
    serviceId,
    success(res) {
      console.log("Characteristics列表:", res.characteristics)
      let writeCharId = null
      let notifyCharId = null
      res.characteristics.forEach(c => {
        if (c.properties.write || c.properties.writeWithoutResponse) {
          writeCharId = c.uuid
        }
        if (c.properties.notify || c.properties.indicate) {
          notifyCharId = c.uuid
        }
      })
      console.log("writeCharId", writeCharId)
      console.log("notifyCharId", notifyCharId)
      // 保存特征值 ID,启用 Notify
      enableNotify(deviceId, serviceId, notifyCharId)
    },
    fail(err) {
      console.error("获取特征值失败", err)
    }
  })
}

说明:

每个 Characteristic 都会声明自己的能力属性:

  • properties.write
  • properties.writeWithoutResponse
  • properties.notify
  • properties.read

这些属性,直接决定了是否能用于打印数据写入。打印通常至少需要两个特征:

  • 可写特征 :用于写入打印数据,但需要特别注意 write ≠ 一定可用于打印

Characteristic 支持 write,仅代表"可以写入数据",并不代表打印机会执行这些数据。

  • 通知特征(可选):用于接收打印状态回传,状态通知特征并非必需,但在批量或大数据打印时非常重要
启用特征值变化通知

对于支持 notifyindicate 的特征值,需调用notifyBLECharacteristicValueChange 启用通知功能。示例代码如下:

复制代码
function enableNotify(deviceId, serviceId, characteristicId) {
  uni.notifyBLECharacteristicValueChange({
    deviceId,
    serviceId,
    characteristicId,
    state: true, // 启用 notify
    success() {
      console.log("Notify 已启用")
      // 监听特征值变化事件
      uni.onBLECharacteristicValueChange(res => {
        const hexStr = res.value // 返回 hex 字符串
        console.log("Notify 收到数据", hexStr)
      })
    },
    fail(err) {
      console.error("启用 Notify 失败", err)
    }
  })
}

说明

  • 必须先启用 notify 才能监听到设备 characteristicValueChange 事件。
  • 设备的特征值必须支持 notifyindicate 才可以成功调用,具体参照 characteristic 的 properties 属性。
  • 订阅操作成功后,需要设备主动更新特征值的 value,才会触发 onBLECharacteristicValueChange
  • 订阅方式效率比较高,推荐使用订阅代替 read 方式。
  • 注意调用顺序 :最好在连接之后就调用 notifyBLECharacteristicValueChange 方法。
被动断开:监听与自动重连

蓝牙连接随时可能因为距离过远、设备断电、信号干扰等原因而意外断开。我们必须通过监听连接状态变化事件来应对这种情况。

复制代码
// 监听连接状态变化
uni.onBLEConnectionStateChanged((res) => {
  if (!res.connected) {
    // 做相应的处理
  }
});

说明

  • 若对未连接的设备调用数据读写操作接口,会返回 10006 错误,此时应执行重连。
  • 避免重复监听 :每次调用 on 方法监听事件之前,最好先调用 off 方法关闭之前的事件监听,防止多次注册导致事件被多次触发。
主动断开与清理:完整的退出机制

当用户主动关闭页面或完成打印后,我们需要手动断开连接并释放系统资源。一个标准的清理流程应该包含三步:

  1. 停止搜索设备(如果还在搜索中)

  2. 断开设备连接,

  3. 关闭蓝牙适配器。

    const releaseBluetoothResources = () => {
    // 1. 停止搜索设备(如果还在搜索中)
    stopBluetoothDevicesDiscovery()

    // 2. 断开与当前已连接蓝牙设备的连接
    if (activeDeviceId.value) {
    uni.closeBLEConnection({
    deviceId: activeDeviceId.value,
    success: () => {
    console.log("成功断开设备连接")
    },
    fail: err => {
    console.error("断开设备连接失败", err)
    }
    })
    }

    // 3. 最后,关闭蓝牙适配器,彻底释放系统资源
    closeBluetoothAdapter()
    .then(() => {
    console.log("蓝牙适配器已关闭,资源已释放")
    // 重置所有连接相关状态
    activeDeviceId.value = ""
    blueDeviceList.value = []
    })
    .catch(err => {
    console.error("关闭蓝牙适配器失败", err)
    })
    }

说明

  • 分步操作 :虽然 closeBluetoothAdapter 会断开所有连接并释放资源,但为了逻辑清晰和状态可控,建议还是显式地调用 closeBLEConnection 进行清理。
  • 调用时机 :此方法建议在页面的 onUnload 生命周期中调用。因为 closeBluetoothAdapter 是异步操作,不建议将其与 openBluetoothAdapter 一起用作异常处理,效率低且易引发线程同步问题。
  • 页面卸载 :点击小程序右上角关闭按钮时,小程序可能仅进入后台而非立即销毁,因此需要在 onHideonUnload 中主动调用清理逻辑,确保连接被及时断开。

工程化建议

蓝牙打印的交互链路很长,涉及权限、扫描、连接、状态管理、异常恢复等诸多环节。若将所有逻辑耦合在一起,代码会迅速膨胀且难以维护。建议将蓝牙能力拆分为两个独立实体:

BluetoothAdapter(能力适配层)

负责与蓝牙 API 的直接交互,向上屏蔽平台差异与底层细节。核心职责:

  • API 适配 :封装 openBluetoothAdapter、startBluetoothDevicesDiscovery({ 等基础调用,统一返回 Promise 接口
  • 权限校验:收敛蓝牙与定位权限的检查逻辑
  • 异常告警:统一捕获蓝牙错误码,映射为业务可理解的提示(如"系统蓝牙未开启")
  • 埋点上报:记录扫描耗时、连接成功率、异常断开次数等关键指标
createBLEConnection(连接实例层)

每一次打印任务对应一个连接实例,内置完整的状态机与连接属性。核心职责:

  • 连接属性 :持有 deviceIdserviceIdwriteCharIdnotifyCharId 等关键标识
  • 状态机 :管理 idle → scanning → connecting → ready → disconnecting 等状态流转,杜绝非法操作
  • 生命周期:统一处理连接建立、心跳维持、异常断连重试、资源释放
  • 事件管理 :自动绑定与解绑蓝牙 等事件监听,防止泄漏

小结

BLE 打印连接的本质并不是一次性调用成功,而是一个持续运行的状态机系统,其核心能力在于:

  • 连接状态管理
  • 异常恢复机制
  • 事件驱动模型

只有在稳定连接的基础上,才能保证打印数据的可靠传输与状态反馈。

在完成稳定的 BLE 连接建立之后,下一步需要解决的问题是:如何将业务数据转换为打印机可识别的二进制指令流,这将引出打印领域的核心协议模型------ESC/POS 指令体系

复制代码
<template>
  <view>
    <scroll-view scroll-y class="box">
      <view class="item" v-for="item in blueDeviceList" :key="item.deviceId" @click="connectBluetooth(item.deviceId)">
        <view>
          <text>id: {{ item.deviceId }}</text>
        </view>
        <view>
          <text>name: {{ item.name }}</text>
        </view>
      </view>
    </scroll-view>
    <button @click="resetBluetoothAdapter">重置蓝牙</button>
    <button @click="openBluetoothAdapter">1 初始化蓝牙</button>
    <button @click="startBluetoothDevicesDiscovery">2 搜索附近蓝牙设备</button>
    <button @click="releaseBluetoothResources">销毁</button>
  </view>
</template>

<script setup>
import { onMounted, ref } from "vue" // 补上了 ref 的引入

let timeoutId = null
const timeout = 5000

// 搜索到的蓝牙设备列表
const blueDeviceList = ref([])

// 新增:全局记录当前连接的设备 ID
const activeDeviceId = ref("")

onMounted(() => {
  uni.onBluetoothAdapterStateChange(res => {
    console.log("uni.onBluetoothAdapterStateChange", res)

    const { available } = res

    if (!available) {
      uni.showModal({
        title: "提示",
        content: "系统蓝牙未开启"
      })
    }
  })

  uni.onBluetoothDeviceFound(res => {
    res.devices.forEach(device => {
      device.name && blueDeviceList.value.push(device)
    })
  })
})

const openBluetoothAdapter = () => {
  return new Promise((resolve, reject) => {
    uni.openBluetoothAdapter({
      success() {
        resolve()
      },
      fail(err) {
        if (err.errCode === 10001) {
          uni.showToast({
            title: "蓝牙未开启",
            icon: "none"
          })
        }

        reject(err)
      }
    })
  })
}

const startBluetoothDevicesDiscovery = () => {
  return new Promise((resolve, reject) => {
    uni.startBluetoothDevicesDiscovery({
      allowDuplicatesKey: false, // 是否允许重复上报同一设备, true 表示允许
      interval: 0, // 搜索间隔 (1000ms ~ 120000ms)
      success() {
        timeoutId = setTimeout(() => {
          resolve()
          stopBluetoothDevicesDiscovery()
          clearTimeout(timeoutId)
        }, timeout)
      },
      fail(err) {
        reject(err)
      }
    })
  })
}

const stopBluetoothDevicesDiscovery = () => {
  return new Promise((resolve, reject) => {
    uni.stopBluetoothDevicesDiscovery({
      success() {
        resolve()
      },
      fail(err) {
        reject(err)
      }
    })
  })
}
const closeBluetoothAdapter = () => {
  return new Promise((resolve, reject) => {
    uni.closeBluetoothAdapter({
      success() {
        resolve()
      },
      fail(err) {
        reject(err)
      }
    })
  })
}
const resetBluetoothAdapter = async () => {
  // 清空搜索到的蓝牙设备列表
  blueDeviceList.value = []
  // 清空当前连接的 ID
  activeDeviceId.value = ""
  // 清除定时器
  clearTimeout(timeoutId)
  // 停止搜索附近蓝牙设备
  await stopBluetoothDevicesDiscovery()
  // 关闭蓝牙模块
  await closeBluetoothAdapter()
  // 等待系统释放资源(非常重要)
  await new Promise(resolve => setTimeout(resolve, 300))

  // 重新初始化蓝牙模块
  await openBluetoothAdapter()

  uni.showToast({
    title: "蓝牙已重置",
    icon: "success"
  })
}
const connectBluetooth = async deviceId => {
  uni.createBLEConnection({
    deviceId,
    success() {
      console.log("设备连接成功")
      // 核心改动:连接成功后保存当前设备 ID
      activeDeviceId.value = deviceId

      // 连接成功后立即停止扫描,避免干扰后续操作
      stopBluetoothDevicesDiscovery()
      // 获取服务列表
      getDeviceServices(deviceId)
    },
    fail(err) {
      console.error("连接失败", err)
    }
  })
}
function getDeviceServices(deviceId) {
  uni.getBLEDeviceServices({
    deviceId,
    success(res) {
      console.log("Services列表:", res.services)
      // 通常打印服务使用自定义 UUID
      const services = res.services.sort((a, b) => getServicePriority(a.uuid) - getServicePriority(b.uuid))
      const printService = services.find(service => service.uuid.includes("FF00") || service.isPrimary)
      if (printService) {
        getServiceCharacteristics(deviceId, printService.uuid)
      }
    },
    fail(err) {
      console.error("获取服务失败", err)
    }
  })
}

function getServiceCharacteristics(deviceId, serviceId) {
  console.log("deviceId", deviceId)
  console.log("serviceId", serviceId)
  uni.getBLEDeviceCharacteristics({
    deviceId,
    serviceId,
    success(res) {
      console.log("Characteristics列表:", res.characteristics)
      let writeCharId = null
      let notifyCharId = null
      res.characteristics.forEach(c => {
        if (c.properties.write || c.properties.writeWithoutResponse) {
          writeCharId = c.uuid
        }
        if (c.properties.notify || c.properties.indicate) {
          notifyCharId = c.uuid
        }
      })
      console.log("writeCharId", writeCharId)
      console.log("notifyCharId", notifyCharId)
      // 保存特征值 ID,启用 Notify
      enableNotify(deviceId, serviceId, notifyCharId)
    },
    fail(err) {
      console.error("获取特征值失败", err)
    }
  })
}
function enableNotify(deviceId, serviceId, characteristicId) {
  uni.notifyBLECharacteristicValueChange({
    deviceId,
    serviceId,
    characteristicId,
    state: true, // 启用 notify
    success() {
      console.log("Notify 已启用")
      // 监听特征值变化事件
      uni.onBLECharacteristicValueChange(res => {
        const hexStr = res.value // 返回 hex 字符串
        console.log("Notify 收到数据", hexStr)
      })
    },
    fail(err) {
      console.error("启用 Notify 失败", err)
    }
  })
}
const getServicePriority = uuid => {
  const u = uuid.toLowerCase()

  // 提取短 UUID(如 0000fff0)
  const shortUUID = u.startsWith("0000") ? u.slice(4, 8) : ""

  // 1. 标准服务(最低优先级)
  const standardServices = ["1800", "1801", "180a", "180f"]
  if (standardServices.includes(shortUUID)) {
    return 100
  }

  // 2. 打印透传服务(最高优先级)
  if (shortUUID.startsWith("ff")) {
    return 0
  }

  // 3. 纯 128 位厂商 UUID(中优先级,需验证)
  if (!u.includes("0000-1000-8000-00805f9b34fb")) {
    return 50
  }

  // 4. 其他标准扩展服务
  return 60
}

// 核心改动:修改后的资源清理方法
const releaseBluetoothResources = () => {
  // 1. 停止搜索设备(如果还在搜索中)
  stopBluetoothDevicesDiscovery()

  // 2. 断开与当前已连接蓝牙设备的连接
  if (activeDeviceId.value) {
    uni.closeBLEConnection({
      deviceId: activeDeviceId.value,
      success: () => {
        console.log("成功断开设备连接")
      },
      fail: err => {
        console.error("断开设备连接失败", err)
      }
    })
  }

  // 3. 最后,关闭蓝牙适配器,彻底释放系统资源
  closeBluetoothAdapter()
    .then(() => {
      console.log("蓝牙适配器已关闭,资源已释放")
      // 重置所有连接相关状态
      activeDeviceId.value = ""
      blueDeviceList.value = []
    })
    .catch(err => {
      console.error("关闭蓝牙适配器失败", err)
    })
}
</script>

<style>
.box {
  width: 98%;
  height: 400rpx;
  box-sizing: border-box;
  margin: 0 auto 20rpx;
  border: 2px solid dodgerblue;
}
.item {
  box-sizing: border-box;
  padding: 10rpx;
  border-bottom: 1px solid #ccc;
}
button {
  margin-bottom: 20rpx;
}

.msg_x {
  border: 2px solid seagreen;
  width: 98%;
  margin: 10rpx auto;
  box-sizing: border-box;
  padding: 20rpx;
}

.msg_x .msg_txt {
  margin-bottom: 20rpx;
}
</style>
相关推荐
SeaTunnel2 小时前
87 个 PR 迭代复盘|Apache SeaTunnel 5 月版本重点更新解读
大数据·数据库·开源·apache·seatunnel
DolphinScheduler社区2 小时前
实战演示 | 基于 Apache DolphinScheduler 与 Apache SeaTunnel 实现 MySQL 到 Doris 离线定时增量同步
数据库·mysql·开源·apache·海豚调度·大数据工作流调度
chéng ௹2 小时前
uniapp封装火山引擎 DataRangers 埋点 SDK
uni-app·apache·火山引擎
阿坤带你走近大数据3 小时前
Apache Hop的详细介绍
apache
就叫_这个吧2 天前
servlet整合tomcat项目启动报错解决,org.apache.tomcat.util.descriptor.web.WebXml.setVersion
java·servlet·tomcat·apache
云器科技2 天前
Apache Iceberg-cpp:原生性能架构与演进路线
架构·apache
Par@ish2 天前
Ubuntu Apache日志存储周期变更
linux·ubuntu·apache
一次旅行4 天前
【数据分析/可视化】Apache Superset企业级BI数据可视化平台实战详解
信息可视化·数据分析·apache
万岳科技系统开发5 天前
互联网医院小程序搭建如何快速上线?完整建设方案解析
小程序·apache