m
This commit is contained in:
1 parent
76a6c81af2
commit
b19d083031
68 files changed
+3889
-343
No files matched your search
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
logs/
|
||||
-2
@@ -3,8 +3,6 @@
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Environment-dependent path to Maven home directory
|
||||
/mavenHomeManager.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<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" />
|
||||
@@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<methods>
|
||||
<method importPath="base-framework/pkg/utils/db" receiver="*Client" name="QueryRows" />
|
||||
</methods>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/base-project.iml" filepath="$PROJECT_DIR$/.idea/base-project.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/base-farmework.iml" filepath="$PROJECT_DIR$/.idea/base-farmework.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Whitespace-only changes.
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"base-framework/internal/app"
|
||||
"base-framework/pkg/config"
|
||||
"base-framework/pkg/logx"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_, err := config.InitApplicationConfig("./configs/app/application.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
err = config.InitDBConfig("./configs/app/database.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("加载数据库配置失败: %v", err)
|
||||
return
|
||||
}
|
||||
// 初始化日志
|
||||
logx.InitLogger()
|
||||
|
||||
// 初始化路由
|
||||
r := app.InitAppRouter()
|
||||
|
||||
err = http.ListenAndServe(":"+strconv.Itoa(config.Server.Port), r)
|
||||
if err != nil {
|
||||
log.Fatalf("server start failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 8082
|
||||
|
||||
jwt:
|
||||
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
|
||||
expiry: 168h
|
||||
|
||||
log:
|
||||
dir: "logs" # 日志根目录
|
||||
level: "debug" # 全局最低日志级别
|
||||
format: "json" # 输出格式,可选 json 或 console
|
||||
daily: true # 是否按天滚动
|
||||
maxDays: 30 # 保留天数,0表示无限
|
||||
caller: true # 是否记录调用文件行号
|
||||
levelDirs: false # 是否按级别目录
|
||||
console: true # 是否打印到控制台
|
||||
|
||||
snowflake:
|
||||
datacenterID: 0
|
||||
workerID: 0
|
||||
@@ -0,0 +1,6 @@
|
||||
g3cd:
|
||||
host: 117.72.182.135
|
||||
port: 5432
|
||||
username: postgres
|
||||
password: zhang520..
|
||||
dbname: test
|
||||
@@ -0,0 +1,27 @@
|
||||
module base-framework
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/spf13/viper v1.20.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"base-framework/internal/app/handle"
|
||||
"base-framework/pkg/router"
|
||||
)
|
||||
|
||||
func InitAuth(r *router.Router) {
|
||||
group := r.Group("/auth")
|
||||
group.POST("/login", handle.Login)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"base-framework/internal/app/handle"
|
||||
"base-framework/pkg/router"
|
||||
)
|
||||
|
||||
func InitTest(r *router.Router) {
|
||||
group := r.Group("/test")
|
||||
group.GET("/", handle.TestDB)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"base-framework/pkg/config"
|
||||
"base-framework/pkg/reponse"
|
||||
"base-framework/pkg/router"
|
||||
"base-framework/pkg/utils/jwt"
|
||||
)
|
||||
|
||||
// Login 登录接口
|
||||
func Login(c *router.Context) {
|
||||
// 绑定请求体
|
||||
var req struct {
|
||||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"userId"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
reponse.Error(c).Message("请求体不能为空").Send()
|
||||
return
|
||||
}
|
||||
|
||||
if req.OrgID == "" {
|
||||
reponse.Error(c).Message("机构码不能为空").Send()
|
||||
return
|
||||
}
|
||||
|
||||
// 参数校验
|
||||
if req.UserID == "" {
|
||||
reponse.Error(c).Message("账户不能为空").Send()
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
reponse.Error(c).Message("密码不能为空").Send()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有该机构号
|
||||
hasOrg := false
|
||||
for k := range config.GetDBConfigs() {
|
||||
if k == req.OrgID {
|
||||
hasOrg = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOrg {
|
||||
reponse.Error(c).Code(reponse.CodeInvalidOrgCode).Send()
|
||||
return
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
token, err := jwt.CreateToken(req.OrgID, req.UserID)
|
||||
|
||||
if err != nil {
|
||||
reponse.Error(c).Message("生成令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
reponse.Success(c).Data(
|
||||
map[string]string{
|
||||
"token": token,
|
||||
}).Send()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package handle
|
||||
|
||||
import (
|
||||
"base-framework/pkg/router"
|
||||
)
|
||||
|
||||
func TestDB(c *router.Context) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"base-framework/internal/app/api"
|
||||
"base-framework/pkg/middleware"
|
||||
"base-framework/pkg/router"
|
||||
)
|
||||
|
||||
func InitAppRouter() *router.Router {
|
||||
r := router.NewRouter()
|
||||
|
||||
r.Use(middleware.Routinex())
|
||||
r.Use(middleware.Error())
|
||||
r.Use(middleware.Auth()).ExcludePaths("/auth/login")
|
||||
r.Use(middleware.Logger())
|
||||
|
||||
initApi(r)
|
||||
return r
|
||||
}
|
||||
|
||||
func initApi(r *router.Router) {
|
||||
api.InitTest(r)
|
||||
api.InitAuth(r)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"base-framework/pkg/types"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 全局导出变量,指向 types 包中的配置实例
|
||||
var (
|
||||
Server *types.Server
|
||||
JWT *types.Jwt
|
||||
Log *types.Log
|
||||
Snowflake *types.Snowflake
|
||||
)
|
||||
|
||||
// InitApplicationConfig 初始化配置,传入配置文件路径
|
||||
func InitApplicationConfig(configPath string) (err error, err2 error) {
|
||||
v := viper.New()
|
||||
v.SetConfigFile(configPath)
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return err, err2
|
||||
}
|
||||
|
||||
// server 配置
|
||||
var server types.Server
|
||||
if err := v.UnmarshalKey("server", &server); err != nil {
|
||||
return err, err2
|
||||
}
|
||||
|
||||
// jwt 配置
|
||||
var jwt types.Jwt
|
||||
if err := v.UnmarshalKey("jwt", &jwt); err != nil {
|
||||
return err, err2
|
||||
}
|
||||
|
||||
// 初始化日志模块
|
||||
var log types.Log
|
||||
if err := v.UnmarshalKey("log", &log); err != nil {
|
||||
return err, err2
|
||||
}
|
||||
|
||||
// 初始化日志模块
|
||||
var snowflake types.Snowflake
|
||||
if err := v.UnmarshalKey("snowflake", &snowflake); err != nil {
|
||||
return err, err2
|
||||
}
|
||||
|
||||
Server = &server
|
||||
JWT = &jwt
|
||||
Log = &log
|
||||
Snowflake = &snowflake
|
||||
|
||||
return nil, err2
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"base-framework/pkg/types"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
_ "github.com/lib/pq" // PostgreSQL 驱动,根据实际数据库替换
|
||||
)
|
||||
|
||||
// 默认连接池参数
|
||||
const (
|
||||
defaultMaxOpenConns = 10
|
||||
defaultMaxIdleConns = 5
|
||||
defaultConnMaxLifetime = time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
dbConfigs map[string]types.DBConfig // 配置副本
|
||||
dataSources = make(map[string]*sql.DB) // 连接池
|
||||
configsDSN = make(map[string]string) // key -> dsn
|
||||
mu sync.RWMutex
|
||||
v *viper.Viper
|
||||
)
|
||||
|
||||
// InitDBConfig 初始化并加载 db.yaml 配置,同时启动监听
|
||||
func InitDBConfig(configPath string) error {
|
||||
v = viper.New()
|
||||
v.SetConfigFile(configPath)
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("读取数据库配置失败: %w", err)
|
||||
}
|
||||
|
||||
if err := unmarshalConfigs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 首次加载后,初始化数据源
|
||||
if err := ReloadDataSources(GetDBConfigs()); err != nil {
|
||||
log.Printf("首次加载数据源失败: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 监听配置文件变化
|
||||
v.WatchConfig()
|
||||
v.OnConfigChange(func(e fsnotify.Event) {
|
||||
log.Printf("数据库配置文件发生变化: %s\n", e.Name)
|
||||
if err := unmarshalConfigs(); err != nil {
|
||||
log.Printf("重新加载数据库配置失败: %v\n", err)
|
||||
} else {
|
||||
if err := ReloadDataSources(GetDBConfigs()); err != nil {
|
||||
log.Printf("更新数据源失败: %v\n", err)
|
||||
} else {
|
||||
log.Println("数据库连接池已更新")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalConfigs 解析配置到全局变量,线程安全
|
||||
func unmarshalConfigs() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
temp := make(map[string]types.DBConfig)
|
||||
if err := v.Unmarshal(&temp); err != nil {
|
||||
return fmt.Errorf("解析数据库配置失败: %w", err)
|
||||
}
|
||||
dbConfigs = temp
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDBConfigs 并发安全地返回当前数据库配置副本
|
||||
func GetDBConfigs() map[string]types.DBConfig {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
res := make(map[string]types.DBConfig, len(dbConfigs))
|
||||
for k, v := range dbConfigs {
|
||||
res[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// buildDSN 构建 PostgreSQL DSN
|
||||
func buildDSN(cfg types.DBConfig) string {
|
||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Dbname)
|
||||
}
|
||||
|
||||
// ReloadDataSources 支持增量更新数据源,并验证数据库连接
|
||||
func ReloadDataSources(newConfigs map[string]types.DBConfig) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
retain := make(map[string]bool)
|
||||
|
||||
for key, cfg := range newConfigs {
|
||||
newDSN := buildDSN(cfg)
|
||||
oldDSN, exists := configsDSN[key]
|
||||
|
||||
// 新增数据源或配置变更
|
||||
if !exists || oldDSN != newDSN {
|
||||
// 关闭旧连接(如果存在)
|
||||
if exists {
|
||||
if oldDB := dataSources[key]; oldDB != nil {
|
||||
_ = oldDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", newDSN)
|
||||
if err != nil {
|
||||
log.Printf("[db] 数据源 %s Open 失败: %v", key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试实际连接数据库
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Printf("[db] 数据源 %s 连接失败: %v", key, err)
|
||||
_ = db.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
applyDBConfig(db, cfg)
|
||||
dataSources[key] = db
|
||||
configsDSN[key] = newDSN
|
||||
|
||||
if !exists {
|
||||
log.Printf("[db] 新增数据源 %s 成功", key)
|
||||
} else {
|
||||
log.Printf("[db] 更新数据源 %s 成功", key)
|
||||
}
|
||||
}
|
||||
|
||||
// 配置没变,不用操作
|
||||
retain[key] = true
|
||||
}
|
||||
|
||||
// 关闭并移除不再需要的数据源
|
||||
for key, db := range dataSources {
|
||||
if !retain[key] {
|
||||
_ = db.Close()
|
||||
delete(dataSources, key)
|
||||
delete(configsDSN, key)
|
||||
log.Printf("[db] 移除数据源 %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyDBConfig 设置 sql.DB 连接池参数,并应用默认值
|
||||
func applyDBConfig(db *sql.DB, cfg types.DBConfig) {
|
||||
maxOpen := cfg.MaxOpenConns
|
||||
if maxOpen <= 0 {
|
||||
maxOpen = defaultMaxOpenConns
|
||||
}
|
||||
db.SetMaxOpenConns(maxOpen)
|
||||
|
||||
maxIdle := cfg.MaxIdleConns
|
||||
if maxIdle <= 0 {
|
||||
maxIdle = defaultMaxIdleConns
|
||||
}
|
||||
db.SetMaxIdleConns(maxIdle)
|
||||
|
||||
timeout := cfg.ConnMaxLifetime
|
||||
if timeout <= 0 {
|
||||
timeout = defaultConnMaxLifetime
|
||||
}
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
}
|
||||
|
||||
// GetDB 获取数据库连接
|
||||
func GetDB(key string) (*sql.DB, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
db, ok := dataSources[key]
|
||||
return db, ok
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// StackError 保存原始错误和堆栈
|
||||
type StackError struct {
|
||||
err error
|
||||
stack string
|
||||
}
|
||||
|
||||
// 实现 error 接口
|
||||
func (s *StackError) Error() string {
|
||||
if s.err == nil {
|
||||
return ""
|
||||
}
|
||||
return s.err.Error()
|
||||
}
|
||||
|
||||
// StackTrace 返回堆栈信息
|
||||
func (s *StackError) StackTrace() string {
|
||||
return s.stack
|
||||
}
|
||||
|
||||
// WrapWithStack 给错误附加堆栈,如果已经是 StackError 则直接返回
|
||||
func WrapWithStack(err error) *StackError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var se *StackError
|
||||
if errors.As(err, &se) {
|
||||
return se
|
||||
}
|
||||
|
||||
return &StackError{
|
||||
err: err,
|
||||
stack: string(debug.Stack()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package logx
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func String(key, val string) zap.Field { return zap.String(key, val) }
|
||||
func Stringp(key string, val *string) zap.Field { return zap.Stringp(key, val) }
|
||||
func Strings(key string, val []string) zap.Field { return zap.Strings(key, val) }
|
||||
|
||||
func Bool(key string, val bool) zap.Field { return zap.Bool(key, val) }
|
||||
func Boolp(key string, val *bool) zap.Field { return zap.Boolp(key, val) }
|
||||
func Bools(key string, val []bool) zap.Field { return zap.Bools(key, val) }
|
||||
|
||||
func Int(key string, val int) zap.Field { return zap.Int(key, val) }
|
||||
func Intp(key string, val *int) zap.Field { return zap.Intp(key, val) }
|
||||
func Ints(key string, val []int) zap.Field { return zap.Ints(key, val) }
|
||||
|
||||
func Int64(key string, val int64) zap.Field { return zap.Int64(key, val) }
|
||||
func Int64p(key string, val *int64) zap.Field { return zap.Int64p(key, val) }
|
||||
func Int64s(key string, val []int64) zap.Field { return zap.Int64s(key, val) }
|
||||
|
||||
func Int32(key string, val int32) zap.Field { return zap.Int32(key, val) }
|
||||
func Int32p(key string, val *int32) zap.Field { return zap.Int32p(key, val) }
|
||||
func Int32s(key string, val []int32) zap.Field { return zap.Int32s(key, val) }
|
||||
|
||||
func Int16(key string, val int16) zap.Field { return zap.Int16(key, val) }
|
||||
func Int16p(key string, val *int16) zap.Field { return zap.Int16p(key, val) }
|
||||
func Int16s(key string, val []int16) zap.Field { return zap.Int16s(key, val) }
|
||||
|
||||
func Int8(key string, val int8) zap.Field { return zap.Int8(key, val) }
|
||||
func Int8p(key string, val *int8) zap.Field { return zap.Int8p(key, val) }
|
||||
func Int8s(key string, val []int8) zap.Field { return zap.Int8s(key, val) }
|
||||
|
||||
func Uint(key string, val uint) zap.Field { return zap.Uint(key, val) }
|
||||
func Uintp(key string, val *uint) zap.Field { return zap.Uintp(key, val) }
|
||||
func Uints(key string, val []uint) zap.Field { return zap.Uints(key, val) }
|
||||
|
||||
func Uint64(key string, val uint64) zap.Field { return zap.Uint64(key, val) }
|
||||
func Uint64p(key string, val *uint64) zap.Field { return zap.Uint64p(key, val) }
|
||||
func Uint64s(key string, val []uint64) zap.Field { return zap.Uint64s(key, val) }
|
||||
|
||||
func Uint32(key string, val uint32) zap.Field { return zap.Uint32(key, val) }
|
||||
func Uint32p(key string, val *uint32) zap.Field { return zap.Uint32p(key, val) }
|
||||
func Uint32s(key string, val []uint32) zap.Field { return zap.Uint32s(key, val) }
|
||||
|
||||
func Uint16(key string, val uint16) zap.Field { return zap.Uint16(key, val) }
|
||||
func Uint16p(key string, val *uint16) zap.Field { return zap.Uint16p(key, val) }
|
||||
func Uint16s(key string, val []uint16) zap.Field { return zap.Uint16s(key, val) }
|
||||
|
||||
func Uint8(key string, val uint8) zap.Field { return zap.Uint8(key, val) }
|
||||
func Uint8p(key string, val *uint8) zap.Field { return zap.Uint8p(key, val) }
|
||||
|
||||
func Uintptr(key string, val uintptr) zap.Field { return zap.Uintptr(key, val) }
|
||||
func Uintptrp(key string, val *uintptr) zap.Field { return zap.Uintptrp(key, val) }
|
||||
func Uintptrs(key string, val []uintptr) zap.Field { return zap.Uintptrs(key, val) }
|
||||
|
||||
func Float32(key string, val float32) zap.Field { return zap.Float32(key, val) }
|
||||
func Float32p(key string, val *float32) zap.Field { return zap.Float32p(key, val) }
|
||||
func Float32s(key string, val []float32) zap.Field { return zap.Float32s(key, val) }
|
||||
|
||||
func Float64(key string, val float64) zap.Field { return zap.Float64(key, val) }
|
||||
func Float64p(key string, val *float64) zap.Field { return zap.Float64p(key, val) }
|
||||
func Float64s(key string, val []float64) zap.Field { return zap.Float64s(key, val) }
|
||||
|
||||
func Complex64(key string, val complex64) zap.Field { return zap.Complex64(key, val) }
|
||||
func Complex64p(key string, val *complex64) zap.Field { return zap.Complex64p(key, val) }
|
||||
func Complex64s(key string, val []complex64) zap.Field { return zap.Complex64s(key, val) }
|
||||
|
||||
func Complex128(key string, val complex128) zap.Field { return zap.Complex128(key, val) }
|
||||
func Complex128p(key string, val *complex128) zap.Field { return zap.Complex128p(key, val) }
|
||||
func Complex128s(key string, val []complex128) zap.Field { return zap.Complex128s(key, val) }
|
||||
|
||||
func Binary(key string, val []byte) zap.Field { return zap.Binary(key, val) }
|
||||
|
||||
func Time(key string, val time.Time) zap.Field { return zap.Time(key, val) }
|
||||
func Timep(key string, val *time.Time) zap.Field { return zap.Timep(key, val) }
|
||||
func Times(key string, val []time.Time) zap.Field { return zap.Times(key, val) }
|
||||
|
||||
func Duration(key string, val time.Duration) zap.Field { return zap.Duration(key, val) }
|
||||
func Durationp(key string, val *time.Duration) zap.Field { return zap.Durationp(key, val) }
|
||||
func Durations(key string, val []time.Duration) zap.Field { return zap.Durations(key, val) }
|
||||
|
||||
func Any(key string, val any) zap.Field { return zap.Any(key, val) }
|
||||
@@ -0,0 +1,237 @@
|
||||
package logx
|
||||
|
||||
import (
|
||||
"base-framework/pkg/routinex"
|
||||
"base-framework/pkg/utils/strutil"
|
||||
"base-framework/pkg/utils/uid"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"base-framework/pkg/config"
|
||||
"base-framework/pkg/types"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
Logger *zap.Logger
|
||||
TrackID = "trackId"
|
||||
once sync.Once
|
||||
currentDate string
|
||||
mu sync.Mutex
|
||||
|
||||
infoFile *os.File
|
||||
warnFile *os.File
|
||||
errorFile *os.File
|
||||
|
||||
infoCore zapcore.Core
|
||||
warnCore zapcore.Core
|
||||
errorCore zapcore.Core
|
||||
)
|
||||
|
||||
const logTmFmtWithMS = "2006-01-02 15:04:05.000"
|
||||
|
||||
var levelColor = map[zapcore.Level]string{
|
||||
zapcore.DebugLevel: "\033[36mDEBUG\033[0m",
|
||||
zapcore.InfoLevel: "\033[32mINFO\033[0m",
|
||||
zapcore.WarnLevel: "\033[33mWARN\033[0m",
|
||||
zapcore.ErrorLevel: "\033[31mERROR\033[0m",
|
||||
zapcore.DPanicLevel: "\033[35mDPANIC\033[0m",
|
||||
zapcore.PanicLevel: "\033[35mPANIC\033[0m",
|
||||
zapcore.FatalLevel: "\033[35mFATAL\033[0m",
|
||||
}
|
||||
|
||||
// ----------------- trackCore -----------------
|
||||
// 在写入日志时自动从 routinex 获取 log_id 并加入字段
|
||||
type trackCore struct {
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
func (c *trackCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return &trackCore{Core: c.Core.With(fields)}
|
||||
}
|
||||
|
||||
func (c *trackCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(entry.Level) {
|
||||
return ce.AddCore(entry, c)
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func (c *trackCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
val := routinex.Get(TrackID)
|
||||
if val == nil {
|
||||
val = uid.NextID()
|
||||
routinex.Set(TrackID, val) // 保证同一请求复用同一 trackID
|
||||
}
|
||||
|
||||
fields = append(fields, zap.String(TrackID, strutil.ToString(val)))
|
||||
return c.Core.Write(entry, fields)
|
||||
}
|
||||
|
||||
// InitLogger ----------------- 初始化日志 -----------------
|
||||
func InitLogger() {
|
||||
once.Do(func() {
|
||||
if config.Log == nil {
|
||||
config.Log = defaultLogConfig()
|
||||
}
|
||||
createDirIfNotExist(config.Log.Dir)
|
||||
switchDate(time.Now().Format("2006-01-02"))
|
||||
})
|
||||
}
|
||||
|
||||
func defaultLogConfig() *types.Log {
|
||||
return &types.Log{
|
||||
Dir: "logs",
|
||||
Level: "debug",
|
||||
Format: "json",
|
||||
Daily: true,
|
||||
MaxDays: 30,
|
||||
Caller: true,
|
||||
LevelDirs: true,
|
||||
Console: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- 初始化核心 -----------------
|
||||
func switchDate(today string) {
|
||||
currentDate = today
|
||||
|
||||
if infoFile != nil {
|
||||
_ = infoFile.Close()
|
||||
}
|
||||
if warnFile != nil {
|
||||
_ = warnFile.Close()
|
||||
}
|
||||
if errorFile != nil {
|
||||
_ = errorFile.Close()
|
||||
}
|
||||
|
||||
infoPath, warnPath, errorPath := config.Log.Dir, config.Log.Dir, config.Log.Dir
|
||||
if config.Log.LevelDirs {
|
||||
infoPath = filepath.Join(config.Log.Dir, "info")
|
||||
warnPath = filepath.Join(config.Log.Dir, "warn")
|
||||
errorPath = filepath.Join(config.Log.Dir, "error")
|
||||
createDirIfNotExist(infoPath)
|
||||
createDirIfNotExist(warnPath)
|
||||
createDirIfNotExist(errorPath)
|
||||
} else {
|
||||
createDirIfNotExist(config.Log.Dir)
|
||||
}
|
||||
|
||||
infoFile = getOrCreateFile(filepath.Join(infoPath, fmt.Sprintf("info-%s.log", today)))
|
||||
warnFile = getOrCreateFile(filepath.Join(warnPath, fmt.Sprintf("warn-%s.log", today)))
|
||||
errorFile = getOrCreateFile(filepath.Join(errorPath, fmt.Sprintf("error-%s.log", today)))
|
||||
|
||||
var fileEncoder zapcore.Encoder
|
||||
if config.Log.Format == "console" {
|
||||
fileEncoder = newConsoleEncoder(false)
|
||||
} else {
|
||||
fileEncoder = newJSONEncoder()
|
||||
}
|
||||
|
||||
infoCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(infoFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl <= zapcore.InfoLevel
|
||||
}))}
|
||||
|
||||
warnCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(warnFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl == zapcore.WarnLevel
|
||||
}))}
|
||||
|
||||
errorCore = &trackCore{zapcore.NewCore(fileEncoder, zapcore.AddSync(errorFile), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl >= zapcore.ErrorLevel
|
||||
}))}
|
||||
|
||||
cores := []zapcore.Core{infoCore, warnCore, errorCore}
|
||||
|
||||
if config.Log.Console {
|
||||
consoleCore := &trackCore{zapcore.NewCore(newConsoleEncoder(true), zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl >= zapcore.DebugLevel
|
||||
}))}
|
||||
cores = append(cores, consoleCore)
|
||||
}
|
||||
|
||||
Logger = zap.New(zapcore.NewTee(cores...))
|
||||
|
||||
if config.Log.Caller {
|
||||
Logger = Logger.WithOptions(zap.AddCaller())
|
||||
} else {
|
||||
Logger = Logger.WithOptions(zap.AddCallerSkip(1))
|
||||
}
|
||||
|
||||
Logger = Logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error {
|
||||
if config.Log.Daily {
|
||||
checkDateSwitch()
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
func newConsoleEncoder(enableColor bool) zapcore.Encoder {
|
||||
cfg := zapcore.EncoderConfig{
|
||||
TimeKey: "ts",
|
||||
LevelKey: "level",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format(logTmFmtWithMS))
|
||||
},
|
||||
EncodeLevel: func(lvl zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
||||
if enableColor {
|
||||
color, ok := levelColor[lvl]
|
||||
if !ok {
|
||||
color = lvl.CapitalString()
|
||||
}
|
||||
enc.AppendString(color)
|
||||
} else {
|
||||
enc.AppendString(lvl.CapitalString())
|
||||
}
|
||||
},
|
||||
EncodeCaller: func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
|
||||
pid := os.Getpid()
|
||||
enc.AppendString(fmt.Sprintf("[%d] [%d] %s", pid, routinex.CurGID(), caller.TrimmedPath()))
|
||||
},
|
||||
}
|
||||
return zapcore.NewConsoleEncoder(cfg)
|
||||
}
|
||||
|
||||
func newJSONEncoder() zapcore.Encoder {
|
||||
cfg := zap.NewProductionEncoderConfig()
|
||||
cfg.TimeKey = "ts"
|
||||
cfg.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format(logTmFmtWithMS))
|
||||
}
|
||||
cfg.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
cfg.EncodeCaller = zapcore.ShortCallerEncoder
|
||||
return zapcore.NewJSONEncoder(cfg)
|
||||
}
|
||||
|
||||
func checkDateSwitch() {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
if today != currentDate {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if today != currentDate {
|
||||
switchDate(today)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createDirIfNotExist(dir string) {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrCreateFile(file string) *os.File {
|
||||
f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"base-framework/pkg/config"
|
||||
"base-framework/pkg/reponse"
|
||||
"base-framework/pkg/router"
|
||||
"base-framework/pkg/utils/jwt"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Auth() router.HandlerFunc {
|
||||
return func(c *router.Context) {
|
||||
|
||||
tokenHeader := c.Header("Authorization")
|
||||
userIdHeader := c.Header("userId")
|
||||
orgIDHeader := c.Header("orgId")
|
||||
|
||||
// 缺少登录信息
|
||||
if tokenHeader == "" || userIdHeader == "" || orgIDHeader == "" {
|
||||
reponse.Error(c).Code(reponse.CodeNoLogin).Send()
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Bearer token 格式校验
|
||||
parts := strings.Fields(tokenHeader)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
reponse.Error(c).Code(reponse.CodeInvalidToken).Send()
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 token
|
||||
tokenStr := parts[1]
|
||||
claims, err := jwt.VerifyToken(tokenStr)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, jwt.ErrTokenExpired):
|
||||
reponse.Error(c).Code(reponse.CodeLoginExpired).Send()
|
||||
default:
|
||||
reponse.Error(c).Code(reponse.CodeInvalidToken).Send()
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// user_id 校验
|
||||
if claims.UserID != userIdHeader {
|
||||
reponse.Error(c).Code(reponse.CodeInvalidToken).Send()
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// org_id 校验
|
||||
if claims.OrgID != orgIDHeader {
|
||||
reponse.Error(c).Code(reponse.CodeInvalidToken).Send()
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有该机构号
|
||||
hasOrg := false
|
||||
for k := range config.GetDBConfigs() {
|
||||
if k == orgIDHeader {
|
||||
hasOrg = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOrg {
|
||||
reponse.Error(c).Code(reponse.CodeInvalidOrgCode).Send()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("orgId", orgIDHeader)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"base-framework/pkg/logx"
|
||||
"base-framework/pkg/reponse"
|
||||
"base-framework/pkg/router"
|
||||
"base-framework/pkg/routinex"
|
||||
"base-framework/pkg/utils/strutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 仅初始化一次的项目信息
|
||||
var (
|
||||
initOnce sync.Once
|
||||
projectBase string // 例:base-farmework
|
||||
projectAbsPrefix string // 例:/abs/path/to/base-farmework/
|
||||
projectAbsPrefixLower string // 小写版,用于不区分大小写前缀比较(Windows 安全)
|
||||
)
|
||||
|
||||
// Error 中间件:捕获 c.Errors() 并返回统一的 ErrorInfo
|
||||
func Error() router.HandlerFunc {
|
||||
// 注册时预热(只做一次)
|
||||
initOnce.Do(initProjectInfo)
|
||||
|
||||
return func(c *router.Context) {
|
||||
c.Next()
|
||||
|
||||
errs := c.Errors()
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
lastErr := errs[len(errs)-1]
|
||||
stack := lastErr.StackTrace()
|
||||
location := extractBusinessLocation(stack)
|
||||
trackId := routinex.Get(logx.TrackID)
|
||||
|
||||
var trackIDStr string
|
||||
if trackId != nil {
|
||||
trackIDStr = strutil.ToString(trackId) // 用你之前写的工具类
|
||||
}
|
||||
|
||||
var info = reponse.ErrorInfo{
|
||||
Method: c.Request.Method,
|
||||
Path: c.Request.URL.Path,
|
||||
Message: lastErr.Error(),
|
||||
//Stack: stack,
|
||||
Location: location,
|
||||
TrackId: trackIDStr,
|
||||
}
|
||||
|
||||
logx.Logger.Error(
|
||||
"系统异常",
|
||||
logx.String("trackID", trackIDStr),
|
||||
logx.String("method", c.Request.Method),
|
||||
logx.String("path", c.Request.URL.Path),
|
||||
logx.String("location", location),
|
||||
logx.String("stack", stack),
|
||||
)
|
||||
|
||||
reponse.Error(c).ErrorInfo(info).Send()
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在注册时执行:确定 go.mod 根目录并缓存前缀
|
||||
func initProjectInfo() {
|
||||
// 以当前源文件为起点向上找 go.mod
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
if root, ok := findGoModRoot(file); ok {
|
||||
projectBase = filepath.Base(root)
|
||||
|
||||
p := filepath.ToSlash(root)
|
||||
if !strings.HasSuffix(p, "/") {
|
||||
p += "/"
|
||||
}
|
||||
projectAbsPrefix = p
|
||||
projectAbsPrefixLower = strings.ToLower(p)
|
||||
}
|
||||
}
|
||||
|
||||
// extractBusinessLocation:获取 AddError 后的业务 Handler 文件行(仅返回 <project>/<subpath>.go:<line>)
|
||||
func extractBusinessLocation(stack string) string {
|
||||
lines := strings.Split(stack, "\n")
|
||||
foundAddError := false
|
||||
countGo := 0
|
||||
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
|
||||
if strings.Contains(line, "AddError") {
|
||||
foundAddError = true
|
||||
continue
|
||||
}
|
||||
if !foundAddError {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, ".go:") {
|
||||
countGo++
|
||||
if countGo == 2 {
|
||||
// 去掉 " +0x..." 偏移
|
||||
if i := strings.Index(line, " +"); i != -1 {
|
||||
line = line[:i]
|
||||
}
|
||||
return trimToProject(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 将绝对路径裁剪为 "<project>/<subpath>.go:<line>"
|
||||
func trimToProject(goLine string) string {
|
||||
// 统一斜杠
|
||||
line := strings.ReplaceAll(goLine, "\\", "/")
|
||||
|
||||
// 用最后一个冒号分离(避免 Windows 盘符)
|
||||
idx := strings.LastIndex(line, ":")
|
||||
if idx == -1 || idx+1 >= len(line) {
|
||||
return line
|
||||
}
|
||||
filePath, lineNo := line[:idx], line[idx+1:]
|
||||
|
||||
// 优先:前缀裁剪(O(1)),比 filepath.Rel 更快
|
||||
if projectAbsPrefix != "" {
|
||||
fp := filepath.ToSlash(filePath)
|
||||
fpl := strings.ToLower(fp)
|
||||
if strings.HasPrefix(fpl, projectAbsPrefixLower) {
|
||||
rel := fp[len(projectAbsPrefix):]
|
||||
return projectBase + "/" + rel + ":" + lineNo
|
||||
}
|
||||
}
|
||||
|
||||
// 其次:internal/cmd/pkg 作为锚点
|
||||
segs := strings.Split(filePath, "/")
|
||||
for i, s := range segs {
|
||||
if s == "internal" || s == "cmd" || s == "pkg" {
|
||||
if i > 0 {
|
||||
return strings.Join(segs[i-1:], "/") + ":" + lineNo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 启发式:目录名包含 '-' 或 '_',当作仓库名
|
||||
for i, s := range segs {
|
||||
if strings.ContainsAny(s, "-_") {
|
||||
return strings.Join(segs[i:], "/") + ":" + lineNo
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:末尾 4 级
|
||||
start := len(segs) - 4
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
return strings.Join(segs[start:], "/") + ":" + lineNo
|
||||
}
|
||||
|
||||
// 从给定文件路径向上查找包含 go.mod 的目录(仅注册时调用一次)
|
||||
func findGoModRoot(filePath string) (string, bool) {
|
||||
dir := filepath.Clean(filepath.Dir(filepath.FromSlash(filePath)))
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir, true
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", false
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"base-framework/pkg/logx"
|
||||
"base-framework/pkg/router"
|
||||
"base-framework/pkg/routinex"
|
||||
"base-framework/pkg/utils/uid"
|
||||
)
|
||||
|
||||
func Logger() router.HandlerFunc {
|
||||
return func(c *router.Context) {
|
||||
routinex.Set(logx.TrackID, uid.NextID())
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"base-framework/pkg/reponse"
|
||||
"base-framework/pkg/router"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func Recovery() router.HandlerFunc {
|
||||
return func(c *router.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 打印 panic 信息和堆栈
|
||||
log.Printf("[Recovery] panic recovered: %v\n%s", err, debug.Stack())
|
||||
|
||||
reponse.Error(c).Send()
|
||||
}
|
||||
}()
|
||||
|
||||
// 调用后续 handler
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"base-framework/pkg/router"
|
||||
"base-framework/pkg/routinex"
|
||||
)
|
||||
|
||||
func Routinex() router.HandlerFunc {
|
||||
return func(c *router.Context) {
|
||||
defer routinex.Clear()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package reponse
|
||||
|
||||
import (
|
||||
"base-framework/pkg/router"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ErrorInfo struct {
|
||||
TrackId string `json:"trackId,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
// Result 统一响应结构体
|
||||
type Result struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error ErrorInfo `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
CodeSuccess = "0000" // 成功
|
||||
CodeFail = "0001" // 失败
|
||||
|
||||
CodeNoLogin = "1001" // 未登录
|
||||
CodeLoginExpired = "1002" // 登录过期
|
||||
CodeInvalidToken = "1003" // Token 无效
|
||||
CodeInvalidOrgCode = "1004" // 机构码有误
|
||||
CodeInvalidAccount = "1005" // 账号或密码有误
|
||||
)
|
||||
|
||||
var codeMessages = map[string]string{
|
||||
CodeSuccess: "请求成功",
|
||||
CodeFail: "请求失败",
|
||||
CodeNoLogin: "未登录",
|
||||
CodeLoginExpired: "登录已过期",
|
||||
CodeInvalidToken: "Token 无效",
|
||||
CodeInvalidOrgCode: "机构码有误",
|
||||
CodeInvalidAccount: "账号或密码有误",
|
||||
}
|
||||
|
||||
// 兜底提示
|
||||
const defaultMessage = "未知错误"
|
||||
|
||||
// Builder 链式构造器
|
||||
type Builder struct {
|
||||
c *router.Context
|
||||
result Result
|
||||
}
|
||||
|
||||
// Success 构造成功响应
|
||||
func Success(c *router.Context) *Builder {
|
||||
return &Builder{
|
||||
c: c,
|
||||
result: Result{
|
||||
Code: CodeSuccess,
|
||||
Message: getMessage(CodeSuccess),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Error 构造失败响应
|
||||
func Error(c *router.Context) *Builder {
|
||||
return &Builder{
|
||||
c: c,
|
||||
result: Result{
|
||||
Code: CodeFail,
|
||||
Message: getMessage(CodeFail),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Code 设置状态码(自动填充默认提示,除非后面手动改)
|
||||
func (b *Builder) Code(code string) *Builder {
|
||||
b.result.Code = code
|
||||
b.result.Message = getMessage(code)
|
||||
return b
|
||||
}
|
||||
|
||||
// Message 设置提示消息(覆盖默认的)
|
||||
func (b *Builder) Message(msg string) *Builder {
|
||||
b.result.Message = msg
|
||||
return b
|
||||
}
|
||||
|
||||
// Data 设置返回数据
|
||||
func (b *Builder) Data(data interface{}) *Builder {
|
||||
b.result.Data = data
|
||||
return b
|
||||
}
|
||||
|
||||
// ErrorInfo 设置错误信息
|
||||
func (b *Builder) ErrorInfo(err ErrorInfo) *Builder {
|
||||
b.result.Error = err
|
||||
return b
|
||||
}
|
||||
|
||||
// Send 发送响应
|
||||
func (b *Builder) Send() {
|
||||
if b.result.Data == nil {
|
||||
b.result.Data = ""
|
||||
}
|
||||
b.c.JSON(http.StatusOK, b.result)
|
||||
}
|
||||
|
||||
// getMessage 获取状态码对应提示,兜底 defaultMessage
|
||||
func getMessage(code string) string {
|
||||
if msg, ok := codeMessages[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return defaultMessage
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
error2 "base-framework/pkg/error"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// BodyCache 专门负责缓存请求体,保证只读一次
|
||||
type BodyCache struct {
|
||||
once sync.Once
|
||||
Data []byte
|
||||
Err error
|
||||
}
|
||||
|
||||
// Load 读取请求体并缓存,只执行一次
|
||||
func (b *BodyCache) Load(r *http.Request) ([]byte, error) {
|
||||
b.once.Do(func() {
|
||||
if r.Body == nil {
|
||||
b.Err = http.ErrBodyNotAllowed
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
b.Err = func() error {
|
||||
_, err := io.Copy(&buf, r.Body)
|
||||
return err
|
||||
}()
|
||||
if b.Err != nil {
|
||||
return
|
||||
}
|
||||
b.Data = buf.Bytes()
|
||||
r.Body = io.NopCloser(bytes.NewReader(b.Data))
|
||||
})
|
||||
return b.Data, b.Err
|
||||
}
|
||||
|
||||
// Context 自定义请求上下文
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Writer http.ResponseWriter
|
||||
params map[string]string
|
||||
index int
|
||||
handlers []HandlerFunc
|
||||
|
||||
bodyCache BodyCache
|
||||
keys map[string]interface{}
|
||||
|
||||
aborted bool // 是否中止
|
||||
}
|
||||
|
||||
// Next 执行下一个中间件或处理函数
|
||||
func (c *Context) Next() {
|
||||
c.index++
|
||||
for c.index < len(c.handlers) {
|
||||
if c.aborted {
|
||||
break
|
||||
}
|
||||
c.handlers[c.index](c)
|
||||
c.index++
|
||||
}
|
||||
}
|
||||
|
||||
// Abort 中断后续中间件/处理函数
|
||||
func (c *Context) Abort() {
|
||||
c.aborted = true
|
||||
}
|
||||
|
||||
// Param 获取路由参数
|
||||
func (c *Context) Param(key string) string {
|
||||
return c.params[key]
|
||||
}
|
||||
|
||||
// Header 获取请求头
|
||||
func (c *Context) Header(key string) string {
|
||||
return c.Request.Header.Get(key)
|
||||
}
|
||||
|
||||
// JSON 返回 JSON 格式响应
|
||||
func (c *Context) JSON(statusCode int, data interface{}) {
|
||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
c.Writer.WriteHeader(statusCode)
|
||||
if err := json.NewEncoder(c.Writer).Encode(data); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Body 方便读取请求体
|
||||
func (c *Context) Body() ([]byte, error) {
|
||||
return c.bodyCache.Load(c.Request)
|
||||
}
|
||||
|
||||
// BindJSON 反序列化 JSON 请求体到 obj
|
||||
func (c *Context) BindJSON(obj interface{}) error {
|
||||
body, err := c.Body()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, obj)
|
||||
}
|
||||
|
||||
// PostForm 获取表单参数
|
||||
func (c *Context) PostForm(key string) string {
|
||||
if err := c.Request.ParseForm(); err != nil {
|
||||
return ""
|
||||
}
|
||||
return c.Request.FormValue(key)
|
||||
}
|
||||
|
||||
// Set 存储键值对
|
||||
func (c *Context) Set(key string, value interface{}) {
|
||||
if c.keys == nil {
|
||||
c.keys = make(map[string]interface{})
|
||||
}
|
||||
c.keys[key] = value
|
||||
|
||||
}
|
||||
|
||||
// Get 取值
|
||||
func (c *Context) Get(key string) (interface{}, bool) {
|
||||
if c.keys == nil {
|
||||
return nil, false
|
||||
}
|
||||
val, ok := c.keys[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// AddError 记录错误,自动包装 StackError
|
||||
func (c *Context) AddError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.keys == nil {
|
||||
c.keys = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if _, exists := c.keys["errors"]; !exists {
|
||||
c.keys["errors"] = []*error2.StackError{}
|
||||
}
|
||||
|
||||
// 统一生成堆栈
|
||||
se := error2.WrapWithStack(err)
|
||||
c.keys["errors"] = append(c.keys["errors"].([]*error2.StackError), se)
|
||||
}
|
||||
|
||||
// Errors 返回 []*utils.StackError
|
||||
func (c *Context) Errors() []*error2.StackError {
|
||||
if c.keys == nil {
|
||||
return nil
|
||||
}
|
||||
if errs, exists := c.keys["errors"]; exists {
|
||||
return errs.([]*error2.StackError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HandlerFunc 路由和中间件处理函数签名
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
// node 路由树节点
|
||||
type node struct {
|
||||
pattern string
|
||||
part string
|
||||
children []*node
|
||||
isParam bool
|
||||
handler HandlerFunc
|
||||
}
|
||||
|
||||
// 匹配子节点,返回第一个匹配的
|
||||
func (n *node) matchChild(part string) *node {
|
||||
for _, child := range n.children {
|
||||
if child.part == part || child.isParam {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 匹配所有匹配的子节点
|
||||
func (n *node) matchChildren(part string) []*node {
|
||||
nodes := make([]*node, 0)
|
||||
for _, child := range n.children {
|
||||
if child.part == part || child.isParam {
|
||||
nodes = append(nodes, child)
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// 插入路由节点
|
||||
func (n *node) insert(pattern string, parts []string, height int, handler HandlerFunc) {
|
||||
if len(parts) == height {
|
||||
n.pattern = pattern
|
||||
n.handler = handler
|
||||
return
|
||||
}
|
||||
part := parts[height]
|
||||
child := n.matchChild(part)
|
||||
if child == nil {
|
||||
child = &node{
|
||||
part: part,
|
||||
isParam: len(part) > 0 && part[0] == ':',
|
||||
}
|
||||
n.children = append(n.children, child)
|
||||
}
|
||||
child.insert(pattern, parts, height+1, handler)
|
||||
}
|
||||
|
||||
// 搜索路由节点,并收集参数
|
||||
func (n *node) search(parts []string, height int, params map[string]string) *node {
|
||||
if len(parts) == height || n.part == "*" {
|
||||
if n.pattern == "" {
|
||||
return nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
part := parts[height]
|
||||
children := n.matchChildren(part)
|
||||
|
||||
for _, child := range children {
|
||||
if child.isParam {
|
||||
params[child.part[1:]] = part
|
||||
}
|
||||
res := child.search(parts, height+1, params)
|
||||
if res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 中间件条目,包含排除路径集合
|
||||
type middlewareEntry struct {
|
||||
handler HandlerFunc
|
||||
excludePaths map[string]struct{}
|
||||
}
|
||||
|
||||
// Router 路由器结构
|
||||
type Router struct {
|
||||
roots map[string]*node
|
||||
middleware []middlewareEntry
|
||||
basePath string
|
||||
}
|
||||
|
||||
// NewRouter 创建路由器实例
|
||||
func NewRouter() *Router {
|
||||
return &Router{
|
||||
roots: make(map[string]*node),
|
||||
}
|
||||
}
|
||||
|
||||
// MiddlewareHandle 用于链式配置中间件排除路径
|
||||
type MiddlewareHandle struct {
|
||||
router *Router
|
||||
entryIdx int
|
||||
}
|
||||
|
||||
// Use 注册中间件,返回链式配置句柄
|
||||
func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle {
|
||||
entry := middlewareEntry{
|
||||
handler: handler,
|
||||
excludePaths: nil,
|
||||
}
|
||||
r.middleware = append(r.middleware, entry)
|
||||
return &MiddlewareHandle{
|
||||
router: r,
|
||||
entryIdx: len(r.middleware) - 1,
|
||||
}
|
||||
}
|
||||
|
||||
// ExcludePaths 设置排除路径
|
||||
func (mh *MiddlewareHandle) ExcludePaths(paths ...string) *Router {
|
||||
excludeMap := make(map[string]struct{}, len(paths))
|
||||
for _, p := range paths {
|
||||
excludeMap[p] = struct{}{}
|
||||
}
|
||||
mh.router.middleware[mh.entryIdx].excludePaths = excludeMap
|
||||
return mh.router
|
||||
}
|
||||
|
||||
// Group 路由分组,返回新的 Router,继承根节点和中间件,basePath拼接
|
||||
func (r *Router) Group(prefix string, m ...HandlerFunc) *Router {
|
||||
// 复制原middleware
|
||||
newMiddleware := make([]middlewareEntry, len(r.middleware))
|
||||
copy(newMiddleware, r.middleware)
|
||||
|
||||
// 将新增的 HandlerFunc 转成 middlewareEntry
|
||||
for _, handler := range m {
|
||||
entry := middlewareEntry{
|
||||
handler: handler,
|
||||
excludePaths: nil,
|
||||
}
|
||||
newMiddleware = append(newMiddleware, entry)
|
||||
}
|
||||
|
||||
return &Router{
|
||||
roots: r.roots,
|
||||
middleware: newMiddleware,
|
||||
basePath: joinPaths(r.basePath, prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数,提取middlewareEntry中的handler为HandlerFunc slice
|
||||
func (r *Router) middlewareHandlers() []HandlerFunc {
|
||||
handlers := make([]HandlerFunc, 0, len(r.middleware))
|
||||
for _, m := range r.middleware {
|
||||
handlers = append(handlers, m.handler)
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
// GET 注册GET请求路由
|
||||
func (r *Router) GET(path string, handler HandlerFunc) *Router {
|
||||
return r.handle("GET", path, handler)
|
||||
}
|
||||
|
||||
// POST 注册POST请求路由
|
||||
func (r *Router) POST(path string, handler HandlerFunc) *Router {
|
||||
return r.handle("POST", path, handler)
|
||||
}
|
||||
|
||||
// handle 注册具体方法和路径的处理函数
|
||||
func (r *Router) handle(method, path string, handler HandlerFunc) *Router {
|
||||
fullPath := joinPaths(r.basePath, path)
|
||||
parts := parsePattern(fullPath)
|
||||
if r.roots[method] == nil {
|
||||
r.roots[method] = &node{}
|
||||
}
|
||||
r.roots[method].insert(fullPath, parts, 0, handler)
|
||||
return r
|
||||
}
|
||||
|
||||
// ServeHTTP 实现 http.Handler,执行路由匹配和中间件
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
root := r.roots[req.Method]
|
||||
if root == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
searchParts := parsePattern(req.URL.Path)
|
||||
params := make(map[string]string)
|
||||
n := root.search(searchParts, 0, params)
|
||||
if n == nil || n.handler == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// 按排除路径过滤中间件
|
||||
handlers := make([]HandlerFunc, 0)
|
||||
for _, m := range r.middleware {
|
||||
if m.excludePaths != nil {
|
||||
if _, excluded := m.excludePaths[req.URL.Path]; excluded {
|
||||
continue
|
||||
}
|
||||
}
|
||||
handlers = append(handlers, m.handler)
|
||||
}
|
||||
handlers = append(handlers, n.handler)
|
||||
|
||||
c := &Context{
|
||||
Writer: w,
|
||||
Request: req,
|
||||
params: params,
|
||||
index: -1,
|
||||
handlers: handlers,
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// 解析路径,去除空字符串
|
||||
func parsePattern(pattern string) []string {
|
||||
vs := strings.Split(strings.Trim(pattern, "/"), "/")
|
||||
parts := make([]string, 0, len(vs))
|
||||
for _, item := range vs {
|
||||
if item != "" {
|
||||
parts = append(parts, item)
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// 拼接两个路径字符串,保证中间只有一个 '/'
|
||||
func joinPaths(a, b string) string {
|
||||
if a == "" {
|
||||
if b == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(b, "/") {
|
||||
return "/" + b
|
||||
}
|
||||
return b
|
||||
}
|
||||
if b == "" {
|
||||
return a
|
||||
}
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
default:
|
||||
return a + b
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package routinex
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ------------------- goroutine id -------------------
|
||||
func CurGID() uint64 {
|
||||
var buf [64]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
line := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
|
||||
gid, _ := strconv.ParseUint(line, 10, 64)
|
||||
return gid
|
||||
}
|
||||
|
||||
type RoutineLocal struct {
|
||||
data sync.Map // gid -> map[key]any
|
||||
}
|
||||
|
||||
var (
|
||||
instance *RoutineLocal
|
||||
instanceOnce sync.Once
|
||||
)
|
||||
|
||||
func GetInstance() *RoutineLocal {
|
||||
instanceOnce.Do(func() {
|
||||
instance = &RoutineLocal{}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// Set 存储单条数据
|
||||
func Set(key string, value any) {
|
||||
gid := CurGID()
|
||||
v, _ := GetInstance().data.LoadOrStore(gid, &sync.Map{})
|
||||
m := v.(*sync.Map)
|
||||
m.Store(key, value)
|
||||
}
|
||||
|
||||
// Get 获取单条数据
|
||||
func Get(key string) any {
|
||||
gid := CurGID()
|
||||
if v, ok := GetInstance().data.Load(gid); ok {
|
||||
m := v.(*sync.Map)
|
||||
if val, ok := m.Load(key); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear 删除当前 goroutine 所有数据
|
||||
func Clear() {
|
||||
GetInstance().data.Delete(CurGID())
|
||||
}
|
||||
|
||||
// Go 封装函数,自动继承父 goroutine 数据
|
||||
func Go(f func()) {
|
||||
parentGID := CurGID()
|
||||
parentData, _ := GetInstance().data.Load(parentGID)
|
||||
|
||||
go func() {
|
||||
if parentData != nil {
|
||||
GetInstance().data.Store(CurGID(), parentData)
|
||||
defer Clear()
|
||||
}
|
||||
f()
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// Server 服务器配置结构体(包内私有)
|
||||
type Server struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Jwt 配置结构体(包内私有)
|
||||
type Jwt struct {
|
||||
Secret string
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Dir string // 日志根目录
|
||||
Level string // 全局最低日志级别
|
||||
Format string // 输出格式,可选 json 或 console
|
||||
Daily bool // 是否按天滚动
|
||||
MaxDays int // 保留天数,0表示无限
|
||||
Caller bool // 是否记录调用文件行号
|
||||
LevelDirs bool // 是否按级别目录
|
||||
Console bool
|
||||
}
|
||||
|
||||
// DBConfig 数据库配置结构体
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Dbname string
|
||||
MaxOpenConns int // 最大打开连接数
|
||||
MaxIdleConns int // 最大空闲连接数
|
||||
ConnMaxLifetime time.Duration // 连接最大生命周期
|
||||
}
|
||||
|
||||
type Snowflake struct {
|
||||
DatacenterID uint64
|
||||
WorkerID uint64
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"base-framework/pkg/config"
|
||||
"base-framework/pkg/router"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *sql.DB
|
||||
tx *sql.Tx
|
||||
}
|
||||
|
||||
// JdbcTemplate 创建 JdbcTemplate
|
||||
func JdbcTemplate(c *router.Context) (*Client, error) {
|
||||
val, ok := c.Get("orgId")
|
||||
if !ok {
|
||||
return nil, errors.New("missing orgId in context")
|
||||
}
|
||||
orgId, ok := val.(string)
|
||||
if !ok || orgId == "" {
|
||||
return nil, errors.New("invalid orgId in context")
|
||||
}
|
||||
conn, ok := config.GetDB(orgId)
|
||||
if !ok || conn == nil {
|
||||
return nil, fmt.Errorf("no db connection found for orgId=%s", orgId)
|
||||
}
|
||||
return &Client{conn: conn}, nil
|
||||
}
|
||||
|
||||
// ------------------------ 内部方法 ------------------------
|
||||
|
||||
// 执行查询,返回 *sql.Rows
|
||||
func (c *Client) query(query string, args ...any) (*sql.Rows, error) {
|
||||
if c.tx != nil {
|
||||
return c.tx.Query(query, args...)
|
||||
}
|
||||
return c.conn.Query(query, args...)
|
||||
}
|
||||
|
||||
// 执行执行类语句(insert/update/delete)
|
||||
func (c *Client) exec(query string, args ...any) (sql.Result, error) {
|
||||
if c.tx != nil {
|
||||
return c.tx.Exec(query, args...)
|
||||
}
|
||||
return c.conn.Exec(query, args...)
|
||||
}
|
||||
|
||||
// WithTransaction 自动处理事务提交或回滚
|
||||
func (c *Client) WithTransaction(fn func(txClient *Client) error) error {
|
||||
if c.tx != nil {
|
||||
// 已经在事务中,直接执行
|
||||
return fn(c)
|
||||
}
|
||||
|
||||
tx, err := c.conn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txClient := &Client{conn: c.conn, tx: tx}
|
||||
if err := fn(txClient); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func (c *Client) Delete(query string, args ...any) (sql.Result, error) { return c.exec(query, args...) }
|
||||
@@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func (c *Client) Insert(query string, args ...any) (sql.Result, error) { return c.exec(query, args...) }
|
||||
|
||||
// BatchInsert 批量插入,传入多组参数
|
||||
func (c *Client) BatchInsert(query string, params [][]any) error {
|
||||
for _, args := range params {
|
||||
if _, err := c.Insert(query, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func (c *Client) Select(query string, args ...any) ([]map[string]any, error) {
|
||||
rows, err := c.query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(rows *sql.Rows) {
|
||||
_ = rows.Close()
|
||||
}(rows)
|
||||
|
||||
cols, _ := rows.Columns()
|
||||
var result []map[string]any
|
||||
|
||||
for rows.Next() {
|
||||
columns := make([]any, len(cols))
|
||||
columnPointers := make([]any, len(cols))
|
||||
for i := range columns {
|
||||
columnPointers[i] = &columns[i]
|
||||
}
|
||||
if err := rows.Scan(columnPointers...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rowMap := make(map[string]any)
|
||||
for i, colName := range cols {
|
||||
rowMap[colName] = columns[i]
|
||||
}
|
||||
result = append(result, rowMap)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) SelectOne(query string, args ...any) (map[string]any, error) {
|
||||
rows, err := c.query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(rows *sql.Rows) {
|
||||
_ = rows.Close()
|
||||
}(rows)
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
cols, _ := rows.Columns()
|
||||
columns := make([]any, len(cols))
|
||||
columnPointers := make([]any, len(cols))
|
||||
for i := range columns {
|
||||
columnPointers[i] = &columns[i]
|
||||
}
|
||||
if err := rows.Scan(columnPointers...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rowMap := make(map[string]any)
|
||||
for i, colName := range cols {
|
||||
rowMap[colName] = columns[i]
|
||||
}
|
||||
return rowMap, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
|
||||
func (c *Client) Update(query string, args ...any) (sql.Result, error) { return c.exec(query, args...) }
|
||||
@@ -0,0 +1,70 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
commonConfig "base-framework/pkg/config"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenExpired = errors.New("token expired")
|
||||
ErrTokenInvalid = errors.New("token invalid")
|
||||
)
|
||||
|
||||
// CustomClaims 定义自己的 payload 结构
|
||||
type CustomClaims struct {
|
||||
OrgID string `json:"org_id"`
|
||||
UserID string `json:"user_id"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// CreateToken 创建一个 JWT token
|
||||
func CreateToken(orgID, userID string) (string, error) {
|
||||
expireAt := time.Now().Add(commonConfig.JWT.Expiry)
|
||||
fmt.Println("Token 过期时间:", expireAt.Format(time.RFC3339))
|
||||
|
||||
claims := CustomClaims{
|
||||
OrgID: orgID,
|
||||
UserID: userID,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: expireAt.Unix(),
|
||||
IssuedAt: time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(commonConfig.JWT.Secret))
|
||||
}
|
||||
|
||||
// VerifyToken 验证并解析 JWT token
|
||||
func VerifyToken(tokenString string) (*CustomClaims, error) {
|
||||
signKey := []byte(commonConfig.JWT.Secret)
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// 校验签名算法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
return signKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// 判断是否过期错误
|
||||
var ve *jwt.ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
if ve.Errors&jwt.ValidationErrorExpired != 0 {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
}
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package strutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ToString 通用高效转换为 string
|
||||
func ToString(v any) string {
|
||||
switch n := v.(type) {
|
||||
// 整数类型
|
||||
case int:
|
||||
return strconv.FormatInt(int64(n), 10)
|
||||
case int8:
|
||||
return strconv.FormatInt(int64(n), 10)
|
||||
case int16:
|
||||
return strconv.FormatInt(int64(n), 10)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(n), 10)
|
||||
case int64:
|
||||
return strconv.FormatInt(n, 10)
|
||||
|
||||
// 无符号整数
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(n), 10)
|
||||
case uint8:
|
||||
return strconv.FormatUint(uint64(n), 10)
|
||||
case uint16:
|
||||
return strconv.FormatUint(uint64(n), 10)
|
||||
case uint32:
|
||||
return strconv.FormatUint(uint64(n), 10)
|
||||
case uint64:
|
||||
return strconv.FormatUint(n, 10)
|
||||
|
||||
// 浮点数
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(n), 'f', -1, 32)
|
||||
case float64:
|
||||
return strconv.FormatFloat(n, 'f', -1, 64)
|
||||
|
||||
// 字符串
|
||||
case string:
|
||||
return n
|
||||
|
||||
// 布尔
|
||||
case bool:
|
||||
return strconv.FormatBool(n)
|
||||
|
||||
default:
|
||||
// fallback
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package uid
|
||||
|
||||
import (
|
||||
"base-framework/pkg/config"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
datacenterBits = uint64(5)
|
||||
workerBits = uint64(5)
|
||||
sequenceBits = uint64(12)
|
||||
|
||||
maxSequence = -1 ^ (-1 << sequenceBits)
|
||||
workerShift = sequenceBits
|
||||
datacenterShift = sequenceBits + workerBits
|
||||
timestampShift = sequenceBits + workerBits + datacenterBits
|
||||
|
||||
epoch = int64(1672531200000) // 2023-01-01
|
||||
)
|
||||
|
||||
type Snowflake struct {
|
||||
lastStamp uint64 // 上一次时间戳 (毫秒)
|
||||
sequence uint64 // 序列号
|
||||
dcID uint64
|
||||
workerID uint64
|
||||
}
|
||||
|
||||
var (
|
||||
sfInstance *Snowflake
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// lazy 初始化实例
|
||||
func initInstance() {
|
||||
var dcID, wID uint64
|
||||
if config.Snowflake != nil {
|
||||
dcID = config.Snowflake.DatacenterID
|
||||
wID = config.Snowflake.WorkerID
|
||||
}
|
||||
sfInstance = &Snowflake{
|
||||
dcID: dcID,
|
||||
workerID: wID,
|
||||
}
|
||||
}
|
||||
|
||||
// NextID 生成全局唯一 ID,无锁版本
|
||||
func NextID() uint64 {
|
||||
once.Do(initInstance)
|
||||
|
||||
for {
|
||||
now := uint64(time.Now().UnixNano() / 1e6)
|
||||
|
||||
last := atomic.LoadUint64(&sfInstance.lastStamp)
|
||||
seq := atomic.LoadUint64(&sfInstance.sequence)
|
||||
|
||||
if now < last {
|
||||
// 系统时钟回拨,等待
|
||||
now = waitNextMillis(last)
|
||||
}
|
||||
|
||||
if now == last {
|
||||
seq = (seq + 1) & uint64(maxSequence)
|
||||
if seq == 0 {
|
||||
now = waitNextMillis(last)
|
||||
}
|
||||
} else {
|
||||
seq = 0
|
||||
}
|
||||
|
||||
if atomic.CompareAndSwapUint64(&sfInstance.lastStamp, last, now) {
|
||||
atomic.StoreUint64(&sfInstance.sequence, seq)
|
||||
id := ((now - uint64(epoch)) << timestampShift) |
|
||||
(sfInstance.dcID << datacenterShift) |
|
||||
(sfInstance.workerID << workerShift) |
|
||||
seq
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待下一个毫秒
|
||||
func waitNextMillis(last uint64) uint64 {
|
||||
now := uint64(time.Now().UnixNano() / 1e6)
|
||||
for now <= last {
|
||||
now = uint64(time.Now().UnixNano() / 1e6)
|
||||
}
|
||||
return now
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
@@ -0,0 +1,10 @@
|
||||
// eslint.config.mjs
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu({
|
||||
stylistic: {
|
||||
indent: 2, // 4, or 'tab'
|
||||
quotes: 'single', // or 'double'
|
||||
},
|
||||
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "base-vue",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^5.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.34.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
}
|
||||
+3571
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo">
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo">
|
||||
</a>
|
||||
</div>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": ["vite/client", "node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx", "./**/*.vue"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Reference in new issue
Block a user