初始化

This commit is contained in:
李琦
2026-06-26 14:31:24 +08:00
commit 7049d7b1a5
20 changed files with 1917 additions and 0 deletions

153
handlers/cell.go Normal file
View File

@@ -0,0 +1,153 @@
// Package handlers 实现单元格相关的API请求处理器
package handlers
import (
"excel-api/models"
"excel-api/utils"
"github.com/gin-gonic/gin"
)
// CellRequest 单元格更新请求结构体
type CellRequest struct {
Row int `json:"row" binding:"required"` // 行号0-based必填
Col int `json:"col" binding:"required"` // 列号0-based必填
CellType string `json:"cellType"` // 单元格类型
Value string `json:"value"` // 文本值
Formula string `json:"formula"` // 公式表达式
FileURL string `json:"fileUrl"` // 文件URL
FileName string `json:"fileName"` // 文件名
FileSize int64 `json:"fileSize"` // 文件大小
MimeType string `json:"mimeType"` // MIME类型
}
// GetCells 获取工作表的单元格数据
// 请求方式: GET
// 路径: /api/sheets/:id/cells
// 路径参数: id - 工作表ID
// 查询参数: row(起始行), col(起始列), endRow(结束行), endCol(结束列)
func GetCells(c *gin.Context) {
id := c.Param("id")
// 验证工作表是否存在
var sheet models.Sheet
if result := models.DB.First(&sheet, id); result.Error != nil {
utils.Error(c, 404, "工作表不存在")
return
}
// 获取查询参数(可选范围查询)
query := models.DB.Where("sheet_id = ?", id)
// 如果提供了范围参数,则按范围过滤
if row := c.Query("row"); row != "" {
query = query.Where("row >= ?", row)
}
if col := c.Query("col"); col != "" {
query = query.Where("col >= ?", col)
}
if endRow := c.Query("endRow"); endRow != "" {
query = query.Where("row <= ?", endRow)
}
if endCol := c.Query("endCol"); endCol != "" {
query = query.Where("col <= ?", endCol)
}
var cells []models.Cell
result := query.Find(&cells)
if result.Error != nil {
utils.Error(c, 500, "查询单元格数据失败")
return
}
utils.Success(c, cells)
}
// BatchUpdateCells 批量更新单元格数据
// 请求方式: POST
// 路径: /api/sheets/:id/cells
// 路径参数: id - 工作表ID
// 请求体: { "cells": [{ "row": 0, "col": 0, "value": "Hello" }, ...] }
func BatchUpdateCells(c *gin.Context) {
id := c.Param("id")
// 验证工作表是否存在
var sheet models.Sheet
if result := models.DB.First(&sheet, id); result.Error != nil {
utils.Error(c, 404, "工作表不存在")
return
}
var req struct {
Cells []CellRequest `json:"cells" binding:"required"` // 单元格更新列表,必填
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误: "+err.Error())
return
}
// 使用事务批量更新单元格
tx := models.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
for _, cellReq := range req.Cells {
// 查找已存在的单元格
var cell models.Cell
result := tx.Where("sheet_id = ? AND row = ? AND col = ?", id, cellReq.Row, cellReq.Col).First(&cell)
if result.Error != nil {
// 单元格不存在,创建新记录
cell = models.Cell{
SheetID: sheet.ID,
Row: cellReq.Row,
Col: cellReq.Col,
CellType: cellReq.CellType,
Value: cellReq.Value,
Formula: cellReq.Formula,
FileURL: cellReq.FileURL,
FileName: cellReq.FileName,
FileSize: cellReq.FileSize,
MimeType: cellReq.MimeType,
}
// 设置默认类型
if cell.CellType == "" {
cell.CellType = "text"
}
if err := tx.Create(&cell).Error; err != nil {
tx.Rollback()
utils.Error(c, 500, "创建单元格失败")
return
}
} else {
// 单元格已存在,更新字段
if cellReq.CellType != "" {
cell.CellType = cellReq.CellType
}
cell.Value = cellReq.Value
cell.Formula = cellReq.Formula
cell.FileURL = cellReq.FileURL
cell.FileName = cellReq.FileName
cell.FileSize = cellReq.FileSize
cell.MimeType = cellReq.MimeType
if err := tx.Save(&cell).Error; err != nil {
tx.Rollback()
utils.Error(c, 500, "更新单元格失败")
return
}
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
utils.Error(c, 500, "提交事务失败")
return
}
utils.SuccessWithMessage(c, "批量更新单元格成功", nil)
}

174
handlers/sheet.go Normal file
View File

@@ -0,0 +1,174 @@
// Package handlers 实现工作表相关的API请求处理器
package handlers
import (
"excel-api/models"
"excel-api/utils"
"strconv"
"github.com/gin-gonic/gin"
)
// GetSheets 获取工作簿下的工作表列表
// 请求方式: GET
// 路径: /api/workbooks/:wid/sheets
// 路径参数: wid - 工作簿ID
func GetSheets(c *gin.Context) {
wid := c.Param("wid")
// 验证工作簿是否存在
var workbook models.Workbook
if result := models.DB.First(&workbook, wid); result.Error != nil {
utils.Error(c, 404, "工作簿不存在")
return
}
var sheets []models.Sheet
// 查询该工作簿下的所有工作表,按索引排序
result := models.DB.Where("workbook_id = ?", wid).Order("index ASC").Find(&sheets)
if result.Error != nil {
utils.Error(c, 500, "查询工作表列表失败")
return
}
utils.Success(c, sheets)
}
// CreateSheet 在工作簿下创建新工作表
// 请求方式: POST
// 路径: /api/workbooks/:wid/sheets
// 路径参数: wid - 工作簿ID
// 请求体: { "name": "工作表名称" }
func CreateSheet(c *gin.Context) {
wid := c.Param("wid")
// 验证工作簿是否存在
var workbook models.Workbook
if result := models.DB.First(&workbook, wid); result.Error != nil {
utils.Error(c, 404, "工作簿不存在")
return
}
var req struct {
Name string `json:"name" binding:"required"` // 工作表名称,必填
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误: "+err.Error())
return
}
// 获取当前工作簿下最大的排序索引
var maxIndex int
models.DB.Model(&models.Sheet{}).Where("workbook_id = ?", wid).Select("COALESCE(MAX(index), -1)").Scan(&maxIndex)
widNum, _ := strconv.Atoi(wid)
sheet := models.Sheet{
WorkbookID: uint(widNum),
Name: req.Name,
Index: maxIndex + 1,
ColCount: 26,
RowCount: 100,
}
result := models.DB.Create(&sheet)
if result.Error != nil {
utils.Error(c, 500, "创建工作表失败")
return
}
utils.SuccessWithMessage(c, "创建工作表成功", sheet)
}
// GetSheet 获取工作表详情
// 请求方式: GET
// 路径: /api/sheets/:id
// 路径参数: id - 工作表ID
func GetSheet(c *gin.Context) {
id := c.Param("id")
var sheet models.Sheet
result := models.DB.First(&sheet, id)
if result.Error != nil {
utils.Error(c, 404, "工作表不存在")
return
}
utils.Success(c, sheet)
}
// UpdateSheet 更新工作表信息
// 请求方式: PUT
// 路径: /api/sheets/:id
// 路径参数: id - 工作表ID
// 请求体: { "name": "新名称", "index": 0, "colCount": 26, "rowCount": 100 }
func UpdateSheet(c *gin.Context) {
id := c.Param("id")
var sheet models.Sheet
result := models.DB.First(&sheet, id)
if result.Error != nil {
utils.Error(c, 404, "工作表不存在")
return
}
var req struct {
Name string `json:"name"` // 工作表名称
Index int `json:"index"` // 排序索引
ColCount int `json:"colCount"` // 列数量
RowCount int `json:"rowCount"` // 行数量
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误: "+err.Error())
return
}
if req.Name != "" {
sheet.Name = req.Name
}
if req.Index >= 0 {
sheet.Index = req.Index
}
if req.ColCount > 0 {
sheet.ColCount = req.ColCount
}
if req.RowCount > 0 {
sheet.RowCount = req.RowCount
}
result = models.DB.Save(&sheet)
if result.Error != nil {
utils.Error(c, 500, "更新工作表失败")
return
}
utils.SuccessWithMessage(c, "更新工作表成功", sheet)
}
// DeleteSheet 删除工作表(软删除)
// 请求方式: DELETE
// 路径: /api/sheets/:id
// 路径参数: id - 工作表ID
func DeleteSheet(c *gin.Context) {
id := c.Param("id")
var sheet models.Sheet
result := models.DB.First(&sheet, id)
if result.Error != nil {
utils.Error(c, 404, "工作表不存在")
return
}
// 软删除工作表及其关联的单元格
result = models.DB.Delete(&sheet)
if result.Error != nil {
utils.Error(c, 500, "删除工作表失败")
return
}
// 同时删除关联的单元格
models.DB.Where("sheet_id = ?", id).Delete(&models.Cell{})
utils.SuccessWithMessage(c, "删除工作表成功", nil)
}

130
handlers/upload.go Normal file
View File

@@ -0,0 +1,130 @@
// Package handlers 实现文件上传相关的API请求处理器
// 支持图片、视频、音频、文档等文件上传
package handlers
import (
"excel-api/config"
"excel-api/utils"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// allowedExtensions 允许上传的文件扩展名白名单
var allowedExtensions = map[string]bool{
// 图片格式
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".svg": true,
// 视频格式
".mp4": true, ".webm": true, ".mov": true,
// 音频格式
".mp3": true, ".wav": true, ".ogg": true, ".flac": true,
// 压缩包格式
".zip": true, ".rar": true, ".7z": true, ".tar": true, ".gz": true,
// 文档格式
".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
".ppt": true, ".pptx": true, ".txt": true, ".csv": true,
}
// DetectFileType 根据文件扩展名自动检测文件类型
// 返回值: image/video/audio/archive/file
func DetectFileType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg":
return "image"
case ".mp4", ".webm", ".mov":
return "video"
case ".mp3", ".wav", ".ogg", ".flac":
return "audio"
case ".zip", ".rar", ".7z", ".tar", ".gz":
return "archive"
default:
return "file"
}
}
// UploadResponse 文件上传响应结构体
type UploadResponse struct {
URL string `json:"url"` // 文件访问URL
Name string `json:"name"` // 原始文件名
Size int64 `json:"size"` // 文件大小(字节)
MimeType string `json:"mimeType"` // 文件MIME类型
FileType string `json:"fileType"` // 自动检测的文件类型
}
// Upload 处理文件上传请求
// 请求方式: POST
// 路径: /api/upload
// 请求体: multipart/form-data字段名为 "file"
func Upload(c *gin.Context) {
// 获取上传的文件
file, err := c.FormFile("file")
if err != nil {
utils.Error(c, 400, "请选择要上传的文件")
return
}
// 验证文件扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedExtensions[ext] {
utils.Error(c, 400, "不支持的文件格式: "+ext)
return
}
// 限制文件大小(最大 100MB
const maxFileSize = 100 * 1024 * 1024
if file.Size > maxFileSize {
utils.Error(c, 400, "文件大小超过限制最大100MB")
return
}
// 确定存储路径
storagePath := config.App.Storage.Local.Path
if storagePath == "" {
storagePath = "./uploads"
}
// 按日期创建子目录uploads/2024/01/
dateDir := time.Now().Format("2006/01")
uploadDir := filepath.Join(storagePath, dateDir)
// 创建存储目录(如果不存在)
if err := os.MkdirAll(uploadDir, 0755); err != nil {
utils.Error(c, 500, "创建上传目录失败")
return
}
// 生成唯一文件名:时间戳 + 随机数 + 原始扩展名
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
filePath := filepath.Join(uploadDir, filename)
// 保存文件到磁盘
if err := c.SaveUploadedFile(file, filePath); err != nil {
utils.Error(c, 500, "保存文件失败")
return
}
// 检测文件MIME类型
mimeType := file.Header.Get("Content-Type")
if mimeType == "" {
mimeType = "application/octet-stream"
}
// 自动检测文件类型
fileType := DetectFileType(file.Filename)
// 返回文件访问URL相对路径
url := fmt.Sprintf("/uploads/%s/%s", dateDir, filename)
utils.Success(c, UploadResponse{
URL: url,
Name: file.Filename,
Size: file.Size,
MimeType: mimeType,
FileType: fileType,
})
}

205
handlers/workbook.go Normal file
View File

@@ -0,0 +1,205 @@
// Package handlers 实现API请求处理器
// 包含工作簿Excel文档的增删改查操作
// 工作簿通过 documents 表存储元数据workbooks 表存储Excel特化数据
package handlers
import (
"excel-api/models"
"excel-api/utils"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// GetWorkbooks 获取工作簿列表
// 支持分页查询通过JOIN关联文档表获取完整信息
// 请求方式: GET
// 路径: /api/workbooks
// 查询参数: page(页码默认1), pageSize(每页条数默认20)
func GetWorkbooks(c *gin.Context) {
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
var workbooks []models.Workbook
var total int64
// 查询总数(关联文档表)
models.DB.Model(&models.Workbook{}).Joins("JOIN documents ON documents.id = workbooks.document_id").Count(&total)
// 分页查询工作簿列表,预加载关联的工作表
result := models.DB.
Joins("JOIN documents ON documents.id = workbooks.document_id").
Preload("Sheets").
Offset((page - 1) * pageSize).Limit(pageSize).
Order("documents.created_at DESC").
Find(&workbooks)
if result.Error != nil {
utils.Error(c, 500, "查询工作簿列表失败")
return
}
utils.SuccessPage(c, workbooks, total, page, pageSize)
}
// CreateWorkbook 创建新工作簿
// 同时创建对应的文档记录和默认工作表
// 请求方式: POST
// 路径: /api/workbooks
// 请求体: { "title": "文档标题", "description": "描述" }
func CreateWorkbook(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"` // 文档标题,必填
Description string `json:"description"` // 文档描述,可选
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误: "+err.Error())
return
}
// 创建文档记录excel类型
document := models.Document{
Type: "excel",
Title: req.Title,
Description: req.Description,
CreatorID: 1, // 前期默认用户ID
Status: 1,
}
result := models.DB.Create(&document)
if result.Error != nil {
utils.Error(c, 500, "创建文档失败")
return
}
// 创建工作簿记录
workbook := models.Workbook{
DocumentID: document.ID,
}
result = models.DB.Create(&workbook)
if result.Error != nil {
utils.Error(c, 500, "创建工作簿失败")
return
}
// 为新工作簿创建默认工作表
defaultSheet := models.Sheet{
WorkbookID: workbook.ID,
Name: "Sheet1",
Index: 0,
ColCount: 26,
RowCount: 100,
}
models.DB.Create(&defaultSheet)
// 返回完整信息
workbook.Document = document
utils.SuccessWithMessage(c, "创建工作簿成功", workbook)
}
// GetWorkbook 获取工作簿详情
// 请求方式: GET
// 路径: /api/workbooks/:id
// 路径参数: id - 工作簿ID
func GetWorkbook(c *gin.Context) {
id := c.Param("id")
var workbook models.Workbook
// 查询工作簿并预加载关联的工作表和文档
result := models.DB.Preload("Sheets").Preload("Document").First(&workbook, id)
if result.Error != nil {
utils.Error(c, 404, "工作簿不存在")
return
}
utils.Success(c, workbook)
}
// UpdateWorkbook 更新工作簿信息
// 更新关联的文档表信息
// 请求方式: PUT
// 路径: /api/workbooks/:id
// 路径参数: id - 工作簿ID
// 请求体: { "title": "新标题", "description": "新描述" }
func UpdateWorkbook(c *gin.Context) {
id := c.Param("id")
var workbook models.Workbook
// 查找要更新的工作簿
result := models.DB.First(&workbook, id)
if result.Error != nil {
utils.Error(c, 404, "工作簿不存在")
return
}
var req struct {
Title string `json:"title"` // 文档标题
Description string `json:"description"` // 文档描述
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误: "+err.Error())
return
}
// 更新关联的文档表
var document models.Document
if err := models.DB.First(&document, workbook.DocumentID).Error; err == nil {
if req.Title != "" {
document.Title = req.Title
}
document.Description = req.Description
models.DB.Save(&document)
}
utils.SuccessWithMessage(c, "更新工作簿成功", workbook)
}
// DeleteWorkbook 删除工作簿(软删除)
// 同时删除关联的文档、工作表和单元格
// 请求方式: DELETE
// 路径: /api/workbooks/:id
// 路径参数: id - 工作簿ID
func DeleteWorkbook(c *gin.Context) {
id := c.Param("id")
var workbook models.Workbook
// 查找要删除的工作簿
result := models.DB.First(&workbook, id)
if result.Error != nil {
utils.Error(c, 404, "工作簿不存在")
return
}
// 获取关联的工作表IDs
var sheetIDs []uint
models.DB.Model(&models.Sheet{}).Where("workbook_id = ?", id).Pluck("id", &sheetIDs)
// 软删除单元格
if len(sheetIDs) > 0 {
models.DB.Where("sheet_id IN ?", sheetIDs).Delete(&models.Cell{})
}
// 软删除工作表
models.DB.Where("workbook_id = ?", id).Delete(&models.Sheet{})
// 软删除工作簿
models.DB.Delete(&workbook)
// 软删除关联的文档
models.DB.Delete(&models.Document{}, workbook.DocumentID)
utils.SuccessWithMessage(c, "删除工作簿成功", nil)
}
// HandleOptions 处理OPTIONS预检请求
// 用于CORS跨域请求的预检
func HandleOptions(c *gin.Context) {
c.Status(http.StatusNoContent)
}