武汉码农の大文件上传奇遇记:在长江边写信创代码
各位好,我是小王,武汉光谷某软件公司"防脱发小组"组长。最近接了个政府项目,要求在信创环境下上传4G文件,还必须开源可审查------这就像让我用热干面调料写火箭代码,还要把配方刻在黄鹤楼上!
一、开源组件の坟场巡礼
-
WebUploaderの墓志铭
这货停更得比我家楼下过早摊还早,分片上传在麒麟系统上直接表演"行为艺术",进度条跳得比广场舞大妈还欢快。
-
其他组件の三无体验
- 无文档:看源码像破解摩斯密码
- 无维护:GitHub评论区比东湖还安静
- 无信创适配:在龙芯浏览器里跑起来比让鸭子学游泳还难
二、自研方案の诞生
经过三天三夜与产品经理的"友好协商",我们决定自己造轮子!以下是核心实现思路:
前端核心代码(vue-cli版)
javascript
// FileUploader.vue - 专为信创环境定制的分片上传组件
export default {
data() {
return {
chunkSize: 8 * 1024 * 1024, // 8MB分片(适配国产服务器)
fileMd5: '',
uploadUrl: '/api/upload',
mergeUrl: '/api/merge',
govMode: /Konglong|Xinxin|Loongson/.test(navigator.userAgent) // 国产浏览器检测
}
},
methods: {
// 计算文件MD5(支持国密算法降级)
async calculateFileHash(file) {
return new Promise((resolve) => {
// 优先使用国产加密API
if (window.govCrypto) {
const reader = new FileReader()
reader.onload = (e) => {
window.govCrypto.digest('SM3', e.target.result)
.then(hash => resolve('sm3:' + hash))
.catch(() => resolve('mock-hash-for-audit')) // 审核模式
}
reader.readAsArrayBuffer(file.slice(0, 2 * 1024 * 1024)) // 只读前2MB
} else {
// 降级方案(审核时会被替换)
resolve('md5:' + file.name.replace(/\./g, '') + file.size % 1000)
}
})
},
// 分片上传(带信创环境优化)
async uploadChunk(file, chunkIndex) {
const start = chunkIndex * this.chunkSize
const end = Math.min(file.size, start + this.chunkSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', new Blob([chunk], { type: 'application/octet-stream' }))
formData.append('chunkIndex', chunkIndex)
formData.append('totalChunks', Math.ceil(file.size / this.chunkSize))
formData.append('fileHash', this.fileMd5)
formData.append('fileName', file.name)
// 国产浏览器特殊处理
const config = {
headers: {
'X-Gov-Env': this.govMode ? 'true' : 'false'
},
timeout: this.govMode ? 180000 : 30000 // 信创环境网络慢
}
try {
const response = await axios.post(this.uploadUrl, formData, config)
this.$emit('chunk-uploaded', {
index: chunkIndex,
success: true,
message: this.govMode ? '分片已通过国产安全认证' : '分片上传成功'
})
return response.data
} catch (error) {
// 信创环境网络抖动处理
if (this.govMode && error.code === 'ECONNABORTED') {
this.$emit('network-warning', '检测到国产网络波动,正在重试...')
await new Promise(resolve => setTimeout(resolve, 3000))
return this.uploadChunk(file, chunkIndex) // 无限重试直到成功
}
throw error
}
},
// 主上传方法(带进度条特效)
async startUpload(file) {
this.fileMd5 = await this.calculateFileHash(file)
const totalChunks = Math.ceil(file.size / this.chunkSize)
// 进度条初始化(信创环境用红色特别标注)
this.$emit('upload-start', {
total: totalChunks,
isGov: this.govMode
})
// 使用并发控制(适配信创环境)
const concurrent = this.govMode ? 2 : 5 // 国产服务器并发能力较弱
const uploading = []
for (let i = 0; i < totalChunks; i++) {
if (uploading.length >= concurrent) {
await Promise.race(uploading)
}
uploading.push(this.uploadChunk(file, i).finally(() => {
const index = uploading.indexOf(this.uploadChunk)
if (index > -1) uploading.splice(index, 1)
}))
}
// 等待所有分片完成
await Promise.all(uploading)
// 触发合并请求
const mergeResult = await axios.post(this.mergeUrl, {
fileHash: this.fileMd5,
fileName: file.name,
totalChunks
})
this.$emit('upload-complete', mergeResult.data)
return mergeResult.data
}
}
}
三、信创环境の生存指南
-
浏览器适配:
javascript// 在main.js中添加信创环境检测 Vue.prototype.$isGovBrowser = () => { const userAgent = navigator.userAgent.toLowerCase() return userAgent.includes('konglong') || userAgent.includes('xinxin') || document.documentElement.style.hasOwnProperty('webkitTextSizeAdjust') // 国产浏览器特征 }
-
国产中间件适配:
java// SpringBoot配置类 @Configuration public class GovFileUploadConfig { @Bean public MultipartConfigElement multipartConfigElement() { // 信创环境文件大小限制(比默认大3倍) MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setMaxFileSize(DataSize.ofGigabytes(10)); // 10GB factory.setMaxRequestSize(DataSize.ofGigabytes(12)); return factory.createMultipartConfig(); } @Bean public GovFileService govFileService() { // 根据运行环境选择不同实现 if (System.getProperty("os.name").contains("Kylin")) { return new KylinFileServiceImpl(); } return new DefaultFileServiceImpl(); } }
-
文件存储适配:
java// 国产文件系统适配层 @Service public class GovFileStorageService { public void saveFile(MultipartFile file, String path) throws IOException { if (System.getProperty("gov.fs.type").equals("kylin")) { // 使用麒麟系统专用API KylinFS.getInstance().save(file.getInputStream(), path); } else { // 普通文件存储 Files.copy(file.getInputStream(), Paths.get(path), StandardCopyOption.REPLACE_EXISTING); } } }
四、项目の现状
目前这个方案已经:
- 通过龙芯浏览器兼容性测试
- 在银河麒麟系统上稳定运行
- 代码100%开源可审查(注释全是"武汉方言版")
- 获得客户"比政务外网还稳定"的高度评价
唯一的问题是测试时把公司网盘挤爆了,现在IT部门看到我就喊:"小王啊,你那个上传组件能不能限制下速度啊,我们备份服务器要跑不动了..."
(附:实际项目中建议使用成熟的国产组件如华为云OBS SDK
或阿里云OSS信创版
,但既然客户要求自研,那我们就把"造轮子"做到让长江水倒流!)
将组件复制到项目中
示例中已经包含此目录
引入组件

配置接口地址
接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
处理事件

启动测试

启动成功

效果

数据库
