文件上传数据表
在这个指南中,我们将详细介绍如何设计和实现一个文件上传和管理系统的数据库
1. 文件表设计
首先,我们需要设计一个表来存储上传文件的相关信息。以下是一个示例的 SQL 表结构:
-- 请手动执行建表 SQL。已有表时先核对字段,不要删除已有数据。
CREATE TABLE tio_boot_admin_system_upload_file (
id BIGINT NOT NULL, -- 文件ID
md5 VARCHAR(32) NOT NULL, -- 文件的MD5值,用于校验文件一致性
name VARCHAR(255) NOT NULL, -- 文件原名
size BIGINT NOT NULL, -- 文件大小,单位为字节
user_id VARCHAR(32), -- 用户ID,标识上传文件的用户
platform VARCHAR(64) NOT NULL, -- 上传平台(如S3)
region_name VARCHAR(64), -- 区域名
bucket_name VARCHAR(255) NOT NULL, -- 存储桶名称
file_id VARCHAR(255) NOT NULL, -- 文件存储ID
target_name VARCHAR(1024) NOT NULL, -- 文件存储路径
tags JSON, -- 文件标签,使用JSON格式
creator VARCHAR(64) DEFAULT '', -- 创建者
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updater VARCHAR(64) DEFAULT '', -- 更新者
update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
deleted SMALLINT NOT NULL DEFAULT 0, -- 删除标志
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID
PRIMARY KEY (id) -- 主键
);
create index "index_tio_boot_admin_system_upload_file_md5" on tio_boot_admin_system_upload_file("md5");
字段容量设计
| 字段 | 类型 | 设计依据 |
|---|---|---|
name | VARCHAR(255) | 保存原始文件名,兼容较长中文名称;不包含本地目录 |
target_name | VARCHAR(1024) | 保存对象路径,允许分类、编号和后缀,不复用原名的短字段容量 |
bucket_name | VARCHAR(255) | 为不同存储平台的桶名称预留空间;平台本身的命名规则单独验证 |
file_id | VARCHAR(255) | 保存平台返回的文件标识或 ETag,不假定所有平台都是 32/64 位 |
region_name | VARCHAR(64) | 容纳区域标识 |
md5 | VARCHAR(32) | 保存 MD5 的 32 位十六进制文本 |
size | BIGINT | 文件内容字节数,避免使用 int 限制大文件 |
platform、用户及审计字段保持原有容量和类型,保留 tags JSON,避免本次字段扩容引入额外数据迁移。应用应确保 size 非负;是否接受空文件由上传接口决定。
PostgreSQL 的 VARCHAR 长度以字符计数,云平台对象路径可能按 UTF-8 字节计数,二者不能等同。统一上传示例把分类限制为 128 位 ASCII 字母、数字、下划线或短横线,后缀使用字母数字,路径由“分类/雪花 ID.后缀”组成;即使文件名达到 255 位,生成的对象路径仍不超过 404 字节。业务若改为使用中文目录或原名作为对象路径,应增加所选平台的字节长度校验。
文件原名可以包含中文;Java 示例使用 String.length() 控制 255 个 UTF-16 编码单元,包含补充字符(例如部分 emoji)时,比数据库字符长度上限更保守。分类是应用参数,不新增分类列,最终路径保存到 target_name。
已有表的扩容 SQL
手动执行以下迁移,再部署使用新长度的代码。迁移只扩大字段,不删除表、不重建数据、不改动业务关联。字段扩容会获取表锁,请选择适合的执行时机。不要在应用启动时自动执行。
-- Execute manually against the application schema before deploying the new upload limits.
-- Widen existing columns without dropping the table or changing existing records.
BEGIN;
ALTER TABLE tio_boot_admin_system_upload_file
ALTER COLUMN name TYPE VARCHAR(255),
ALTER COLUMN region_name TYPE VARCHAR(64),
ALTER COLUMN bucket_name TYPE VARCHAR(255),
ALTER COLUMN file_id TYPE VARCHAR(255),
ALTER COLUMN target_name TYPE VARCHAR(1024);
COMMENT ON COLUMN tio_boot_admin_system_upload_file.name IS 'Original filename; maximum 255 characters';
COMMENT ON COLUMN tio_boot_admin_system_upload_file.size IS 'File content length in bytes';
COMMENT ON COLUMN tio_boot_admin_system_upload_file.target_name IS 'Object key; validate provider byte limits separately';
COMMIT;
插入示例数据:
INSERT INTO tio_boot_admin_system_upload_file (
id, md5, name, size, user_id, platform, bucket_name, file_id, target_name, tags, creator, create_time, updater, update_time, deleted, tenant_id
) VALUES (
1, -- 文件ID
'd41d8cd98f00b204e9800998ecf8427e', -- MD5值
'example.txt', -- 文件名
1024, -- 文件大小
'user123', -- 用户ID
's3', -- 平台
'sd', -- 存储桶名称
'367962274737995776', -- 文件存储ID
'public/images/367962274737995776.png', -- 文件存储路径
'{"genre": "text", "language": "English"}', -- 标签,JSON格式
'admin', -- 创建者
CURRENT_TIMESTAMP, -- 创建时间
'admin', -- 更新者
CURRENT_TIMESTAMP, -- 更新时间
0, -- 删除标志
100 -- 租户ID
);
该表记录了文件的基本信息和元数据,如文件的 MD5 值、文件名、文件大小、存储路径和标签等。
2. 业务表设计
业务表用于存储与业务相关的信息,同时记录与文件关联的数据。以下是业务表的示例:
CREATE TABLE professors (
id BIGINT NOT NULL, -- 教授ID
name VARCHAR(256), -- 教授姓名
department VARCHAR(256), -- 所属部门
email VARCHAR(256), -- 邮箱
description TEXT, -- 描述
files JSON, -- 文件信息,JSON格式
remark VARCHAR(256), -- 备注
creator VARCHAR(64) DEFAULT '', -- 创建者
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updater VARCHAR(64) DEFAULT '', -- 更新者
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
deleted SMALLINT DEFAULT 0, -- 删除标志
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID
PRIMARY KEY (id) -- 主键
);
文件字段的示例数据如下:
[
{
"uid": "1719137636702",
"name": "image.png",
"status": "done",
"size": 424869,
"type": "image/png",
"id": "394523104107352064",
"url": "https://rumiapp.s3.us-west-1.amazonaws.com/sjsj/professors/394523099162267648.png"
},
{
"uid": "1719137646121",
"name": "image.png",
"status": "done",
"size": 424869,
"type": "image/png",
"id": "394523144213319680",
"url": "https://rumiapp.s3.us-west-1.amazonaws.com/sjsj/professors/394523138647478272.png"
}
]
3. 上传场景业务逻辑描述
1. 检查文件是否已存在
前端生成文件的 MD5 值,通过后端的 URL 接口判断对应的文件是否存在。如果文件存在,返回如下数据:
GET /api/system/file/s3/url?md5=c9fa90c0e145f2d62560f5431b271326
响应示例:
{
"code": 1,
"data": {
"id": 394519014493130752,
"url": "https://rumiapp.s3.us-west-1.amazonaws.com/public%2Fimages%2F394519008180703232.png"
},
"ok": true
}
2. 上传文件
如果文件不存在,前端调用上传接口上传文件,后端同时将文件信息存入文件表。上传完成后,返回包含文件信息的 id 和 url。
3. 提交业务数据
前端在完成业务数据录入后,将包含文件字段的数据提交到后端。请求示例如下:
POST /api/professor/create
{
"name": "Tong Li",
"其他业务字段": "其他值",
"files": [
{
"uid": "1719430104206",
"name": "image.png",
"status": "done",
"size": 424869,
"type": "image/png",
"id": 394519014493130750,
"url": "https://rumiapp.s3.us-west-1.amazonaws.com/public%2Fimages%2F394519008180703232.png"
}
]
}
通过上述步骤,您可以轻松实现一个文件上传和管理系统的数据库设计,并确保数据的完整性和一致性。
实体类
已经内置了 tio-boot-admin 中
package nexus.io.tio.boot.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class UploadResult {
private Long id;
private Long size;
private String name, targetName, url, md5;
public UploadResult(long id, String filename, Long size, String url, String md5) {
this.id = id;
this.name = filename;
this.size = size;
this.url = url;
this.md5 = md5;
}
public UploadResult(long id, String filename, String url, String md5) {
this.id = id;
this.name = filename;
this.url = url;
this.md5 = md5;
}
public UploadResult(long id, String filename, String targetName, String url, String md5) {
this.id = id;
this.name = filename;
this.targetName = targetName;
this.url = url;
this.md5 = md5;
}
}
数据操作层
package nexus.io.tio.boot.admin.dao;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
public class SystemUploadFileDao {
public static final String tableName = "tio_boot_admin_system_upload_file";
public static final String getFileBasicInfoByMd5Sql = String.format(
//
"select id,name,size,bucket_name,target_name from %s where md5=? and deleted=0", tableName);
public static final String getFileBasicInfoByIdSql = String.format(
//
"select md5,name,size,bucket_name,target_name from %s where id=? and deleted=0", tableName);
public Row getFileBasicInfoByMd5(String md5) {
return Db.findFirst(getFileBasicInfoByMd5Sql, md5);
}
public Row getFileBasicInfoById(long id) {
return Db.findFirst(getFileBasicInfoByIdSql, id);
}
public boolean save(long id, String md5, String originname, int fileSize, String platform, String bucketName,
//
String targetName) {
Row row = Row.by("id", id)
//
.set("md5", md5).set("name", originname).set("size", fileSize)
//
.set("platform", platform).set("bucket_name", bucketName)
//
.set("target_name", targetName);
return Db.save(tableName, row);
}
}
业务操作层
package nexus.io.tio.boot.admin.services;
import com.jfinal.kit.Kv;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.admin.dao.SystemUploadFileDao;
import nexus.io.tio.boot.admin.utils.AwsS3Utils;
import nexus.io.model.upload.UploadResult;
public class SystemUploadFileService {
public UploadResult getUrlById(String id) {
return getUrlById(Long.parseLong(id));
}
public UploadResult getUrlById(long id) {
Row row = Aop.get(SystemUploadFileDao.class).getFileBasicInfoById(id);
if (row == null) {
return null;
}
String url = this.getUrl(row.getStr("bucket_name"), row.getStr("target_name"));
String originFilename = row.getStr("fielename");
String md5 = row.getStr("md5");
Long size = row.getLong("size");
return new UploadResult(id, originFilename, size, url, md5);
}
public UploadResult getUrlByMd5(String md5) {
Row row = Aop.get(SystemUploadFileDao.class).getFileBasicInfoByMd5(md5);
if (row == null) {
return null;
}
Long id = row.getLong("id");
String url = this.getUrl(row.getStr("bucket_name"), row.getStr("target_name"));
Kv kv = row.toKv();
kv.set("url", url);
kv.set("md5", md5);
String originFilename = row.getStr("name");
Long size = row.getLong("size");
return new UploadResult(id, originFilename, size, url, md5);
}
public String getUrl(String bucketName, String targetName) {
return String.format(AwsS3Utils.urlFormat, AwsS3Utils.bucketName, targetName);
}
}
业务层接口
package nexus.io.tio.boot.admin.services;
import nexus.io.model.body.RespBodyVo;
import nexus.io.model.upload.UploadResult;
import nexus.io.model.upload.UploadFile;
public interface StorageService {
public RespBodyVo upload(String category, UploadFile uploadFile);
public UploadResult uploadBytes(String category, String originname, int size, byte[] fileContent);
public UploadResult uploadBytes(long id, String originname, String targetName, byte[] fileContent,
//
int size, String suffix);
public String getUrl(String bucketName, String targetName);
public UploadResult getUrlById(String id);
public UploadResult getUrlById(long id);
public UploadResult getUrlByMd5(String md5);
}
