初始化

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

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

9
.idea/excel-api.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

11
.idea/go.imports.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/excel-api.iml" filepath="$PROJECT_DIR$/.idea/excel-api.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

42
config.yaml Normal file
View File

@@ -0,0 +1,42 @@
# 应用配置文件
# 本文件用于配置数据库连接、文件存储、服务器端口等
# 服务器配置
server:
port: 16702 # 后端API服务端口与前端Vite代理配置保持一致
ws_port: 16703 # WebSocket服务端口独立端口用于在线协作
# 数据库配置MySQL 8+
database:
host: "127.0.0.1" # 数据库主机地址
port: 3306 # 数据库端口
username: "root" # 数据库用户名
password: "123456" # 数据库密码
dbname: "excel_t" # 数据库名称
charset: "utf8mb4" # 字符集
# 文件存储配置
storage:
# 存储类型local本地磁盘/ qiniu七牛云/ tencent腾讯云/ aliyun阿里云
type: "local"
# 本地存储配置
local:
path: "./uploads" # 本地文件存储目录
# 七牛云配置storage.type 为 qiniu 时生效)
qiniu:
access_key: ""
secret_key: ""
bucket: ""
domain: ""
# 腾讯云配置storage.type 为 tencent 时生效)
tencent:
secret_id: ""
secret_key: ""
bucket: ""
region: ""
# 阿里云配置storage.type 为 aliyun 时生效)
aliyun:
access_key_id: ""
access_key_secret: ""
bucket: ""
endpoint: ""

93
config/config.go Normal file
View File

@@ -0,0 +1,93 @@
// Package config 负责加载和管理应用程序配置
// 从 config.yaml 文件中读取配置,并提供全局访问接口
package config
import (
"os"
"gopkg.in/yaml.v2"
)
// Config 应用程序全局配置结构体
type Config struct {
Server ServerConfig `yaml:"server"` // 服务器配置
Database DatabaseConfig `yaml:"database"` // 数据库配置
Storage StorageConfig `yaml:"storage"` // 文件存储配置
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port int `yaml:"port"` // API服务监听端口
WsPort int `yaml:"ws_port"` // WebSocket服务监听端口
}
// DatabaseConfig 数据库连接配置
type DatabaseConfig struct {
Host string `yaml:"host"` // 数据库主机地址
Port int `yaml:"port"` // 数据库端口
Username string `yaml:"username"` // 数据库用户名
Password string `yaml:"password"` // 数据库密码
DBName string `yaml:"dbname"` // 数据库名称
Charset string `yaml:"charset"` // 字符集
}
// StorageConfig 文件存储配置
type StorageConfig struct {
Type string `yaml:"type"` // 存储类型local/qiniu/tencent/aliyun
Local LocalConfig `yaml:"local"` // 本地存储配置
Qiniu CloudConfig `yaml:"qiniu"` // 七牛云配置
Tencent CloudConfig `yaml:"tencent"` // 腾讯云配置
Aliyun CloudConfig `yaml:"aliyun"` // 阿里云配置
}
// LocalConfig 本地存储配置
type LocalConfig struct {
Path string `yaml:"path"` // 本地文件存储目录
}
// CloudConfig 云存储通用配置
type CloudConfig struct {
AccessKey string `yaml:"access_key"` // 访问密钥ID
SecretKey string `yaml:"secret_key"` // 访问密钥Secret
Bucket string `yaml:"bucket"` // 存储桶名称
Domain string `yaml:"domain"` // 域名(七牛云)
Region string `yaml:"region"` // 区域(腾讯云)
SecretID string `yaml:"secret_id"` // SecretID腾讯云
AccessKeyID string `yaml:"access_key_id"` // AccessKeyID阿里云
AccessKeySecret string `yaml:"access_key_secret"` // AccessKeySecret阿里云
Endpoint string `yaml:"endpoint"` // Endpoint阿里云
}
// App 全局配置实例
var App *Config
// Init 从 config.yaml 文件加载配置到全局变量 App
// 如果文件不存在或读取失败,返回错误
func Init() error {
App = &Config{}
data, err := os.ReadFile("config.yaml")
if err != nil {
return err
}
err = yaml.Unmarshal(data, App)
if err != nil {
return err
}
// 设置默认值
if App.Server.Port == 0 {
App.Server.Port = 16702
}
if App.Server.WsPort == 0 {
App.Server.WsPort = 16703
}
if App.Database.Charset == "" {
App.Database.Charset = "utf8mb4"
}
if App.Storage.Type == "" {
App.Storage.Type = "local"
}
if App.Storage.Local.Path == "" {
App.Storage.Local.Path = "./uploads"
}
return nil
}

43
go.mod Normal file
View File

@@ -0,0 +1,43 @@
module excel-api
go 1.21
require (
github.com/gin-gonic/gin v1.10.0
github.com/gorilla/websocket v1.5.3
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

104
go.sum Normal file
View File

@@ -0,0 +1,104 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

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

52
main.go Normal file
View File

@@ -0,0 +1,52 @@
// Package main 应用程序入口
// 初始化配置、数据库、WebSocket Hub启动HTTP服务
// API服务运行在16702端口WebSocket服务运行在16703端口
package main
import (
"fmt"
"log"
"net/http"
"excel-api/config"
"excel-api/models"
"excel-api/router"
ws "excel-api/websocket"
)
func main() {
// 第一步:加载配置文件
if err := config.Init(); err != nil {
log.Fatalf("加载配置文件失败: %v", err)
}
log.Println("配置文件加载成功")
// 第二步:初始化数据库连接
if err := models.InitDB(); err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
// 第三步创建并启动WebSocket Hub
hub := ws.NewHub()
go hub.Run()
log.Println("WebSocket Hub 已启动")
// 第四步启动WebSocket独立服务端口16703
wsAddr := fmt.Sprintf(":%d", config.App.Server.WsPort)
wsRouter := http.NewServeMux()
wsRouter.HandleFunc("/ws", ws.HandleWebSocketHTTP(hub))
go func() {
log.Printf("WebSocket服务启动监听端口 %s", wsAddr)
if err := http.ListenAndServe(wsAddr, wsRouter); err != nil {
log.Printf("WebSocket服务启动失败: %v", err)
}
}()
// 第五步配置路由并启动API HTTP服务端口16702
r := router.Setup(hub)
apiAddr := fmt.Sprintf(":%d", config.App.Server.Port)
log.Printf("API服务启动监听端口 %s", apiAddr)
if err := r.Run(apiAddr); err != nil {
log.Fatalf("API服务启动失败: %v", err)
}
}

83
models/db.go Normal file
View File

@@ -0,0 +1,83 @@
// Package models 提供数据库初始化和连接管理功能
package models
import (
"fmt"
"log"
"excel-api/config"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// DB 全局数据库连接实例
var DB *gorm.DB
// InitDB 初始化数据库连接
// 读取配置文件中的数据库连接信息建立MySQL连接并自动迁移表结构
func InitDB() error {
cfg := config.App.Database
// 构建MySQL DSNData Source Name连接字符串
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.Username,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.DBName,
cfg.Charset,
)
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return fmt.Errorf("数据库连接失败: %w", err)
}
// 获取底层 *sql.DB 用于配置连接池
sqlDB, err := DB.DB()
if err != nil {
return fmt.Errorf("获取数据库连接池失败: %w", err)
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
// 自动迁移:根据模型结构体创建/更新数据库表
// 注意AutoMigrate 仅创建表和添加缺失列,不会删除已有列
// 完整的表结构变更请参照根目录 init.sql 文件
err = DB.AutoMigrate(
&User{},
&Role{},
&Permission{},
&RolePermission{},
&UserRole{},
&Document{},
&Workbook{},
&Sheet{},
&Cell{},
&DocumentRole{},
&UserDocument{},
&WorkbookPermission{},
&OperationLog{},
&ShareLink{},
&Comment{},
&Notification{},
&Tag{},
&DocumentTag{},
&Folder{},
&FileUpload{},
&Version{},
&CollaborationSession{},
&ExportHistory{},
&Setting{},
&ApiKey{},
&AuditLog{},
)
if err != nil {
return fmt.Errorf("数据库迁移失败: %w", err)
}
log.Println("数据库连接成功,表结构迁移完成")
return nil
}

449
models/model.go Normal file
View File

@@ -0,0 +1,449 @@
// Package models 定义数据库模型
// 包含用户、角色、权限、文档、工作簿、工作表、单元格等核心数据模型
// 所有模型结构必须与根目录 init.sql 保持一致
package models
import (
"time"
"gorm.io/gorm"
)
// ============================================================
// 用户相关模型
// ============================================================
// User 用户模型
// 存储系统用户信息,前期不强制登录
type User struct {
ID uint `json:"id" gorm:"primaryKey"` // 用户ID主键自增
Username string `json:"username" gorm:"size:64;uniqueIndex"` // 用户名(登录名)
Password string `json:"-" gorm:"size:255"` // 密码哈希JSON不输出
Nickname string `json:"nickname" gorm:"size:64"` // 昵称/显示名称
Email string `json:"email" gorm:"size:128"` // 邮箱
Phone string `json:"phone" gorm:"size:20"` // 手机号
Avatar string `json:"avatar" gorm:"size:512"` // 头像URL
Status int `json:"status" gorm:"default:1"` // 状态: 0=禁用 1=正常
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
Roles []Role `json:"roles,omitempty" gorm:"many2many:user_roles"` // 关联角色列表
}
// Role 角色模型
// 系统角色定义,区分前台角色和后台角色
type Role struct {
ID uint `json:"id" gorm:"primaryKey"` // 角色ID主键自增
Name string `json:"name" gorm:"size:64;not null"` // 角色名称
Code string `json:"code" gorm:"size:64;uniqueIndex"` // 角色编码(唯一标识)
Type string `json:"type" gorm:"size:16;default:'frontend'"` // 角色类型: frontend=前台 backend=后台
Description string `json:"description" gorm:"size:255"` // 角色描述
SortOrder int `json:"sortOrder" gorm:"default:0"` // 排序序号
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
Permissions []Permission `json:"permissions,omitempty" gorm:"many2many:role_permissions"` // 关联权限列表
}
// Permission 权限模型
// 系统权限定义,区分前台权限和后台权限
type Permission struct {
ID uint `json:"id" gorm:"primaryKey"` // 权限ID主键自增
Name string `json:"name" gorm:"size:64;not null"` // 权限名称
Code string `json:"code" gorm:"size:128;uniqueIndex"` // 权限编码(如 workbook:create
Type string `json:"type" gorm:"size:16;default:'frontend'"` // 权限类型: frontend=前台 backend=后台
Path string `json:"path" gorm:"size:255"` // 关联的API路径
Method string `json:"method" gorm:"size:10"` // HTTP方法: GET/POST/PUT/DELETE
Description string `json:"description" gorm:"size:255"` // 权限描述
SortOrder int `json:"sortOrder" gorm:"default:0"` // 排序序号
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// RolePermission 角色-权限关联模型
type RolePermission struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
RoleID uint `json:"roleId" gorm:"uniqueIndex:uk_role_permission"` // 角色ID
PermissionID uint `json:"permissionId" gorm:"uniqueIndex:uk_role_permission"` // 权限ID
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// UserRole 用户-角色关联模型
type UserRole struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
UserID uint `json:"userId" gorm:"uniqueIndex:uk_user_role"` // 用户ID
RoleID uint `json:"roleId" gorm:"uniqueIndex:uk_user_role"` // 角色ID
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ============================================================
// 文档相关模型
// ============================================================
// Document 文档模型
// 顶层文档表支持多种文档类型excel/word/pdf
type Document struct {
ID uint `json:"id" gorm:"primaryKey"` // 文档ID主键自增
Type string `json:"type" gorm:"size:16;not null;index"` // 文档类型: excel/word/pdf
Title string `json:"title" gorm:"size:255;not null"` // 文档标题
Description string `json:"description" gorm:"size:500"` // 文档描述
CreatorID uint `json:"creatorId" gorm:"index;not null"` // 创建者用户ID
Cover string `json:"cover" gorm:"size:512"` // 封面图URL
FileSize int64 `json:"fileSize" gorm:"default:0"` // 文件大小(字节)
SortOrder int `json:"sortOrder" gorm:"default:0"` // 排序序号
Status int `json:"status" gorm:"default:1"` // 状态: 0=草稿 1=正常 2=归档
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// Workbook 工作簿模型
// Excel文档的工作簿数据通过 document_id 关联文档表
type Workbook struct {
ID uint `json:"id" gorm:"primaryKey"` // 工作簿ID主键自增
DocumentID uint `json:"documentId" gorm:"uniqueIndex;not null"` // 关联文档ID
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
Document Document `json:"document,omitempty" gorm:"foreignKey:DocumentID"` // 关联的文档
Sheets []Sheet `json:"sheets,omitempty" gorm:"foreignKey:WorkbookID"` // 关联的工作表列表
}
// Sheet 工作表/数据表模型
// 每个工作表属于一个工作簿,拥有独立的列配置和布局
type Sheet struct {
ID uint `json:"id" gorm:"primaryKey"` // 工作表ID主键自增
WorkbookID uint `json:"workbookId" gorm:"index;not null"` // 所属工作簿ID
Name string `json:"name" gorm:"size:255;not null"` // 工作表名称
Index int `json:"index" gorm:"default:0"` // 工作表排序索引
ColCount int `json:"colCount" gorm:"default:26"` // 列数量
RowCount int `json:"rowCount" gorm:"default:100"` // 行数量
ColWidths string `json:"colWidths" gorm:"type:text"` // 列宽配置JSON字符串
RowHeights string `json:"rowHeights" gorm:"type:text"` // 行高配置JSON字符串
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// Cell 单元格模型
// 每个单元格属于一个工作表,通过行号和列号定位
type Cell struct {
ID uint `json:"id" gorm:"primaryKey"` // 单元格ID主键自增
SheetID uint `json:"sheetId" gorm:"index;not null"` // 所属工作表ID
Row int `json:"row" gorm:"index;not null"` // 行号0-based
Col int `json:"col" gorm:"index;not null"` // 列号0-based
CellType string `json:"cellType" gorm:"size:20;default:'text'"` // 单元格类型
Value string `json:"value" gorm:"type:text"` // 文本值或计算结果
Formula string `json:"formula" gorm:"type:text"` // 公式表达式仅formula类型
FileURL string `json:"fileUrl" gorm:"type:text"` // 文件URL
FileName string `json:"fileName" gorm:"size:255"` // 文件名
FileSize int64 `json:"fileSize" gorm:"default:0"` // 文件大小(字节)
MimeType string `json:"mimeType" gorm:"size:128"` // MIME类型
Style string `json:"style" gorm:"type:text"` // 单元格样式JSON字符串
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// ============================================================
// 权限相关模型
// ============================================================
// DocumentRole 文档角色模型
// 定义用户对特定文档的角色(创建者/编辑者/访问者)
type DocumentRole struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
DocumentID uint `json:"documentId" gorm:"uniqueIndex:uk_document_user"` // 文档ID
UserID uint `json:"userId" gorm:"uniqueIndex:uk_document_user"` // 用户ID
RoleType string `json:"roleType" gorm:"size:16;not null"` // 角色类型: creator/editor/viewer
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}
// UserDocument 用户-文档关联模型
// 记录用户访问/收藏/最近打开的文档关系
type UserDocument struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
UserID uint `json:"userId" gorm:"uniqueIndex:uk_user_document"` // 用户ID
DocumentID uint `json:"documentId" gorm:"uniqueIndex:uk_user_document"` // 文档ID
IsFavorite int `json:"isFavorite" gorm:"default:0"` // 是否收藏: 0=否 1=是
LastOpenAt *time.Time `json:"lastOpenAt"` // 最近打开时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// WorkbookPermission 工作簿权限模型
// 工作簿级别的细粒度权限控制
type WorkbookPermission struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
WorkbookID uint `json:"workbookId" gorm:"uniqueIndex:uk_workbook_user"` // 工作簿ID
UserID uint `json:"userId" gorm:"uniqueIndex:uk_workbook_user"` // 用户ID
CanCreate int `json:"canCreate" gorm:"default:0"` // 是否可创建工作表
CanEdit int `json:"canEdit" gorm:"default:0"` // 是否可编辑单元格
CanDelete int `json:"canDelete" gorm:"default:0"` // 是否可删除
CanShare int `json:"canShare" gorm:"default:0"` // 是否可分享
CanExport int `json:"canExport" gorm:"default:0"` // 是否可导出
CanPrint int `json:"canPrint" gorm:"default:0"` // 是否可打印
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}
// ============================================================
// 操作历史模型
// ============================================================
// OperationLog 编辑历史模型
// 记录用户在工作表上的所有操作,用于撤销/重做和操作审计
type OperationLog struct {
ID uint `json:"id" gorm:"primaryKey"` // 操作ID主键自增
SheetID uint `json:"sheetId" gorm:"index;not null"` // 工作表ID
UserID uint `json:"userId" gorm:"default:0"` // 操作用户ID0=系统操作)
Operation string `json:"operation" gorm:"size:32;not null"` // 操作类型
TargetType string `json:"targetType" gorm:"size:32;default:'cell'"` // 操作目标类型: cell/row/col/sheet
TargetRange string `json:"targetRange" gorm:"size:128"` // 操作目标范围(如 A1:B5
BeforeData string `json:"beforeData" gorm:"type:json"` // 操作前的数据快照JSON
AfterData string `json:"afterData" gorm:"type:json"` // 操作后的数据快照JSON
Description string `json:"description" gorm:"size:512"` // 操作描述(人类可读)
IPAddress string `json:"ipAddress" gorm:"size:64"` // 操作者IP地址
CreatedAt time.Time `json:"createdAt"` // 操作时间
}
// ============================================================
// 分享链接模型
// ============================================================
// ShareLink 分享链接模型
// 支持生成公开分享链接,可设置密码、有效期、访问次数
type ShareLink struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
DocumentID uint `json:"documentId" gorm:"index;not null"` // 文档ID
UserID uint `json:"userId" gorm:"index;not null"` // 创建者用户ID
Token string `json:"token" gorm:"size:64;uniqueIndex"` // 分享令牌(唯一标识)
Title string `json:"title" gorm:"size:255"` // 分享标题(可自定义)
Password string `json:"password" gorm:"size:255"` // 访问密码(加密存储)
Permission string `json:"permission" gorm:"size:16;default:'view'"` // 访问权限: view/edit
ExpireAt *time.Time `json:"expireAt"` // 过期时间NULL=永不过期)
MaxAccess int `json:"maxAccess" gorm:"default:0"` // 最大访问次数0=不限)
AccessCount int `json:"accessCount" gorm:"default:0"` // 已访问次数
IsActive int `json:"isActive" gorm:"default:1"` // 是否启用
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// ============================================================
// 评论模型
// ============================================================
// Comment 评论模型
// 支持单元格/文档级评论,支持回复和@提及
type Comment struct {
ID uint `json:"id" gorm:"primaryKey"` // 评论ID
DocumentID uint `json:"documentId" gorm:"index;not null"` // 文档ID
SheetID *uint `json:"sheetId" gorm:"index"` // 工作表IDNULL=文档级评论)
ParentID *uint `json:"parentId" gorm:"index"` // 父评论IDNULL=顶级评论)
UserID uint `json:"userId" gorm:"index;not null"` // 评论者用户ID
CellRange string `json:"cellRange" gorm:"size:128"` // 关联的单元格范围
Content string `json:"content" gorm:"type:text;not null"` // 评论内容
Mentions string `json:"mentions" gorm:"type:json"` // @提及的用户ID列表JSON
Status int `json:"status" gorm:"default:1"` // 状态: 0=已隐藏 1=正常 2=已解决
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// ============================================================
// 通知模型
// ============================================================
// Notification 通知模型
// 系统通知:文档分享、评论、协作邀请等
type Notification struct {
ID uint `json:"id" gorm:"primaryKey"` // 通知ID
UserID uint `json:"userId" gorm:"index;not null"` // 接收者用户ID
SenderID *uint `json:"senderId"` // 发送者用户IDNULL=系统通知)
Type string `json:"type" gorm:"size:32;not null"` // 通知类型
Title string `json:"title" gorm:"size:255;not null"` // 通知标题
Content string `json:"content" gorm:"type:text"` // 通知内容
TargetType string `json:"targetType" gorm:"size:32"` // 关联目标类型
TargetID *uint `json:"targetId"` // 关联目标ID
IsRead int `json:"isRead" gorm:"default:0"` // 是否已读
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ============================================================
// 标签模型
// ============================================================
// Tag 标签模型
type Tag struct {
ID uint `json:"id" gorm:"primaryKey"` // 标签ID
UserID uint `json:"userId" gorm:"not null"` // 创建者用户ID
Name string `json:"name" gorm:"size:64;not null"` // 标签名称
Color string `json:"color" gorm:"size:16;default:'#1890ff'"` // 标签颜色
SortOrder int `json:"sortOrder" gorm:"default:0"` // 排序序号
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// DocumentTag 文档-标签关联模型
type DocumentTag struct {
ID uint `json:"id" gorm:"primaryKey"` // 主键ID
DocumentID uint `json:"documentId" gorm:"uniqueIndex:uk_document_tag"` // 文档ID
TagID uint `json:"tagId" gorm:"uniqueIndex:uk_document_tag"` // 标签ID
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ============================================================
// 文件夹模型
// ============================================================
// Folder 文件夹模型
// 支持多层级文件夹组织结构
type Folder struct {
ID uint `json:"id" gorm:"primaryKey"` // 文件夹ID
ParentID *uint `json:"parentId"` // 父文件夹IDNULL=根目录)
UserID uint `json:"userId" gorm:"index;not null"` // 创建者用户ID
Name string `json:"name" gorm:"size:255;not null"` // 文件夹名称
Icon string `json:"icon" gorm:"size:64;default:'folder'"` // 图标名称
SortOrder int `json:"sortOrder" gorm:"default:0"` // 排序序号
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// ============================================================
// 文件上传模型
// ============================================================
// FileUpload 文件上传记录模型
// 集中管理所有上传的文件,支持文件版本控制
type FileUpload struct {
ID uint `json:"id" gorm:"primaryKey"` // 文件ID
UserID uint `json:"userId" gorm:"index;not null"` // 上传者用户ID
DocumentID *uint `json:"documentId"` // 关联文档ID可选
OriginalName string `json:"originalName" gorm:"size:512;not null"` // 原始文件名
StorageName string `json:"storageName" gorm:"size:512;not null"` // 存储文件名
FilePath string `json:"filePath" gorm:"size:1024;not null"` // 文件存储路径
FileSize int64 `json:"fileSize" gorm:"default:0"` // 文件大小(字节)
MimeType string `json:"mimeType" gorm:"size:128"` // MIME类型
FileType string `json:"fileType" gorm:"size:32;default:'file'"` // 文件类型
MD5Hash string `json:"md5Hash" gorm:"size:64"` // 文件MD5哈希用于去重
Version int `json:"version" gorm:"default:1"` // 版本号
ParentID *uint `json:"parentId"` // 父版本ID
Status int `json:"status" gorm:"default:1"` // 状态: 0=已删除 1=正常
CreatedAt time.Time `json:"createdAt"` // 上传时间
}
// ============================================================
// 版本历史模型
// ============================================================
// Version 文档版本历史模型
// 支持版本对比和回滚
type Version struct {
ID uint `json:"id" gorm:"primaryKey"` // 版本ID
DocumentID uint `json:"documentId" gorm:"uniqueIndex:uk_doc_version;not null"` // 文档ID
UserID uint `json:"userId" gorm:"index;not null"` // 操作者用户ID
VersionNumber int `json:"versionNumber" gorm:"uniqueIndex:uk_doc_version;not null"` // 版本号
Title string `json:"title" gorm:"size:255"` // 版本标题
Description string `json:"description" gorm:"size:500"` // 版本描述
Snapshot string `json:"snapshot" gorm:"type:json;not null"` // 文档数据快照JSON
Diff string `json:"diff" gorm:"type:json"` // 与上一版本的差异JSON
IsAuto int `json:"isAuto" gorm:"default:0"` // 是否自动保存
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ============================================================
// 协作会话模型
// ============================================================
// CollaborationSession 协作会话模型
// 记录实时协作的会话信息,跟踪在线用户状态
type CollaborationSession struct {
ID uint `json:"id" gorm:"primaryKey"` // 会话ID
DocumentID uint `json:"documentId" gorm:"index;not null"` // 文档ID
UserID uint `json:"userId" gorm:"index;not null"` // 用户ID
SessionID string `json:"sessionId" gorm:"size:64;index"` // WebSocket会话ID
CursorRow *int `json:"cursorRow"` // 当前光标行位置
CursorCol *int `json:"cursorCol"` // 当前光标列位置
Selection string `json:"selection" gorm:"size:128"` // 当前选区范围
IsActive int `json:"isActive" gorm:"default:1"` // 是否在线
JoinedAt time.Time `json:"joinedAt"` // 加入时间
LastActiveAt time.Time `json:"lastActiveAt"` // 最后活跃时间
}
// ============================================================
// 导出历史模型
// ============================================================
// ExportHistory 导出历史模型
type ExportHistory struct {
ID uint `json:"id" gorm:"primaryKey"` // 导出ID
DocumentID uint `json:"documentId" gorm:"index;not null"` // 文档ID
UserID uint `json:"userId" gorm:"index;not null"` // 导出者用户ID
Format string `json:"format" gorm:"size:16;not null"` // 导出格式: xlsx/csv/pdf
FilePath string `json:"filePath" gorm:"size:1024"` // 导出文件路径
FileSize int64 `json:"fileSize" gorm:"default:0"` // 导出文件大小
Status int `json:"status" gorm:"default:0"` // 状态: 0=处理中 1=完成 2=失败
ErrorMsg string `json:"errorMsg" gorm:"size:512"` // 错误信息
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ============================================================
// 设置模型
// ============================================================
// Setting 设置模型
// 用户个人设置和系统全局配置
type Setting struct {
ID uint `json:"id" gorm:"primaryKey"` // 设置ID
UserID *uint `json:"userId"` // 用户IDNULL=系统设置)
Category string `json:"category" gorm:"size:32;not null"` // 设置分类
Key string `json:"key" gorm:"size:128;not null"` // 设置键名
Value string `json:"value" gorm:"type:text"` // 设置值
Description string `json:"description" gorm:"size:255"` // 设置描述
CreatedAt time.Time `json:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt"` // 更新时间
}
// ============================================================
// API密钥模型
// ============================================================
// ApiKey API密钥模型
type ApiKey struct {
ID uint `json:"id" gorm:"primaryKey"` // 密钥ID
UserID uint `json:"userId" gorm:"index;not null"` // 用户ID
Name string `json:"name" gorm:"size:128;not null"` // 密钥名称
KeyHash string `json:"keyHash" gorm:"size:255;index"` // 密钥哈希
KeyPrefix string `json:"keyPrefix" gorm:"size:16"` // 密钥前缀
Permissions string `json:"permissions" gorm:"type:json"` // 权限范围JSON
RateLimit int `json:"rateLimit" gorm:"default:1000"` // 速率限制
LastUsedAt *time.Time `json:"lastUsedAt"` // 最后使用时间
ExpireAt *time.Time `json:"expireAt"` // 过期时间
IsActive int `json:"isActive" gorm:"default:1"` // 是否启用
CreatedAt time.Time `json:"createdAt"` // 创建时间
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除时间戳
}
// ============================================================
// 审计日志模型
// ============================================================
// AuditLog 审计日志模型
type AuditLog struct {
ID uint `json:"id" gorm:"primaryKey"` // 日志ID
UserID *uint `json:"userId"` // 操作者用户ID
Action string `json:"action" gorm:"size:64;not null"` // 操作类型
ResourceType string `json:"resourceType" gorm:"size:32;not null"` // 资源类型
ResourceID *uint `json:"resourceId"` // 资源ID
OldValue string `json:"oldValue" gorm:"type:json"` // 操作前的值JSON
NewValue string `json:"newValue" gorm:"type:json"` // 操作后的值JSON
IPAddress string `json:"ipAddress" gorm:"size:64"` // 操作者IP地址
UserAgent string `json:"userAgent" gorm:"size:512"` // 浏览器User-Agent
Status int `json:"status" gorm:"default:1"` // 状态: 0=失败 1=成功
ErrorMsg string `json:"errorMsg" gorm:"size:512"` // 错误信息
CreatedAt time.Time `json:"createdAt"` // 操作时间
}

74
router/router.go Normal file
View File

@@ -0,0 +1,74 @@
// Package router 配置API路由和中间件
// 定义所有HTTP接口路径、处理函数和CORS等中间件
package router
import (
"excel-api/handlers"
ws "excel-api/websocket"
"github.com/gin-gonic/gin"
)
// Setup 初始化并返回配置好的Gin引擎
// 包含所有API路由、中间件配置
// hub: WebSocket连接管理中心实例
func Setup(hub *ws.Hub) *gin.Engine {
r := gin.Default()
// 配置CORS中间件允许前端跨域请求
r.Use(CORSMiddleware())
// 静态文件服务:上传的文件通过 /uploads/ 路径访问
r.Static("/uploads", "./uploads")
// API路由组
api := r.Group("/api")
{
// ========== 工作簿相关接口 ==========
api.GET("/workbooks", handlers.GetWorkbooks) // 获取工作簿列表
api.POST("/workbooks", handlers.CreateWorkbook) // 创建新工作簿
api.GET("/workbooks/:id", handlers.GetWorkbook) // 获取工作簿详情
api.PUT("/workbooks/:id", handlers.UpdateWorkbook) // 更新工作簿
api.DELETE("/workbooks/:id", handlers.DeleteWorkbook) // 删除工作簿
// ========== 工作表相关接口 ==========
api.GET("/workbooks/:wid/sheets", handlers.GetSheets) // 获取工作簿下的工作表列表
api.POST("/workbooks/:wid/sheets", handlers.CreateSheet) // 在工作簿下创建新工作表
api.GET("/sheets/:id", handlers.GetSheet) // 获取工作表详情
api.PUT("/sheets/:id", handlers.UpdateSheet) // 更新工作表
api.DELETE("/sheets/:id", handlers.DeleteSheet) // 删除工作表
// ========== 单元格相关接口 ==========
api.GET("/sheets/:id/cells", handlers.GetCells) // 获取单元格数据(支持范围查询)
api.POST("/sheets/:id/cells", handlers.BatchUpdateCells) // 批量更新单元格
// ========== 文件上传接口 ==========
api.POST("/upload", handlers.Upload) // 上传文件
// ========== WebSocket接口 ==========
api.GET("/ws", ws.HandleWebSocket(hub)) // WebSocket连接端点
// ========== 预检请求处理 ==========
api.OPTIONS("/*path", handlers.HandleOptions) // 处理CORS预检请求
}
return r
}
// CORSMiddleware 返回CORS跨域中间件
// 允许前端开发服务器端口8888跨域访问后端API
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

87
utils/response.go Normal file
View File

@@ -0,0 +1,87 @@
// Package utils 提供通用工具函数
// 包括统一的 JSON 响应格式化、错误处理等
package utils
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Response 统一API响应结构体
type Response struct {
Code int `json:"code"` // 业务状态码0=成功非0=失败
Message string `json:"message"` // 提示信息
Data interface{} `json:"data"` // 响应数据,可以是任意类型
}
// Success 返回成功响应
// c: Gin上下文
// data: 响应数据
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}
// SuccessWithMessage 返回带自定义消息的成功响应
// c: Gin上下文
// message: 自定义提示信息
// data: 响应数据
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: message,
Data: data,
})
}
// Error 返回错误响应
// c: Gin上下文
// code: 业务错误码非0
// message: 错误提示信息
func Error(c *gin.Context, code int, message string) {
c.JSON(http.StatusOK, Response{
Code: code,
Message: message,
Data: nil,
})
}
// ErrorWithHTTP 返回带HTTP状态码的错误响应
// c: Gin上下文
// httpCode: HTTP状态码
// code: 业务错误码
// message: 错误提示信息
func ErrorWithHTTP(c *gin.Context, httpCode int, code int, message string) {
c.JSON(httpCode, Response{
Code: code,
Message: message,
Data: nil,
})
}
// PageResult 分页查询结果结构体
type PageResult struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页条数
}
// SuccessPage 返回分页查询成功响应
// c: Gin上下文
// list: 数据列表
// total: 总记录数
// page: 当前页码
// pageSize: 每页条数
func SuccessPage(c *gin.Context, list interface{}, total int64, page, pageSize int) {
Success(c, PageResult{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
})
}

95
websocket/handler.go Normal file
View File

@@ -0,0 +1,95 @@
// Package websocket 提供WebSocket HTTP升级处理器
package websocket
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// upgrader 用于将HTTP连接升级为WebSocket连接
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, // 读缓冲区大小
WriteBufferSize: 1024, // 写缓冲区大小
// 允许所有来源的连接(开发环境)
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// HandleWebSocket 处理WebSocket连接升级请求Gin版本
// 客户端通过此接口建立WebSocket连接加入Hub进行消息通信
// 路径: GET /api/ws
func HandleWebSocket(hub *Hub) gin.HandlerFunc {
return func(c *gin.Context) {
handleWS(hub, c.Writer, c.Request)
}
}
// HandleWebSocketHTTP 处理WebSocket连接升级请求标准net/http版本
// 用于独立的WebSocket服务端口16703
func HandleWebSocketHTTP(hub *Hub) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handleWS(hub, w, r)
}
}
// handleWS WebSocket连接处理的核心逻辑
func handleWS(hub *Hub, w http.ResponseWriter, r *http.Request) {
// 将HTTP连接升级为WebSocket连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket升级失败: %v", err)
return
}
// 创建客户端实例并注册到Hub
client := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, 256),
}
hub.register <- client
// 启动读写goroutine
go client.writePump()
go client.readPump()
}
// readPump 从WebSocket连接读取消息
// 持续监听客户端发送的消息,遇到错误或关闭时断开连接
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
// 读取消息(目前仅用于检测连接状态,后续可扩展协作协议)
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
log.Printf("WebSocket读取错误: %v", err)
}
break
}
// 收到客户端消息后广播给其他客户端(协作基础)
c.hub.BroadcastMessage(message)
}
}
// writePump 向WebSocket连接写入消息
// 从send通道读取消息并发送给客户端
func (c *Client) writePump() {
defer c.conn.Close()
for message := range c.send {
err := c.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("WebSocket写入错误: %v", err)
break
}
}
}

89
websocket/hub.go Normal file
View File

@@ -0,0 +1,89 @@
// Package websocket 实现WebSocket连接管理和消息广播
// 为在线协作功能预留,支持多客户端实时通信
package websocket
import (
"log"
"sync"
"github.com/gorilla/websocket"
)
// Hub WebSocket连接管理中心
// 负责维护所有活跃的WebSocket连接处理消息广播
type Hub struct {
clients map[*Client]bool // 所有已连接的客户端
broadcast chan []byte // 待广播的消息通道
register chan *Client // 客户端注册通道
unregister chan *Client // 客户端注销通道
mu sync.RWMutex // 读写锁,保护 clients map
}
// Client 单个WebSocket客户端连接
type Client struct {
hub *Hub // 所属的Hub
conn *websocket.Conn // WebSocket连接实例
send chan []byte // 待发送的消息队列
}
// NewHub 创建并返回一个新的Hub实例
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte, 256),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
// Run 启动Hub的主循环
// 在独立的goroutine中运行处理客户端注册、注销和消息广播
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
// 新客户端连接注册
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("WebSocket客户端已连接当前在线: %d", len(h.clients))
case client := <-h.unregister:
// 客户端断开连接注销
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
log.Printf("WebSocket客户端已断开当前在线: %d", len(h.clients))
case message := <-h.broadcast:
// 向所有客户端广播消息
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
// 发送缓冲区已满,断开该客户端
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
// BroadcastMessage 向所有连接的客户端广播消息
// message: 要广播的字节数据
func (h *Hub) BroadcastMessage(message []byte) {
h.broadcast <- message
}
// ClientCount 返回当前在线客户端数量
func (h *Hub) ClientCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.clients)
}