小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案

小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案

文章目录

      • [小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案](#小程序弱网 / 无网场景下 CacheManager 离线表单与拍照上传解决方案)
一、方案概述

本方案实现一个支持离线操作的表单与拍照上传系统,核心功能包括:

  • 弱网/无网环境下的表单数据本地缓存

  • 拍照文件本地存储与上传队列管理

  • 网络状态实时监听与自动重试机制(最多3次)

  • 任务状态可视化管理

  • 成功任务自动清理本地缓存

二、技术架构
  • 框架:微信小程序原生框架

  • 本地存储:wx.setStorageSync(表单数据) + 本地文件系统(图片)

  • 网络监听:wx.onNetworkStatusChange

  • 状态管理:全局变量+本地存储结合

  • 上传机制:Promise封装+队列管理

三、核心功能实现
1. 项目目录结构
Plain 复制代码
├── app.js                 // 入口文件、网络监听
├── app.json               // 全局配置
├── app.wxss               // 全局样式
├── pages/
│   ├── form/              // 表单页面
│   │   ├── form.js
│   │   ├── form.json
│   │   ├── form.wxml
│   │   └── form.wxss
│   └── task/              // 任务管理页面
│       ├── task.js
│       ├── task.json
│       ├── task.wxml
│       └── task.wxss
└── utils/
    ├── storage.js         // 本地存储工具
    ├── upload.js          // 上传管理工具
    └── network.js         // 网络工具
2. 关键工具类实现

utils/storage.js - 本地存储工具

javascript 复制代码
// 存储键名常量
const STORAGE_KEYS = {
  FORM_TASKS: 'offline_form_tasks', // 表单任务列表
  UPLOAD_QUEUE: 'upload_queue'      // 上传队列
}

// 获取所有任务
function getTasks() {
  const tasks = wx.getStorageSync(STORAGE_KEYS.FORM_TASKS)
  return tasks ? JSON.parse(tasks) : []
}

// 保存任务
function saveTask(task) {
  const tasks = getTasks()
  // 生成唯一ID
  task.id = Date.now() + Math.floor(Math.random() * 1000)
  task.status = 'pending' // pending, uploading, success, failed
  task.retryCount = 0
  task.createTime = new Date().toISOString()
  
  tasks.unshift(task)
  wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))
  return task
}

// 更新任务状态
function updateTaskStatus(taskId, status, data = {}) {
  const tasks = getTasks()
  const index = tasks.findIndex(t => t.id === taskId)
  if (index !== -1) {
    tasks[index].status = status
    tasks[index].retryCount = data.retryCount !== undefined ? data.retryCount : tasks[index].retryCount
    tasks[index].completeTime = status === 'success' ? new Date().toISOString() : tasks[index].completeTime
    wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))
    return tasks[index]
  }
  return null
}

// 删除任务
function deleteTask(taskId) {
  let tasks = getTasks()
  tasks = tasks.filter(t => t.id !== taskId)
  wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))
  
  // 如果有图片,删除本地缓存
  const task = tasks.find(t => t.id === taskId)
  if (task?.images?.length) {
    task.images.forEach(imgPath => {
      wx.removeSavedFile({ filePath: imgPath })
    })
  }
}

// 清空成功任务
function clearSuccessTasks() {
  let tasks = getTasks()
  const successTasks = tasks.filter(t => t.status === 'success')
  
  // 删除成功任务的图片
  successTasks.forEach(task => {
    if (task.images?.length) {
      task.images.forEach(imgPath => {
        wx.removeSavedFile({ filePath: imgPath })
      })
    }
  })
  
  tasks = tasks.filter(t => t.status !== 'success')
  wx.setStorageSync(STORAGE_KEYS.FORM_TASKS, JSON.stringify(tasks))
}

module.exports = {
  getTasks,
  saveTask,
  updateTaskStatus,
  deleteTask,
  clearSuccessTasks,
  STORAGE_KEYS
}

utils/network.js - 网络工具

javascript 复制代码
// 检查网络状态
function checkNetwork() {
  return new Promise((resolve) => {
    wx.getNetworkType({
      success(res) {
        const networkType = res.networkType
        resolve(networkType !== 'none')
      },
      fail() {
        resolve(false)
      }
    })
  })
}

// 监听网络状态变化
function watchNetworkChange(callback) {
  wx.onNetworkStatusChange(res => {
    callback(res.isConnected)
  })
}

module.exports = {
  checkNetwork,
  watchNetworkChange
}

utils/upload.js - 上传管理工具

javascript 复制代码
const { getTasks, updateTaskStatus, deleteTask } = require('./storage')
const { checkNetwork } = require('./network')

// 模拟上传接口(实际项目替换为真实接口)
function uploadFormData(formData, images) {
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 随机成功失败,模拟网络问题
      if (Math.random() > 0.3) {
        resolve({ success: true, data: { id: Date.now() } })
      } else {
        reject(new Error('上传失败'))
      }
    }, 1500)
  })
}

// 上传单张图片
function uploadImage(tempFilePath) {
  return new Promise((resolve, reject) => {
    // 这里使用微信的上传API
    wx.uploadFile({
      url: 'https://your-server.com/upload/image', // 替换为真实接口
      filePath: tempFilePath,
      name: 'image',
      success(res) {
        if (res.statusCode === 200) {
          const data = JSON.parse(res.data)
          resolve(data.url) // 返回图片URL
        } else {
          reject(new Error('图片上传失败'))
        }
      },
      fail(err) {
        reject(err)
      }
    })
  })
}

// 处理单个任务上传
async function processTask(task) {
  const isConnected = await checkNetwork()
  if (!isConnected) {
    return { success: false, reason: '无网络' }
  }

  try {
    // 更新任务状态为上传中
    updateTaskStatus(task.id, 'uploading')
    
    // 上传图片
    const imageUrls = []
    if (task.images && task.images.length) {
      for (const imgPath of task.images) {
        const url = await uploadImage(imgPath)
        imageUrls.push(url)
      }
    }
    
    // 上传表单数据
    const formData = {
      ...task.formData,
      images: imageUrls
    }
    
    const result = await uploadFormData(formData, imageUrls)
    
    if (result.success) {
      // 上传成功,更新状态
      updateTaskStatus(task.id, 'success')
      return { success: true }
    } else {
      throw new Error('表单上传失败')
    }
  } catch (error) {
    // 处理失败
    const retryCount = task.retryCount + 1
    const status = retryCount >= 3 ? 'failed' : 'pending'
    
    updateTaskStatus(task.id, status, { retryCount })
    return { 
      success: false, 
      reason: error.message,
      retryCount
    }
  }
}

// 处理所有待上传任务
async function processAllTasks() {
  const tasks = getTasks()
  const pendingTasks = tasks.filter(
    t => t.status === 'pending' && t.retryCount < 3
  )
  
  // 依次处理任务,避免并发过高
  for (const task of pendingTasks) {
    await processTask(task)
  }
}

module.exports = {
  processTask,
  processAllTasks,
  uploadImage
}
3. 页面实现

pages/form/form.js - 表单页面

javascript 复制代码
const { saveTask } = require('../../utils/storage')
const { checkNetwork } = require('../../utils/network')
const { processTask } = require('../../utils/upload')

Page({
  data: {
    formData: {
      name: '',
      description: '',
      date: ''
    },
    images: [],
    isSubmitting: false,
    networkStatus: true
  },

  onLoad() {
    // 初始化检查网络状态
    checkNetwork().then(isConnected => {
      this.setData({ networkStatus: isConnected })
    })
    
    // 监听网络变化
    wx.onNetworkStatusChange(res => {
      this.setData({ networkStatus: res.isConnected })
    })
  },

  // 表单输入处理
  handleInput(e) {
    const { field } = e.currentTarget.dataset
    this.setData({
      [`formData.${field}`]: e.detail.value
    })
  },

  // 拍照
  takePhoto() {
    const that = this
    wx.chooseImage({
      count: 3 - that.data.images.length, // 最多3张
      sizeType: ['original', 'compressed'],
      sourceType: ['camera'],
      success(res) {
        // 临时路径
        const tempFilePaths = res.tempFilePaths
        
        // 保存到本地,确保离线可用
        tempFilePaths.forEach(tempPath => {
          wx.saveFile({
            tempFilePath: tempPath,
            success(savedRes) {
              const savedPath = savedRes.savedFilePath
              that.setData({
                images: [...that.data.images, savedPath]
              })
            }
          })
        })
      }
    })
  },

  // 预览图片
  previewImage(e) {
    const { index } = e.currentTarget.dataset
    wx.previewImage({
      current: this.data.images[index],
      urls: this.data.images
    })
  },

  // 删除图片
  deleteImage(e) {
    const { index } = e.currentTarget.dataset
    const newImages = [...this.data.images]
    newImages.splice(index, 1)
    this.setData({ images: newImages })
  },

  // 提交表单
  async submitForm() {
    const { name, date } = this.data.formData
    if (!name || !date) {
      wx.showToast({
        title: '请填写必填项',
        icon: 'none'
      })
      return
    }

    this.setData({ isSubmitting: true })

    // 保存任务到本地
    const task = {
      formData: this.data.formData,
      images: this.data.images
    }
    
    const savedTask = saveTask(task)
    
    // 检查网络状态,有网则尝试立即上传
    const isConnected = await checkNetwork()
    if (isConnected) {
      wx.showLoading({ title: '提交中...' })
      try {
        const result = await processTask(savedTask)
        wx.hideLoading()
        
        if (result.success) {
          wx.showToast({ title: '提交成功' })
        } else {
          wx.showToast({ 
            title: '提交失败,将稍后重试', 
            icon: 'none' 
          })
        }
      } catch (error) {
        wx.hideLoading()
        wx.showToast({ 
          title: '提交失败', 
          icon: 'none' 
        })
      }
    } else {
      wx.showToast({ 
        title: '已保存离线数据', 
        icon: 'none' 
      })
    }

    // 重置表单
    this.setData({
      formData: { name: '', description: '', date: '' },
      images: [],
      isSubmitting: false
    })
  }
})

pages/form/form.wxml - 表单页面模板

xml 复制代码
<view class="container">
  <!-- 网络状态提示 -->
  <view wx:if="{{!networkStatus}}" class="network-tip">
    当前无网络,数据将保存到本地
  </view>

  <form class="form-container">
    <view class="form-item">
      <text class="label">姓名 *</text>
      <input 
        type="text" 
        placeholder="请输入姓名" 
        data-field="name"
        bindinput="handleInput"
        value="{{formData.name}}"
      />
    </view>

    <view class="form-item">
      <text class="label">日期 *</text>
      <picker 
        mode="date" 
        start="2020-01-01" 
        end="{{today}}"
        data-field="date"
        value="{{formData.date}}"
        bindchange="handleInput"
      >
        <view class="picker-view">
          {{formData.date || '请选择日期'}}
        </view>
      </picker>
    </view>

    <view class="form-item">
      <text class="label">描述</text>
      <textarea 
        placeholder="请输入描述信息" 
        data-field="description"
        bindinput="handleInput"
        value="{{formData.description}}"
        rows="3"
      />
    </view>

    <!-- 图片上传区域 -->
    <view class="form-item">
      <text class="label">照片</text>
      <view class="image-upload">
        <view 
          class="add-image" 
          bindtap="takePhoto"
          wx:if="{{images.length < 3}}"
        >
          <text>+</text>
        </view>
        
        <view class="image-item" wx:for="{{images}}" wx:key="index">
          <image 
            src="{{item}}" 
            mode="cover"
            bindtap="previewImage"
            data-index="{{index}}"
          />
          <view 
            class="delete-btn" 
            bindtap="deleteImage"
            data-index="{{index}}"
          >
            ×
          </view>
        </view>
      </view>
    </view>

    <button 
      class="submit-btn" 
      bindtap="submitForm"
      disabled="{{isSubmitting}}"
    >
      {{isSubmitting ? '提交中...' : '提交'}}
    </button>
  </form>

  <!-- 跳转到任务管理 -->
  <navigator url="/pages/task/task" class="task-link">
    查看上传任务 →
  </navigator>
</view>

pages/task/task.js - 任务管理页面

javascript 复制代码
const { 
  getTasks, 
  updateTaskStatus, 
  deleteTask,
  clearSuccessTasks
} = require('../../utils/storage')
const { processTask, processAllTasks } = require('../../utils/upload')
const { checkNetwork } = require('../../utils/network')

Page({
  data: {
    tasks: [],
    networkStatus: true
  },

  onLoad() {
    this.loadTasks()
    
    // 检查网络状态
    checkNetwork().then(isConnected => {
      this.setData({ networkStatus: isConnected })
      // 有网则尝试上传所有任务
      if (isConnected) {
        this.processAllTasks()
      }
    })
    
    // 监听网络变化
    wx.onNetworkStatusChange(res => {
      this.setData({ networkStatus: res.isConnected })
      if (res.isConnected) {
        this.processAllTasks()
      }
    })
  },

  onShow() {
    this.loadTasks()
  },

  // 加载任务列表
  loadTasks() {
    const tasks = getTasks()
    this.setData({ tasks })
  },

  // 处理所有任务
  async processAllTasks() {
    wx.showLoading({ title: '同步中...' })
    try {
      await processAllTasks()
      this.loadTasks()
    } catch (error) {
      console.error('处理任务失败', error)
    } finally {
      wx.hideLoading()
    }
  },

  // 重试单个任务
  async retryTask(e) {
    const { id } = e.currentTarget.dataset
    const tasks = getTasks()
    const task = tasks.find(t => t.id === id)
    
    if (!task) return
    
    // 重置重试次数
    updateTaskStatus(id, 'pending', { retryCount: 0 })
    
    wx.showLoading({ title: '重试中...' })
    try {
      await processTask(task)
      this.loadTasks()
    } catch (error) {
      wx.showToast({ title: '重试失败', icon: 'none' })
    } finally {
      wx.hideLoading()
    }
  },

  // 删除任务
  deleteTask(e) {
    const { id } = e.currentTarget.dataset
    wx.showModal({
      title: '确认删除',
      content: '确定要删除此任务吗?',
      success: (res) => {
        if (res.confirm) {
          deleteTask(id)
          this.loadTasks()
        }
      }
    })
  },

  // 清空成功任务
  clearSuccessTasks() {
    wx.showModal({
      title: '确认清空',
      content: '确定要清空所有成功的任务吗?',
      success: (res) => {
        if (res.confirm) {
          clearSuccessTasks()
          this.loadTasks()
          wx.showToast({ title: '已清空' })
        }
      }
    })
  },

  // 预览图片
  previewImage(e) {
    const { images, index } = e.currentTarget.dataset
    wx.previewImage({
      current: images[index],
      urls: images
    })
  }
})

pages/task/task.wxml - 任务管理页面模板

xml 复制代码
<view class="container">
  <!-- 网络状态提示 -->
  <view wx:if="{{!networkStatus}}" class="network-tip">
    当前无网络,无法同步数据
  </view>

  <!-- 操作栏 -->
  <view class="operation-bar">
    <button 
      class="sync-btn" 
      bindtap="processAllTasks"
      wx:if="{{networkStatus}}"
    >
      同步所有任务
    </button>
    <button 
      class="clear-btn" 
      bindtap="clearSuccessTasks"
    >
      清空成功任务
    </button>
  </view>

  <!-- 任务列表 -->
  <view class="task-list">
    <view wx:if="{{tasks.length === 0}}" class="empty-tip">
      暂无任务
    </view>
    
    <view class="task-item" wx:for="{{tasks}}" wx:key="id">
      <view class="task-header">
        <text class="task-name">{{item.formData.name}}</text>
        <text class="task-status {{item.status}}">
          {{item.status === 'pending' ? '等待上传' : 
            item.status === 'uploading' ? '上传中' : 
            item.status === 'success' ? '已完成' : '上传失败'}}
        </text>
      </view>
      
      <view class="task-info">
        <view class="info-item">日期: {{item.formData.date}}</view>
        <view class="info-item">
          照片: {{item.images.length}}张
          <text 
            class="preview-link" 
            bindtap="previewImage"
            data-images="{{item.images}}"
            data-index="0"
            wx:if="{{item.images.length > 0}}"
          >
            预览
          </text>
        </view>
        <view class="info-item">
          创建时间: {{item.createTime.slice(0, 16)}}
        </view>
        <view class="info-item" wx:if="{{item.status === 'failed'}}">
          失败原因: 上传失败,已重试{{item.retryCount}}次
        </view>
      </view>
      
      <view class="task-actions">
        <button 
          class="retry-btn" 
          bindtap="retryTask"
          data-id="{{item.id}}"
          wx:if="{{item.status === 'failed' || (item.status === 'pending' && networkStatus)}}"
        >
          重试
        </button>
        <button 
          class="delete-btn" 
          bindtap="deleteTask"
          data-id="{{item.id}}"
        >
          删除
        </button>
      </view>
    </view>
  </view>
</view>
4. 全局配置

app.js - 入口文件

javascript 复制代码
const { watchNetworkChange } = require('./utils/network')
const { processAllTasks } = require('./utils/upload')

App({
  onLaunch() {
    // 监听网络变化,网络恢复时自动同步
    watchNetworkChange(isConnected => {
      if (isConnected) {
        console.log('网络已恢复,开始同步任务')
        processAllTasks()
      }
    })
  }
})

app.json - 全局配置

json 复制代码
{
  "pages": [
    "pages/form/form",
    "pages/task/task"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "离线表单系统",
    "navigationBarTextStyle": "black"
  },
  "permission": {
    "scope.camera": {
      "desc": "需要使用相机拍摄照片"
    }
  }
}
四、关键功能说明
一、离线数据存储:持久化留存,关闭不丢失
  • 表单数据采用 wx.setStorageSync 同步存储方案,确保数据实时写入本地。
  • 图片文件通过 wx.saveFile 接口保存至小程序本地文件系统,保障资源本地可访问。
  • 所有表单数据与图片资源均实现全量持久化存储,即使关闭小程序或退出账号,数据也不会丢失,重新打开即可恢复。
二、网络监听机制:智能感知,自动同步
  • 页面初始化阶段自动触发网络状态检测,快速识别当前网络连通情况。
  • 启用全局网络状态监听,实时捕捉网络切换(如Wi-Fi/4G切换、断网/复网)事件。
  • 当网络从离线恢复至在线状态时,系统将自动触发待同步任务队列,无需手动操作即可完成数据上传。
三、任务管理流程:状态清晰,操作灵活
  • 任务生命周期:采用「pending(待上传)→ uploading(上传中)→ success(上传成功)/ failed(上传失败)」的状态流转逻辑,状态可视化呈现。
  • 自动重试机制:上传失败的任务将触发自动重试,默认最多重试3次,降低偶发网络问题导致的上传失败概率。
  • 手动操作支持:提供手动重试(针对失败任务)和手动删除(支持失败/成功/待上传任务)功能,满足个性化操作需求。
四、资源清理策略:自动+手动,高效释放空间
  • 自动清理:任务上传成功后,系统将自动清理本地缓存的对应图片文件,减少无效存储占用。
  • 手动清理:提供「清空成功任务」一键操作功能,可批量删除所有状态为"上传成功"的任务记录及关联冗余数据,快速释放本地存储空间。
五、使用说明
1. 表单页面:离线可填,数据自动留存
  • 支持在线/离线状态下填写表单信息,同时可直接拍摄照片或上传本地图片。
  • 无网络环境时,表单数据将通过本地存储机制自动保存,图片资源同步留存至本地文件系统。
  • 无需手动触发保存操作,填写及拍照完成后即时落地,避免数据丢失。
2. 任务管理页面:状态可视化,操作自主可控
  • 集中展示所有任务的实时状态,包括待上传、上传中、上传成功、上传失败,状态分类清晰易辨。
  • 提供针对性操作入口:支持手动触发待上传任务同步、对失败任务发起重试,也可按需删除任意状态任务。
  • 任务列表直观呈现关键信息(如创建时间、任务类型),方便快速定位目标任务。
3. 网络恢复同步:自动触发,无需干预
  • 系统实时监听网络状态,当网络从离线恢复至在线时,将自动启动同步流程。
  • 所有处于"待上传"状态的任务将按顺序批量上传,无需用户手动操作。
  • 同步过程中实时更新任务状态,同步结果可在任务管理页面查看。
4. 任务成功后处理:自动清理,释放空间
  • 当任务上传成功后,系统将自动识别并清理该任务对应的本地缓存图片。
  • 清理过程后台静默执行,不影响用户操作,有效释放本地存储空间,避免冗余资源堆积。
  • 清理完成后,仅保留任务记录(不含本地图片缓存),确保页面加载流畅。
六、扩展建议
1. 任务优先级设置:灵活排序,重点先行
  • 支持在创建任务时,选择「高/中/低」三级优先级,默认优先级为「中」。
  • 任务管理页面支持按优先级筛选、排序,高优先级任务置顶展示,方便快速聚焦关键任务。
  • 网络恢复同步时,系统将优先处理高优先级任务,确保核心数据优先上传完成。
2. 大文件分片上传:断点续传,高效稳定
  • 针对单张超过50MB的图片或大体积文件,自动启用分片上传机制,将文件拆分為10MB/片进行传输。
  • 支持断点续传,若上传过程中网络中断或小程序退出,重新连接后可从已上传分片继续传输,无需重复上传完整文件。
  • 分片传输过程中实时校验数据完整性,避免因分片丢失导致上传失败,提升大文件上传成功率。
3. 表单模板功能:复用模板,快速填报
  • 支持创建自定义表单模板,可保存常用字段(如固定填写项、默认值),生成模板库供重复使用。
  • 提供模板编辑、删除、重命名功能,可根据业务需求灵活调整模板内容。
  • 新建任务时,可直接选择已有模板快速填充表单,减少重复录入操作,提升填报效率。
4. 数据同步进度条:可视化反馈,清晰可控
  • 任务同步(单任务/批量任务)时,页面显示实时进度条,直观展示上传完成百分比。
  • 进度条关联任务详情,同步过程中显示已上传大小、剩余时间(预估),让用户清晰掌握同步状态。
  • 批量同步时,支持查看整体进度与单个任务进度,兼顾全局与细节反馈。
5. 任务过期清理机制:自动减负,避免冗余
  • 支持自定义任务过期规则,可设置「待上传任务30天过期」「失败任务15天过期」「成功任务90天过期」(默认配置)。
  • 过期任务将自动标记为「已过期」,并在任务列表单独分类,不影响正常任务查看。
  • 系统每月自动清理已过期且超过7天未操作的任务及关联本地缓存,也支持手动一键清理所有过期任务。
6. 数据备份与恢复功能:双重保障,防丢防盗
  • 支持本地数据一键备份,将表单数据、任务记录、模板信息备份至本地安全存储目录,生成备份文件(含备份时间戳)。
  • 提供备份文件管理功能,可查看历史备份记录、手动创建新备份、删除无效备份。
  • 当本地数据异常(如误删、数据损坏)时,可选择任意历史备份文件进行恢复,恢复后保留当前未备份的新增数据。

该方案完整实现了离线表单与拍照上传的核心功能,通过本地存储与网络监听结合的方式,确保在弱网/无网环境下也能正常收集数据,并在网络恢复后自动同步,极大提升了小程序在复杂网络环境下的可用性。

相关推荐
Jerry2505092 小时前
微信小程序必要要安装SSL证书吗?小程序SSL详解
网络·网络协议·网络安全·微信小程序·小程序·https·ssl
WKK_2 小时前
uniapp小程序 订阅消息推送
小程序·uni-app
万岳科技程序员小金2 小时前
多端统一的教育系统源码开发详解:Web、小程序与APP的无缝融合
前端·小程序·软件开发·app开发·在线教育系统源码·教育培训app开发·教育培训小程序
麦嘟学编程2 小时前
开发环境搭建之JDK11+maven3.9.8+tomcat9安装
java
小坏讲微服务2 小时前
使用 Spring Cloud Gateway 实现集群
java·spring boot·分布式·后端·spring cloud·中间件·gateway
wa的一声哭了2 小时前
hf中transformers库中generate的greedy_search
android·java·javascript·pytorch·深度学习·语言模型·transformer
.格子衫.2 小时前
Maven的下载与安装
java·maven
Override笑看人生2 小时前
gitlab中maven私有库使用备忘
java·gitlab·maven
不知几秋2 小时前
配置JDK和MAVEN
java·开发语言·maven