初始化
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal 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
9
.idea/excel-api.iml
generated
Normal 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
11
.idea/go.imports.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
42
config.yaml
Normal 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
93
config/config.go
Normal 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
43
go.mod
Normal 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
104
go.sum
Normal 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
153
handlers/cell.go
Normal 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
174
handlers/sheet.go
Normal 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
130
handlers/upload.go
Normal 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
205
handlers/workbook.go
Normal 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
52
main.go
Normal 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
83
models/db.go
Normal 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 DSN(Data 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
449
models/model.go
Normal 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"` // 操作用户ID(0=系统操作)
|
||||
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"` // 工作表ID(NULL=文档级评论)
|
||||
ParentID *uint `json:"parentId" gorm:"index"` // 父评论ID(NULL=顶级评论)
|
||||
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"` // 发送者用户ID(NULL=系统通知)
|
||||
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"` // 父文件夹ID(NULL=根目录)
|
||||
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"` // 用户ID(NULL=系统设置)
|
||||
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
74
router/router.go
Normal 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
87
utils/response.go
Normal 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
95
websocket/handler.go
Normal 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
89
websocket/hub.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user