Skip to content

文件上传数据表 ​

在这个指南中,我们将详细介绍如何设计和实现一个文件上传和管理系统的数据库

1. 文件表设计 ​

首先,我们需要设计一个表来存储上传文件的相关信息。以下是一个示例的 SQL 表结构:

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");

字段容量设计 ​

字段类型设计依据
nameVARCHAR(255)保存原始文件名,兼容较长中文名称;不包含本地目录
target_nameVARCHAR(1024)保存对象路径,允许分类、编号和后缀,不复用原名的短字段容量
bucket_nameVARCHAR(255)为不同存储平台的桶名称预留空间;平台本身的命名规则单独验证
file_idVARCHAR(255)保存平台返回的文件标识或 ETag,不假定所有平台都是 32/64 位
region_nameVARCHAR(64)容纳区域标识
md5VARCHAR(32)保存 MD5 的 32 位十六进制文本
sizeBIGINT文件内容字节数,避免使用 int 限制大文件

platform、用户及审计字段保持原有容量和类型,保留 tags JSON,避免本次字段扩容引入额外数据迁移。应用应确保 size 非负;是否接受空文件由上传接口决定。

PostgreSQL 的 VARCHAR 长度以字符计数,云平台对象路径可能按 UTF-8 字节计数,二者不能等同。统一上传示例把分类限制为 128 位 ASCII 字母、数字、下划线或短横线,后缀使用字母数字,路径由“分类/雪花 ID.后缀”组成;即使文件名达到 255 位,生成的对象路径仍不超过 404 字节。业务若改为使用中文目录或原名作为对象路径,应增加所选平台的字节长度校验。

文件原名可以包含中文;Java 示例使用 String.length() 控制 255 个 UTF-16 编码单元,包含补充字符(例如部分 emoji)时,比数据库字符长度上限更保守。分类是应用参数,不新增分类列,最终路径保存到 target_name。

已有表的扩容 SQL ​

手动执行以下迁移,再部署使用新长度的代码。迁移只扩大字段,不删除表、不重建数据、不改动业务关联。字段扩容会获取表锁,请选择适合的执行时机。不要在应用启动时自动执行。

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;

插入示例数据:

sql
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. 业务表设计 ​

业务表用于存储与业务相关的信息,同时记录与文件关联的数据。以下是业务表的示例:

sql
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) -- 主键
);

文件字段的示例数据如下:

json
[
  {
    "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 接口判断对应的文件是否存在。如果文件存在,返回如下数据:

http
GET /api/system/file/s3/url?md5=c9fa90c0e145f2d62560f5431b271326

响应示例:

json
{
  "code": 1,
  "data": {
    "id": 394519014493130752,
    "url": "https://rumiapp.s3.us-west-1.amazonaws.com/public%2Fimages%2F394519008180703232.png"
  },
  "ok": true
}

2. 上传文件 ​

如果文件不存在,前端调用上传接口上传文件,后端同时将文件信息存入文件表。上传完成后,返回包含文件信息的 id 和 url。

3. 提交业务数据 ​

前端在完成业务数据录入后,将包含文件字段的数据提交到后端。请求示例如下:

http
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 中

java
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;
  }
}

数据操作层 ​

java
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);
  }
}

业务操作层 ​

java
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);
  }
}

业务层接口 ​

java
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);

}

MIT Licensed