From a5c2e7274034f8f4e8926f03851e3e81daded717 Mon Sep 17 00:00:00 2001 From: samwaf Date: Tue, 12 May 2026 17:00:04 +0800 Subject: [PATCH] feat:add mysql --- customtype/mytime.go | 4 +- global/global.go | 16 ++ go.mod | 5 +- go.sum | 8 + innerbean/web_log.go | 94 +++++----- model/IPTag.go | 8 +- model/account.go | 26 +-- model/allowlist.go | 14 +- model/anticc.go | 16 +- model/base_config.go | 7 - model/baseorm/baseorm.go | 10 +- model/batchtask.go | 18 +- model/blocking_page.go | 14 +- model/blocklist.go | 14 +- model/ca_server_info.go | 6 +- model/cache_rule.go | 18 +- model/center.go | 24 +-- model/data_retention_policy.go | 24 +-- model/delaymsg.go | 6 +- model/firewall_ip_block.go | 14 +- model/hosts.go | 84 ++++----- model/http_auth_base_config.go | 6 +- model/ldp.go | 8 +- model/loadbalance.go | 10 +- model/notify_channel.go | 16 +- model/notify_log.go | 20 +- model/notify_subscription.go | 12 +- model/onekeymod.go | 12 +- model/oplatform_key.go | 18 +- model/oplatform_log.go | 20 +- model/otp.go | 10 +- model/private_group.go | 4 +- model/private_info.go | 10 +- model/rules.go | 20 +- model/sensitive.go | 8 +- model/sharedb.go | 10 +- model/sslconfig.go | 20 +- model/sslexpire.go | 12 +- model/sslorder.go | 40 ++-- model/stats.go | 74 ++++---- model/system_config.go | 16 +- model/task.go | 12 +- model/tunnel.go | 42 ++--- model/waf_sys.go | 4 +- wafconfig/wafconfig.go | 68 ++++++- wafdb/dialect/mysql_dialect.go | 169 +++++++++++++++++ wafdb/localdb.go | 13 ++ wafdb/migrations_core.go | 44 +++-- wafdb/migrations_helper.go | 42 +++++ wafdb/migrations_log.go | 49 ++++- wafdb/migrations_stats.go | 121 ++++++------ wafdb/mysql_localdb.go | 325 +++++++++++++++++++++++++++++++++ 52 files changed, 1189 insertions(+), 476 deletions(-) delete mode 100644 model/base_config.go create mode 100644 wafdb/dialect/mysql_dialect.go create mode 100644 wafdb/migrations_helper.go create mode 100644 wafdb/mysql_localdb.go diff --git a/customtype/mytime.go b/customtype/mytime.go index 3ab6331..ed4b6d8 100644 --- a/customtype/mytime.go +++ b/customtype/mytime.go @@ -32,7 +32,9 @@ func (t *JsonTime) UnmarshalJSON(data []byte) error { func (t JsonTime) Value() (driver.Value, error) { tm := time.Time(t) - //return tm.Format(constant.TimeLayout), nil + if tm.IsZero() { + return nil, nil // MySQL strict mode rejects 0000-00-00; use NULL for unset times + } return tm, nil } diff --git a/global/global.go b/global/global.go index f7cdf92..e7dca27 100644 --- a/global/global.go +++ b/global/global.go @@ -186,6 +186,22 @@ var ( GWAF_PWD_STATDB = "3Y)(27EtO^tK8Bj~STAT" //加密 GWAF_PWD_LOGDB = "3Y)(27EtO^tK8Bj~LOG" //加密 + // Database driver: "sqlite" | "mysql" | "sqlserver" (default: sqlite) + GWAF_DB_DRIVER string = "sqlite" + + // MySQL connection config (populated from conf/config.yml database.mysql.*) + GWAF_MYSQL_HOST string = "127.0.0.1" + GWAF_MYSQL_PORT int = 3306 + GWAF_MYSQL_USER string = "samwaf" + GWAF_MYSQL_PASSWORD string = "" + GWAF_MYSQL_CHARSET string = "utf8mb4" + GWAF_MYSQL_CORE_DB string = "samwaf_core" + GWAF_MYSQL_LOG_DB string = "samwaf_log" + GWAF_MYSQL_STATS_DB string = "samwaf_stats" + GWAF_MYSQL_MAX_OPEN_CONNS int = 50 + GWAF_MYSQL_MAX_IDLE_CONNS int = 10 + GWAF_MYSQL_CONN_MAX_LIFETIME_MINUTES int = 60 + //默认创建的账户和密码 GWAF_DEFAULT_ACCOUNT string = "admin" //默认创建的账户 GWAF_DEFAULT_ACCOUNT_PWD string = "admin868" //默认创建的密码 diff --git a/go.mod b/go.mod index a50dbe2..837fadf 100644 --- a/go.mod +++ b/go.mod @@ -51,11 +51,12 @@ require ( google.golang.org/grpc v1.77.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 - gorm.io/gorm v1.26.1 + gorm.io/gorm v1.30.0 ) require ( dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect @@ -93,6 +94,7 @@ require ( 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.28.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect @@ -165,6 +167,7 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.6.0 // indirect rsc.io/binaryregexp v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 568560e..2a3f2e0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -177,6 +179,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -674,8 +678,12 @@ 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.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= diff --git a/innerbean/web_log.go b/innerbean/web_log.go index 04b7489..11ffc44 100644 --- a/innerbean/web_log.go +++ b/innerbean/web_log.go @@ -3,55 +3,55 @@ package innerbean import "strings" type WebLog struct { - WafInnerDFlag string `json:"waf_inner_dflag"` //日志队列处理方式 - HOST string `json:"host"` - URL string `json:"url"` - RawQuery string `json:"raw_query"` //原始URL查询 - REFERER string `json:"referer"` - USER_AGENT string `json:"user_agent"` - METHOD string `json:"method"` - HEADER string `json:"header"` - SRC_IP string `json:"src_ip"` - SRC_PORT string `json:"src_port"` - COUNTRY string `json:"country"` - PROVINCE string `json:"province"` - CITY string `json:"city"` - CREATE_TIME string `gorm:"index:idx_weblog_time" json:"create_time"` + WafInnerDFlag string `gorm:"size:10" json:"waf_inner_dflag"` //日志队列处理方式 + HOST string `gorm:"size:255" json:"host"` + URL string `gorm:"type:text" json:"url"` + RawQuery string `gorm:"type:text" json:"raw_query"` //原始URL查询 + REFERER string `gorm:"type:text" json:"referer"` + USER_AGENT string `gorm:"size:500" json:"user_agent"` + METHOD string `gorm:"size:20" json:"method"` + HEADER string `gorm:"type:text" json:"header"` + SRC_IP string `gorm:"size:64" json:"src_ip"` + SRC_PORT string `gorm:"size:10" json:"src_port"` + COUNTRY string `gorm:"size:100" json:"country"` + PROVINCE string `gorm:"size:100" json:"province"` + CITY string `gorm:"size:100" json:"city"` + CREATE_TIME string `gorm:"size:32;index:idx_weblog_time" json:"create_time"` CONTENT_LENGTH int64 `json:"content_length"` RES_CONTENT_LENGTH int64 `json:"res_content_length"` //响应内容大小(字节) - COOKIES string `json:"cookies"` - BODY string `json:"body"` - REQ_UUID string `json:"req_uuid"` - USER_CODE string `json:"user_code" gorm:"index"` - TenantId string `json:"tenant_id" gorm:"index"` //租户ID(主要键) - HOST_CODE string `json:"host_code" ` //主机ID (主要键) - Day int `json:"day"` //日 (主要键) - ACTION string `json:"action"` - RULE string `json:"rule"` - STATUS string `json:"status"` //状态 - STATUS_CODE int `json:"status_code"` //状态编码 - RES_BODY string `json:"res_body"` //返回信息 - POST_FORM string `json:"post_form"` //提交的表单数据 - TASK_FLAG int `json:"task_flag" gorm:"default:-1;index"` //任务处理标记 -1 等待处理;1 可以进行处理;2 处理完毕 - UNIX_ADD_TIME int64 `json:"unix_add_time" gorm:"index"` //添加日期unix - RISK_LEVEL int `json:"risk_level"` //危险等级 0:正常 1:轻微 2:有害 3:严重 4:特别严重 - GUEST_IDENTIFICATION string `json:"guest_identification"` //访客身份识别 - IsBot int `json:"is_bot"` //是否是机器人 0 不是机器人 1 机器人 - TimeSpent int64 `json:"time_spent"` //用时 - NetSrcIp string `json:"net_src_ip"` //获取的原始IP - SrcByteBody []byte `json:"src_byte_body"` //原始body信息 - SrcByteResBody []byte `json:"src_byte_res_body"` //返回body bytes信息 - WebLogVersion int `json:"web_log_version"` //日志版本信息早期的是空和0,后期实时增加 - Scheme string `json:"scheme"` //HTTP 协议 - SrcURL []byte `json:"src_url"` //原始url信息 - PreCheckCost int64 `json:"pre_check_cost"` // 前置检查耗时(ms) - ForwardCost int64 `json:"forward_cost"` // 转发耗时(ms) - BackendCheckCost int64 `json:"backend_check_cost"` // 后端处理耗时(ms) - ResHeader string `json:"res_header"` // 返回header情况 - BodyHash string `json:"body_hash"` // body hash值 - LogOnlyMode int `json:"log_only_mode"` //是否只记录日志 1 是 0 不是 - IsBalance int `json:"is_balance"` //是否是负载均衡 1 是 0 不是 - BalanceInfo string `json:"balance_info"` //负载均衡IP端口信息 + COOKIES string `gorm:"type:text" json:"cookies"` + BODY string `gorm:"type:text" json:"body"` + REQ_UUID string `gorm:"size:64" json:"req_uuid"` + USER_CODE string `gorm:"size:64;index" json:"user_code"` + TenantId string `gorm:"size:64;index" json:"tenant_id"` //租户ID(主要键) + HOST_CODE string `gorm:"size:64" json:"host_code"` //主机ID (主要键) + Day int `json:"day"` //日 (主要键) + ACTION string `gorm:"size:100" json:"action"` + RULE string `gorm:"type:text" json:"rule"` + STATUS string `gorm:"size:50" json:"status"` //状态 + STATUS_CODE int `json:"status_code"` //状态编码 + RES_BODY string `gorm:"type:text" json:"res_body"` //返回信息 + POST_FORM string `gorm:"type:text" json:"post_form"` //提交的表单数据 + TASK_FLAG int `json:"task_flag" gorm:"default:-1;index"` //任务处理标记 -1 等待处理;1 可以进行处理;2 处理完毕 + UNIX_ADD_TIME int64 `json:"unix_add_time" gorm:"index"` //添加日期unix + RISK_LEVEL int `json:"risk_level"` //危险等级 0:正常 1:轻微 2:有害 3:严重 4:特别严重 + GUEST_IDENTIFICATION string `gorm:"column:guest_identification;size:191" json:"guest_identification"` //访客身份识别 + IsBot int `json:"is_bot"` //是否是机器人 0 不是机器人 1 机器人 + TimeSpent int64 `json:"time_spent"` //用时 + NetSrcIp string `gorm:"size:64" json:"net_src_ip"` //获取的原始IP + SrcByteBody []byte `json:"src_byte_body"` //原始body信息 + SrcByteResBody []byte `json:"src_byte_res_body"` //返回body bytes信息 + WebLogVersion int `json:"web_log_version"` //日志版本信息早期的是空和0,后期实时增加 + Scheme string `gorm:"size:20" json:"scheme"` //HTTP 协议 + SrcURL []byte `json:"src_url"` //原始url信息 + PreCheckCost int64 `json:"pre_check_cost"` // 前置检查耗时(ms) + ForwardCost int64 `json:"forward_cost"` // 转发耗时(ms) + BackendCheckCost int64 `json:"backend_check_cost"` // 后端处理耗时(ms) + ResHeader string `gorm:"type:text" json:"res_header"` // 返回header情况 + BodyHash string `gorm:"size:100" json:"body_hash"` // body hash值 + LogOnlyMode int `json:"log_only_mode"` //是否只记录日志 1 是 0 不是 + IsBalance int `json:"is_balance"` //是否是负载均衡 1 是 0 不是 + BalanceInfo string `gorm:"size:255" json:"balance_info"` //负载均衡IP端口信息 } // GetHeaderValue 从HEADER字段中提取指定header的值 diff --git a/model/IPTag.go b/model/IPTag.go index 04e2822..f62241f 100644 --- a/model/IPTag.go +++ b/model/IPTag.go @@ -8,10 +8,10 @@ IP Tag 信息 */ type IPTag struct { baseorm.BaseOrm - IP string `json:"ip"` //ip - IPTag string `json:"ip_tag"` - Cnt int64 `json:"cnt"` //触发次数 - Remarks string `json:"remarks"` //备注 + IP string `gorm:"size:64" json:"ip"` //ip + IPTag string `gorm:"size:255" json:"ip_tag"` + Cnt int64 `json:"cnt"` //触发次数 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } type AttackIPTag struct { TenantID string `json:"tenant_id"` diff --git a/model/account.go b/model/account.go index d0969d4..50c1091 100644 --- a/model/account.go +++ b/model/account.go @@ -6,24 +6,24 @@ import ( type Account struct { baseorm.BaseOrm - LoginAccount string `json:"login_account"` //登录账号 - LoginPassword string `json:"login_password"` //密码md5加密 - Role string `json:"role"` //登录角色 - Status int `json:"status"` //状态 - Remarks string `json:"remarks"` //备注 + LoginAccount string `gorm:"size:100" json:"login_account"` //登录账号 + LoginPassword string `gorm:"size:255" json:"login_password"` //密码md5加密 + Role string `gorm:"size:50" json:"role"` //登录角色 + Status int `json:"status"` //状态 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } type AccountLog struct { baseorm.BaseOrm - LoginAccount string `json:"login_account"` //登录账号 - OpType string `json:"op_type"` //操作类型 - OpContent string `json:"op_content"` //操作内容 + LoginAccount string `gorm:"size:100" json:"login_account"` //登录账号 + OpType string `gorm:"size:100" json:"op_type"` //操作类型 + OpContent string `gorm:"type:text" json:"op_content"` //操作内容 } type TokenInfo struct { baseorm.BaseOrm - LoginAccount string `json:"login_account"` //登录账号 - LoginIp string `json:"login_ip"` //登录IP - AccessToken string `json:"access_token" crypto:"aes"` //访问码 - DeviceFingerprint string `json:"device_fingerprint"` //设备指纹 - LoginType string `json:"login_type"` //登录类型 web/mobile + LoginAccount string `gorm:"size:100" json:"login_account"` //登录账号 + LoginIp string `gorm:"size:64" json:"login_ip"` //登录IP + AccessToken string `gorm:"type:text" json:"access_token" crypto:"aes"` //访问码 + DeviceFingerprint string `gorm:"size:255" json:"device_fingerprint"` //设备指纹 + LoginType string `gorm:"size:50" json:"login_type"` //登录类型 web/mobile } diff --git a/model/allowlist.go b/model/allowlist.go index ae5b6ca..50b8480 100644 --- a/model/allowlist.go +++ b/model/allowlist.go @@ -6,14 +6,14 @@ import ( type IPAllowList struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Ip string `json:"ip"` //白名单ip - Remarks string `json:"remarks"` //备注 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Ip string `gorm:"size:64" json:"ip"` //白名单ip + Remarks string `gorm:"size:500" json:"remarks"` //备注 } type URLAllowList struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - CompareType string `json:"compare_type"` //判断类型,包含、开始、结束、完全匹配 - Url string `json:"url"` //请求地址 - Remarks string `json:"remarks"` //备注 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + CompareType string `gorm:"size:50" json:"compare_type"` //判断类型,包含、开始、结束、完全匹配 + Url string `gorm:"type:text" json:"url"` //请求地址 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/anticc.go b/model/anticc.go index 30106d2..3b5551e 100644 --- a/model/anticc.go +++ b/model/anticc.go @@ -10,15 +10,15 @@ import "SamWaf/model/baseorm" type AntiCC struct { baseorm.BaseOrm - HostCode string `json:"host_code" gorm:"column:host_code"` + HostCode string `json:"host_code" gorm:"column:host_code;size:64"` Rate int `json:"rate" gorm:"column:rate"` Limit int `json:"limit" gorm:"column:limit"` LockIPMinutes int `json:"lock_ip_minutes" gorm:"column:lock_ip_minutes"` - LimitMode string `json:"limit_mode" gorm:"column:limit_mode"` // "rate" 或 "window" - IPMode string `json:"ip_mode" gorm:"column:ip_mode"` // "nic" 网卡模式 或 "proxy" 代理模式 - Url string `json:"url"` - IsEnableRule bool `json:"is_enable_rule" gorm:"column:is_enable_rule"` //是否启动规则 - RuleContent string `json:"rule_content" gorm:"column:rule_content"` //规则内容 - SkipGlobalCC bool `json:"skip_global_cc" gorm:"column:skip_global_cc"` //命中局部CC规则后跳过全局CC检测 - Remarks string `json:"remarks"` //备注 + LimitMode string `json:"limit_mode" gorm:"column:limit_mode;size:20"` // "rate" 或 "window" + IPMode string `json:"ip_mode" gorm:"column:ip_mode;size:20"` // "nic" 网卡模式 或 "proxy" 代理模式 + Url string `gorm:"type:text" json:"url"` + IsEnableRule bool `json:"is_enable_rule" gorm:"column:is_enable_rule"` //是否启动规则 + RuleContent string `json:"rule_content" gorm:"column:rule_content;type:text"` //规则内容 + SkipGlobalCC bool `json:"skip_global_cc" gorm:"column:skip_global_cc"` //命中局部CC规则后跳过全局CC检测 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/base_config.go b/model/base_config.go deleted file mode 100644 index 1b923fb..0000000 --- a/model/base_config.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type Base_config struct { - Id int `gorm:"primary_key" json:" - "` // - Name string `json:"name"` // - Value string `json:"value"` // -} diff --git a/model/baseorm/baseorm.go b/model/baseorm/baseorm.go index af1624a..c6b58e7 100644 --- a/model/baseorm/baseorm.go +++ b/model/baseorm/baseorm.go @@ -7,9 +7,9 @@ import "SamWaf/customtype" base orm */ type BaseOrm struct { - Id string `gorm:"primary_key" json:"id"` // - USER_CODE string `json:"user_code"` // 用户码(主要键) - Tenant_ID string `json:"tenant_id"` //租户ID - CREATE_TIME customtype.JsonTime `json:"create_time"` //创建时间 - UPDATE_TIME customtype.JsonTime `json:"update_time"` //更新时间 + Id string `gorm:"primaryKey;size:64" json:"id"` // + USER_CODE string `gorm:"size:64" json:"user_code"` // 用户码(主要键) + Tenant_ID string `gorm:"size:64" json:"tenant_id"` //租户ID + CREATE_TIME customtype.JsonTime `json:"create_time"` //创建时间 + UPDATE_TIME customtype.JsonTime `json:"update_time"` //更新时间 } diff --git a/model/batchtask.go b/model/batchtask.go index 7b8fdb5..8139133 100644 --- a/model/batchtask.go +++ b/model/batchtask.go @@ -5,13 +5,13 @@ import "SamWaf/model/baseorm" // BatchTask 批量任务 type BatchTask struct { baseorm.BaseOrm - BatchTaskName string `json:"batch_task_name"` //任务名 - BatchType string `json:"batch_type"` //任务类型 - BatchHostCode string `json:"batch_host_code"` //网站唯一码 是否绑定到某个主机上 - BatchSourceType string `json:"batch_source_type"` //来源类型(local,url) - BatchTriggerType string `json:"batch_trigger_type"` //触发类型 定时任务 cron ,手动任务 manual - BatchSource string `json:"batch_source"` //来源内容 路径或者实际的url内容 - BatchExecuteMethod string `json:"batch_execute_method"` //任务执行方式 追加,覆盖 - BatchExtraConfig string `json:"batch_extra_config"` //额外配置字段(JSON字符串) - Remark string `json:"remark"` //备注 + BatchTaskName string `gorm:"size:255" json:"batch_task_name"` //任务名 + BatchType string `gorm:"size:50" json:"batch_type"` //任务类型 + BatchHostCode string `gorm:"size:64" json:"batch_host_code"` //网站唯一码 是否绑定到某个主机上 + BatchSourceType string `gorm:"size:50" json:"batch_source_type"` //来源类型(local,url) + BatchTriggerType string `gorm:"size:50" json:"batch_trigger_type"` //触发类型 定时任务 cron ,手动任务 manual + BatchSource string `gorm:"type:text" json:"batch_source"` //来源内容 路径或者实际的url内容 + BatchExecuteMethod string `gorm:"size:100" json:"batch_execute_method"` //任务执行方式 追加,覆盖 + BatchExtraConfig string `gorm:"type:text" json:"batch_extra_config"` //额外配置字段(JSON字符串) + Remark string `gorm:"size:500" json:"remark"` //备注 } diff --git a/model/blocking_page.go b/model/blocking_page.go index d79c293..9ca3f7d 100644 --- a/model/blocking_page.go +++ b/model/blocking_page.go @@ -5,11 +5,11 @@ import "SamWaf/model/baseorm" // BlockingPage 自定义拦截模板界面 type BlockingPage struct { baseorm.BaseOrm - BlockingPageName string `json:"blocking_page_name"` //自定义拦截模板页面名称 - BlockingType string `json:"blocking_type"` //自定义类型 被拦截 - AttackType string `json:"attack_type"` //攻击类型 如: cc_attack, sensitive_word, sql_injection等 - HostCode string `json:"host_code"` //适用于某个网站唯一码 - ResponseCode string `json:"response_code"` //响应代码 默认403 - ResponseHeader string `json:"response_header"` //响应Header头信息(JSON) - ResponseContent string `json:"response_content"` //响应内容 + BlockingPageName string `gorm:"size:255" json:"blocking_page_name"` //自定义拦截模板页面名称 + BlockingType string `gorm:"size:50" json:"blocking_type"` //自定义类型 被拦截 + AttackType string `gorm:"size:100" json:"attack_type"` //攻击类型 如: cc_attack, sensitive_word, sql_injection等 + HostCode string `gorm:"size:64" json:"host_code"` //适用于某个网站唯一码 + ResponseCode string `gorm:"size:10" json:"response_code"` //响应代码 默认403 + ResponseHeader string `gorm:"type:text" json:"response_header"` //响应Header头信息(JSON) + ResponseContent string `gorm:"type:text" json:"response_content"` //响应内容 } diff --git a/model/blocklist.go b/model/blocklist.go index 4ea29e3..256c0cb 100644 --- a/model/blocklist.go +++ b/model/blocklist.go @@ -6,15 +6,15 @@ import ( type IPBlockList struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Ip string `json:"ip"` //限制ip - Remarks string `json:"remarks"` //备注 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Ip string `gorm:"size:64" json:"ip"` //限制ip + Remarks string `gorm:"size:500" json:"remarks"` //备注 } type URLBlockList struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - CompareType string `json:"compare_type" form:"compare_type"` //对比方式 - Url string `json:"url"` //限制请求地址 - Remarks string `json:"remarks"` //备注 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + CompareType string `gorm:"size:50" json:"compare_type" form:"compare_type"` //对比方式 + Url string `gorm:"type:text" json:"url"` //限制请求地址 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/ca_server_info.go b/model/ca_server_info.go index cb38aa8..bd95c60 100644 --- a/model/ca_server_info.go +++ b/model/ca_server_info.go @@ -4,7 +4,7 @@ import "SamWaf/model/baseorm" type CaServerInfo struct { baseorm.BaseOrm - CaServerName string `json:"ca_server_name"` //CA服务器名称 - CaServerAddress string `json:"ca_server_address"` //CA服务器地址 - Remarks string `json:"remarks"` //备注 + CaServerName string `gorm:"size:255" json:"ca_server_name"` //CA服务器名称 + CaServerAddress string `gorm:"size:500" json:"ca_server_address"` //CA服务器地址 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/cache_rule.go b/model/cache_rule.go index 7b19812..f7149b4 100644 --- a/model/cache_rule.go +++ b/model/cache_rule.go @@ -5,13 +5,13 @@ import "SamWaf/model/baseorm" // 缓存规则 type CacheRule struct { baseorm.BaseOrm - HostCode string `json:"host_code" gorm:"column:host_code"` // 主机代码 - RuleName string `json:"rule_name" gorm:"column:rule_name"` // 规则名称 - RuleType int `json:"rule_type" gorm:"column:rule_type"` // 规则类型:1-后缀匹配 ,2-指定目录,3-指定文件 - RuleContent string `json:"rule_content" gorm:"column:rule_content"` // 规则内容 - ParamType int `json:"param_type" gorm:"column:param_type"` // 参数处理:1-忽略参数,2-完整参数 - CacheTime int `json:"cache_time" gorm:"column:cache_time"` // 缓存时间(秒):0-不缓存 - Priority int `json:"priority" gorm:"column:priority"` // 优先级:数字越大优先级越高 - RequestMethod string `json:"request_method" gorm:"column:request_method"` // 请求方式:GET;HEAD;POST等,多个用分号分隔 - Remarks string `json:"remarks" gorm:"column:remarks"` // 备注 + HostCode string `json:"host_code" gorm:"column:host_code;size:64"` // 主机代码 + RuleName string `json:"rule_name" gorm:"column:rule_name;size:255"` // 规则名称 + RuleType int `json:"rule_type" gorm:"column:rule_type"` // 规则类型:1-后缀匹配 ,2-指定目录,3-指定文件 + RuleContent string `json:"rule_content" gorm:"column:rule_content;type:text"` // 规则内容 + ParamType int `json:"param_type" gorm:"column:param_type"` // 参数处理:1-忽略参数,2-完整参数 + CacheTime int `json:"cache_time" gorm:"column:cache_time"` // 缓存时间(秒):0-不缓存 + Priority int `json:"priority" gorm:"column:priority"` // 优先级:数字越大优先级越高 + RequestMethod string `json:"request_method" gorm:"column:request_method;size:20"` // 请求方式:GET;HEAD;POST等,多个用分号分隔 + Remarks string `json:"remarks" gorm:"column:remarks;size:500"` // 备注 } diff --git a/model/center.go b/model/center.go index 5041e87..99e1b36 100644 --- a/model/center.go +++ b/model/center.go @@ -11,16 +11,16 @@ import ( */ type Center struct { baseorm.BaseOrm - ClientServerName string `json:"client_server_name"` // 客户端-自定义名称 - ClientUserCode string `json:"client_user_code"` // 客户端-用户码 - ClientTenantId string `json:"client_tenant_id"` // 客户端-租户ID - ClientToken string `json:"client_token"` // 客户端-访问密钥 - ClientSsl string `json:"client_ssl"` // 客户端-是否https - ClientIP string `json:"client_ip"` //客户端 ip - ClientPort string `json:"client_port"` //客户端 port - ClientNewVersion string `json:"client_new_version"` //客户端 版本号 - ClientNewVersionDesc string `json:"client_new_version_desc"` //客户端 版本描述 - ClientSystemType string `json:"client_system_type"` //操作系统类型 - LastVisitTime customtype.JsonTime `json:"last_visit_time"` //上次访问时间 - Remarks string `json:"remarks"` //备注 + ClientServerName string `gorm:"size:255" json:"client_server_name"` // 客户端-自定义名称 + ClientUserCode string `gorm:"size:64" json:"client_user_code"` // 客户端-用户码 + ClientTenantId string `gorm:"size:64" json:"client_tenant_id"` // 客户端-租户ID + ClientToken string `gorm:"size:500" json:"client_token"` // 客户端-访问密钥 + ClientSsl string `gorm:"size:10" json:"client_ssl"` // 客户端-是否https + ClientIP string `gorm:"size:64" json:"client_ip"` //客户端 ip + ClientPort string `gorm:"size:10" json:"client_port"` //客户端 port + ClientNewVersion string `gorm:"size:64" json:"client_new_version"` //客户端 版本号 + ClientNewVersionDesc string `gorm:"type:text" json:"client_new_version_desc"` //客户端 版本描述 + ClientSystemType string `gorm:"size:50" json:"client_system_type"` //操作系统类型 + LastVisitTime customtype.JsonTime `json:"last_visit_time"` //上次访问时间 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/data_retention_policy.go b/model/data_retention_policy.go index 37f0ce0..7810900 100644 --- a/model/data_retention_policy.go +++ b/model/data_retention_policy.go @@ -13,16 +13,16 @@ import ( // - "core" → GWAF_LOCAL_DB(核心库,预留) type DataRetentionPolicy struct { baseorm.BaseOrm - TableName string `json:"table_name"` // 目标表名,如 stats_ip_days - DbType string `json:"db_type"` // 归属库类型:stats / log / core - RetainDays int64 `json:"retain_days"` // 保留天数,0=不按天清理 - RetainRows int64 `json:"retain_rows"` // 保留行数,0=不按行清理 - DayField string `json:"day_field"` // 天数清理依赖的字段名,如 "day" 或 "create_time" - DayFieldType string `json:"day_field_type"` // 天数字段类型:int_day(YYYYMMDD整数) / datetime(时间类型) - RowOrderField string `json:"row_order_field"` // 行数清理的排序字段,如 "day"、"update_time"、"cnt" - RowOrderDir string `json:"row_order_dir"` // 排序方向:DESC=保留值最大的(最新),ASC=保留值最小的 - CleanEnabled int64 `json:"clean_enabled"` // 1=启用 0=禁用 - LastCleanTime customtype.JsonTime `json:"last_clean_time"` // 上次清理时间 - LastCleanRows int64 `json:"last_clean_rows"` // 上次清理的行数 - Remarks string `json:"remarks"` // 备注 + TableName string `gorm:"size:100" json:"table_name"` // 目标表名,如 stats_ip_days + DbType string `gorm:"size:20" json:"db_type"` // 归属库类型:stats / log / core + RetainDays int64 `json:"retain_days"` // 保留天数,0=不按天清理 + RetainRows int64 `json:"retain_rows"` // 保留行数,0=不按行清理 + DayField string `gorm:"size:100" json:"day_field"` // 天数清理依赖的字段名,如 "day" 或 "create_time" + DayFieldType string `gorm:"size:50" json:"day_field_type"` // 天数字段类型:int_day(YYYYMMDD整数) / datetime(时间类型) + RowOrderField string `gorm:"size:100" json:"row_order_field"` // 行数清理的排序字段,如 "day"、"update_time"、"cnt" + RowOrderDir string `gorm:"size:20" json:"row_order_dir"` // 排序方向:DESC=保留值最大的(最新),ASC=保留值最小的 + CleanEnabled int64 `json:"clean_enabled"` // 1=启用 0=禁用 + LastCleanTime customtype.JsonTime `json:"last_clean_time"` // 上次清理时间 + LastCleanRows int64 `json:"last_clean_rows"` // 上次清理的行数 + Remarks string `gorm:"size:500" json:"remarks"` // 备注 } diff --git a/model/delaymsg.go b/model/delaymsg.go index 9381272..b94095c 100644 --- a/model/delaymsg.go +++ b/model/delaymsg.go @@ -10,7 +10,7 @@ import ( */ type DelayMsg struct { baseorm.BaseOrm - DelayType string `json:"delay_type"` //操作类型 - DelayTile string `json:"delay_title"` //操作标题 - DelayContent string `json:"delay_content"` //操作内容 + DelayType string `gorm:"size:50" json:"delay_type"` //操作类型 + DelayTile string `gorm:"size:255" json:"delay_title"` //操作标题 + DelayContent string `gorm:"type:text" json:"delay_content"` //操作内容 } diff --git a/model/firewall_ip_block.go b/model/firewall_ip_block.go index f9487af..b04dc0d 100644 --- a/model/firewall_ip_block.go +++ b/model/firewall_ip_block.go @@ -7,13 +7,13 @@ import ( // FirewallIPBlock 防火墙IP封禁记录表(操作系统级别的防火墙封禁) type FirewallIPBlock struct { baseorm.BaseOrm - HostCode string `json:"host_code"` // 网站唯一码(主要键) - IP string `json:"ip"` // 被封禁的IP地址,支持单个IP或CIDR格式 - Reason string `json:"reason"` // 封禁原因 - BlockType string `json:"block_type"` // 封禁类型:manual-手动封禁, auto-自动封禁, temp-临时封禁 - Status string `json:"status"` // 状态:active-已生效, inactive-已失效, pending-待生效 - ExpireTime int64 `json:"expire_time"` // 过期时间(时间戳,0表示永久) - Remarks string `json:"remarks"` // 备注 + HostCode string `gorm:"size:64" json:"host_code"` // 网站唯一码(主要键) + IP string `gorm:"size:64" json:"ip"` // 被封禁的IP地址,支持单个IP或CIDR格式 + Reason string `gorm:"type:text" json:"reason"` // 封禁原因 + BlockType string `gorm:"size:32" json:"block_type"` // 封禁类型:manual-手动封禁, auto-自动封禁, temp-临时封禁 + Status string `gorm:"size:16" json:"status"` // 状态:active-已生效, inactive-已失效, pending-待生效 + ExpireTime int64 `json:"expire_time"` // 过期时间(时间戳,0表示永久) + Remarks string `gorm:"size:500" json:"remarks"` // 备注 } // TableName 表名 diff --git a/model/hosts.go b/model/hosts.go index d8cd591..3d39245 100644 --- a/model/hosts.go +++ b/model/hosts.go @@ -7,48 +7,48 @@ import ( type Hosts struct { baseorm.BaseOrm - Code string `json:"code"` //唯一码 - Host string `json:"host"` //域名 - Port int `json:"port"` //端口 - Ssl int `json:"ssl"` //是否是ssl - GUARD_STATUS int `json:"guard_status"` //防御状态 1 是开启防御 0 是防御关闭 - REMOTE_SYSTEM string `json:"remote_system"` //是宝塔 phpstudy等 - REMOTE_APP string `json:"remote_app"` //是什么类型的应用 - Remote_host string `json:"remote_host"` //远端域名 - Remote_port int `json:"remote_port"` //远端端口 - Remote_ip string `json:"remote_ip"` //远端指定IP - Certfile string `json:"certfile"` //证书文件 - Keyfile string `json:"keyfile"` //密钥文件 - REMARKS string `json:"remarks"` //备注 - GLOBAL_HOST int `json:"global_host"` //默认全局 1 全局 0非全局 - DEFENSE_JSON string `json:"defense_json"` //自身防御 json - START_STATUS int `json:"start_status"` //启动状态 如果是0 启动 ; 如果是1 不启动 - EXCLUDE_URL_LOG string `json:"exclude_url_log"` //排除的url开头的数据 换行隔开 - IsEnableLoadBalance int `json:"is_enable_load_balance"` //是否激活负载 1 激活 非1 没有激活 - LoadBalanceStage int `json:"load_balance_stage"` //负载策略 - UnrestrictedPort int `json:"unrestricted_port"` //不限来源匹配端口 0 限制 1,不限制 - BindSslId string `json:"bind_ssl_id"` //绑定SSL的ID - AutoJumpHTTPS int `json:"auto_jump_https"` //是否自动跳转https 0 不自动 1 强制80跳转https - BindMoreHost string `json:"bind_more_host"` //绑定多域名 - IsTransBackDomain int `json:"is_trans_back_domain"` //是否传递后端域名到后端服务器侧 - BindMorePort string `json:"bind_more_port"` //是否绑定多个端口 - IsEnableHttpAuthBase int `json:"is_enable_http_auth_base"` //是否 HTTPAuthBase 1 激活 非1 没有激活 - HttpAuthBaseType string `json:"http_auth_base_type"` //认证类型 authorization(默认Basic Auth) custom(自定义页面) - HttpAuthPathPrefix string `json:"http_auth_path_prefix"` //HTTP认证路径前缀,用于隐藏系统特征,默认为随机生成 - ResponseTimeOut int `json:"response_time_out"` //响应超时时间 默认60秒,为0则无限等待 - HealthyJSON string `json:"healthy_json"` //后端健康度检测 json - InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验 - CaptchaJSON string `json:"captcha_json"` //验证码配置 json - AntiLeechJSON string `json:"anti_leech_json"` //防盗链配置 json - CacheJSON string `json:"cache_json"` //缓存配置 json - StaticSiteJSON string `json:"static_site_json"` //静态站点配置 json - TransportJSON string `json:"transport_json"` //传输配置 json - DefaultEncoding string `json:"default_encoding"` //默认编码 utf-8 或者 gbk auto字符串自动选择 - LogOnlyMode int `json:"log_only_mode"` //仅记录模式 1开启 0关闭 - CustomHeadersJSON string `json:"custom_headers_json"` //自定义头信息配置 json - CustomResponseHeadersJSON string `json:"custom_response_headers_json"` //自定义响应头信息配置 json - ResponseCompressJSON string `json:"response_compress_json"` //响应压缩配置 json(Gzip/Brotli/Zstd) - IPMode string `json:"ip_mode"` //IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式 + Code string `gorm:"size:64" json:"code"` //唯一码 + Host string `gorm:"size:255" json:"host"` //域名 + Port int `json:"port"` //端口 + Ssl int `json:"ssl"` //是否是ssl + GUARD_STATUS int `json:"guard_status"` //防御状态 1 是开启防御 0 是防御关闭 + REMOTE_SYSTEM string `gorm:"size:50" json:"remote_system"` //是宝塔 phpstudy等 + REMOTE_APP string `gorm:"size:100" json:"remote_app"` //是什么类型的应用 + Remote_host string `gorm:"size:255" json:"remote_host"` //远端域名 + Remote_port int `json:"remote_port"` //远端端口 + Remote_ip string `gorm:"size:64" json:"remote_ip"` //远端指定IP + Certfile string `gorm:"type:text" json:"certfile"` //证书文件 + Keyfile string `gorm:"type:text" json:"keyfile"` //密钥文件 + REMARKS string `gorm:"size:500" json:"remarks"` //备注 + GLOBAL_HOST int `json:"global_host"` //默认全局 1 全局 0非全局 + DEFENSE_JSON string `gorm:"type:text" json:"defense_json"` //自身防御 json + START_STATUS int `json:"start_status"` //启动状态 如果是0 启动 ; 如果是1 不启动 + EXCLUDE_URL_LOG string `gorm:"type:text" json:"exclude_url_log"` //排除的url开头的数据 换行隔开 + IsEnableLoadBalance int `json:"is_enable_load_balance"` //是否激活负载 1 激活 非1 没有激活 + LoadBalanceStage int `json:"load_balance_stage"` //负载策略 + UnrestrictedPort int `json:"unrestricted_port"` //不限来源匹配端口 0 限制 1,不限制 + BindSslId string `gorm:"size:64" json:"bind_ssl_id"` //绑定SSL的ID + AutoJumpHTTPS int `json:"auto_jump_https"` //是否自动跳转https 0 不自动 1 强制80跳转https + BindMoreHost string `gorm:"type:text" json:"bind_more_host"` //绑定多域名 + IsTransBackDomain int `json:"is_trans_back_domain"` //是否传递后端域名到后端服务器侧 + BindMorePort string `gorm:"size:255" json:"bind_more_port"` //是否绑定多个端口 + IsEnableHttpAuthBase int `json:"is_enable_http_auth_base"` //是否 HTTPAuthBase 1 激活 非1 没有激活 + HttpAuthBaseType string `gorm:"size:50" json:"http_auth_base_type"` //认证类型 authorization(默认Basic Auth) custom(自定义页面) + HttpAuthPathPrefix string `gorm:"size:255" json:"http_auth_path_prefix"` //HTTP认证路径前缀,用于隐藏系统特征,默认为随机生成 + ResponseTimeOut int `json:"response_time_out"` //响应超时时间 默认60秒,为0则无限等待 + HealthyJSON string `gorm:"type:text" json:"healthy_json"` //后端健康度检测 json + InsecureSkipVerify int `json:"insecure_skip_verify"` //是否开启后端https证书有效性验证 默认 0 是校验 1 是不校验 + CaptchaJSON string `gorm:"type:text" json:"captcha_json"` //验证码配置 json + AntiLeechJSON string `gorm:"type:text" json:"anti_leech_json"` //防盗链配置 json + CacheJSON string `gorm:"type:text" json:"cache_json"` //缓存配置 json + StaticSiteJSON string `gorm:"type:text" json:"static_site_json"` //静态站点配置 json + TransportJSON string `gorm:"type:text" json:"transport_json"` //传输配置 json + DefaultEncoding string `gorm:"size:20" json:"default_encoding"` //默认编码 utf-8 或者 gbk auto字符串自动选择 + LogOnlyMode int `json:"log_only_mode"` //仅记录模式 1开启 0关闭 + CustomHeadersJSON string `gorm:"type:text" json:"custom_headers_json"` //自定义头信息配置 json + CustomResponseHeadersJSON string `gorm:"type:text" json:"custom_response_headers_json"` //自定义响应头信息配置 json + ResponseCompressJSON string `gorm:"type:text" json:"response_compress_json"` //响应压缩配置 json(Gzip/Brotli/Zstd) + IPMode string `gorm:"size:20" json:"ip_mode"` //IP提取模式: "nic" 网卡模式 或 "proxy" 代理模式 } type HostsDefense struct { diff --git a/model/http_auth_base_config.go b/model/http_auth_base_config.go index 7d7c358..5afabc1 100644 --- a/model/http_auth_base_config.go +++ b/model/http_auth_base_config.go @@ -4,7 +4,7 @@ import "SamWaf/model/baseorm" type HttpAuthBase struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - UserName string `json:"user_name"` // 用户名 - Password string `json:"password"` // 访问密码 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + UserName string `gorm:"size:255" json:"user_name"` // 用户名 + Password string `gorm:"size:255" json:"password"` // 访问密码 } diff --git a/model/ldp.go b/model/ldp.go index d274780..684294a 100644 --- a/model/ldp.go +++ b/model/ldp.go @@ -9,9 +9,9 @@ import ( */ type LDPUrl struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) //CompareCol string `json:"CompareCol"` //判断字段 - CompareType string `json:"compare_type"` //判断类型,包含、开始、结束、完全匹配 - Url string `json:"url"` //请求地址 - Remarks string `json:"remarks"` //备注 + CompareType string `gorm:"size:50" json:"compare_type"` //判断类型,包含、开始、结束、完全匹配 + Url string `gorm:"type:text" json:"url"` //请求地址 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/loadbalance.go b/model/loadbalance.go index 0030a55..7f4ab8a 100644 --- a/model/loadbalance.go +++ b/model/loadbalance.go @@ -7,9 +7,9 @@ import "SamWaf/model/baseorm" */ type LoadBalance struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Remote_port int `json:"remote_port"` //远端端口 - Remote_ip string `json:"remote_ip"` //远端指定IP - Weight int `json:"weight"` //权重 - Remarks string `json:"remarks"` //备注 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Remote_port int `json:"remote_port"` //远端端口 + Remote_ip string `gorm:"size:64" json:"remote_ip"` //远端指定IP + Weight int `json:"weight"` //权重 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/notify_channel.go b/model/notify_channel.go index a301135..78f8505 100644 --- a/model/notify_channel.go +++ b/model/notify_channel.go @@ -10,12 +10,12 @@ import ( */ type NotifyChannel struct { baseorm.BaseOrm - Name string `json:"name"` // 渠道名称 - Type string `json:"type"` // 渠道类型:dingtalk, feishu, wechat, email等 - WebhookURL string `json:"webhook_url"` // Webhook地址 - Secret string `json:"secret"` // 密钥(用于签名验证) - AccessToken string `json:"access_token"` // 访问令牌 - ConfigJSON string `json:"config_json"` // 额外配置(JSON格式) - Status int `json:"status"` // 状态:1启用,0禁用 - Remarks string `json:"remarks"` // 备注 + Name string `gorm:"size:255" json:"name"` // 渠道名称 + Type string `gorm:"size:50" json:"type"` // 渠道类型:dingtalk, feishu, wechat, email等 + WebhookURL string `gorm:"size:500" json:"webhook_url"` // Webhook地址 + Secret string `gorm:"size:255" json:"secret"` // 密钥(用于签名验证) + AccessToken string `gorm:"size:500" json:"access_token"` // 访问令牌 + ConfigJSON string `gorm:"type:text" json:"config_json"` // 额外配置(JSON格式) + Status int `json:"status"` // 状态:1启用,0禁用 + Remarks string `gorm:"size:500" json:"remarks"` // 备注 } diff --git a/model/notify_log.go b/model/notify_log.go index 5ae0fab..6463eb4 100644 --- a/model/notify_log.go +++ b/model/notify_log.go @@ -10,14 +10,14 @@ import ( */ type NotifyLog struct { baseorm.BaseOrm - ChannelId string `json:"channel_id"` // 渠道ID - ChannelName string `json:"channel_name"` // 渠道名称 - ChannelType string `json:"channel_type"` // 渠道类型 - MessageType string `json:"message_type"` // 消息类型 - MessageTitle string `json:"message_title"` // 消息标题 - MessageContent string `json:"message_content"` // 消息内容 - Recipients string `json:"recipients"` // 收件人(仅邮件类型) - Status int `json:"status"` // 发送状态:1成功,0失败 - ErrorMsg string `json:"error_msg"` // 错误信息 - SendTime string `json:"send_time"` // 发送时间 + ChannelId string `gorm:"size:64" json:"channel_id"` // 渠道ID + ChannelName string `gorm:"size:255" json:"channel_name"` // 渠道名称 + ChannelType string `gorm:"size:50" json:"channel_type"` // 渠道类型 + MessageType string `gorm:"size:100" json:"message_type"` // 消息类型 + MessageTitle string `gorm:"size:500" json:"message_title"` // 消息标题 + MessageContent string `gorm:"type:text" json:"message_content"` // 消息内容 + Recipients string `gorm:"type:text" json:"recipients"` // 收件人(仅邮件类型) + Status int `json:"status"` // 发送状态:1成功,0失败 + ErrorMsg string `gorm:"type:text" json:"error_msg"` // 错误信息 + SendTime string `gorm:"size:100" json:"send_time"` // 发送时间 } diff --git a/model/notify_subscription.go b/model/notify_subscription.go index 01187fb..7c08250 100644 --- a/model/notify_subscription.go +++ b/model/notify_subscription.go @@ -10,12 +10,12 @@ import ( */ type NotifySubscription struct { baseorm.BaseOrm - ChannelId string `json:"channel_id"` // 关联的渠道ID - MessageType string `json:"message_type"` // 消息类型:user_login, attack_info, weekly_report等 - Recipients string `json:"recipients"` // 收件人列表(逗号分隔,主要用于邮件类型)留空则使用渠道默认收件人 - Status int `json:"status"` // 状态:1启用,0禁用 - FilterJSON string `json:"filter_json"` // 过滤条件(JSON格式) - Remarks string `json:"remarks"` // 备注 + ChannelId string `gorm:"size:64" json:"channel_id"` // 关联的渠道ID + MessageType string `gorm:"size:100" json:"message_type"` // 消息类型:user_login, attack_info, weekly_report等 + Recipients string `gorm:"type:text" json:"recipients"` // 收件人列表(逗号分隔,主要用于邮件类型)留空则使用渠道默认收件人 + Status int `json:"status"` // 状态:1启用,0禁用 + FilterJSON string `gorm:"type:text" json:"filter_json"` // 过滤条件(JSON格式) + Remarks string `gorm:"size:500" json:"remarks"` // 备注 } // 消息类型常量 diff --git a/model/onekeymod.go b/model/onekeymod.go index d3deade..a9f18ef 100644 --- a/model/onekeymod.go +++ b/model/onekeymod.go @@ -8,10 +8,10 @@ import "SamWaf/model/baseorm" */ type OneKeyMod struct { baseorm.BaseOrm - OpSystem string `json:"op_system"` //系统类型 - FilePath string `json:"file_path"` //文件路径 - BeforeContent string `json:"before_content"` //修改前内容 - AfterContent string `json:"after_content"` //修改后内容 - IsRestore int `json:"is_restore"` //是否还原 - Remarks string `json:"remarks"` //备注 + OpSystem string `gorm:"size:50" json:"op_system"` //系统类型 + FilePath string `gorm:"size:500" json:"file_path"` //文件路径 + BeforeContent string `gorm:"type:text" json:"before_content"` //修改前内容 + AfterContent string `gorm:"type:text" json:"after_content"` //修改后内容 + IsRestore int `json:"is_restore"` //是否还原 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/oplatform_key.go b/model/oplatform_key.go index 18ee52b..b11c0f1 100644 --- a/model/oplatform_key.go +++ b/model/oplatform_key.go @@ -10,13 +10,13 @@ import ( */ type OPlatformKey struct { baseorm.BaseOrm - KeyName string `json:"key_name"` //Key 名称 - ApiKey string `json:"api_key"` //API Key(自动生成,用于 X-API-Key 请求头鉴权) - Status int `json:"status"` //状态 1启用 0禁用 - Remark string `json:"remark"` //备注 - RateLimit int64 `json:"rate_limit"` //每分钟限流次数,0不限 - IPWhitelist string `json:"ip_whitelist"` //IP白名单,逗号分隔,空不限 - ExpireTime string `json:"expire_time"` //过期时间,空不过期 - LastUseTime string `json:"last_use_time"` //最后使用时间 - CallCount int64 `json:"call_count"` //累计调用次数 + KeyName string `gorm:"size:255" json:"key_name"` //Key 名称 + ApiKey string `gorm:"size:255" json:"api_key"` //API Key(自动生成,用于 X-API-Key 请求头鉴权) + Status int `json:"status"` //状态 1启用 0禁用 + Remark string `gorm:"size:500" json:"remark"` //备注 + RateLimit int64 `json:"rate_limit"` //每分钟限流次数,0不限 + IPWhitelist string `gorm:"type:text" json:"ip_whitelist"` //IP白名单,逗号分隔,空不限 + ExpireTime string `gorm:"size:50" json:"expire_time"` //过期时间,空不过期 + LastUseTime string `gorm:"size:50" json:"last_use_time"` //最后使用时间 + CallCount int64 `json:"call_count"` //累计调用次数 } diff --git a/model/oplatform_log.go b/model/oplatform_log.go index cbfc1fa..b259996 100644 --- a/model/oplatform_log.go +++ b/model/oplatform_log.go @@ -10,14 +10,14 @@ import ( */ type OPlatformLog struct { baseorm.BaseOrm - ApiKeyId string `json:"api_key_id"` //关联的Key ID - KeyName string `json:"key_name"` //Key名称(冗余,方便查询) - RequestPath string `json:"request_path"` //请求路径 - RequestMethod string `json:"request_method"` //请求方法 - RequestBody string `json:"request_body"` //请求体(非文件类型记录) - ResponseBody string `json:"response_body"` //响应体 - ClientIP string `json:"client_ip"` //调用者IP - StatusCode int `json:"status_code"` //响应状态码 - Duration int64 `json:"duration"` //耗时(ms) - TimeStr string `json:"time_str"` //调用时间字符串 + ApiKeyId string `gorm:"size:64" json:"api_key_id"` //关联的Key ID + KeyName string `gorm:"size:255" json:"key_name"` //Key名称(冗余,方便查询) + RequestPath string `gorm:"size:500" json:"request_path"` //请求路径 + RequestMethod string `gorm:"size:20" json:"request_method"` //请求方法 + RequestBody string `gorm:"type:text" json:"request_body"` //请求体(非文件类型记录) + ResponseBody string `gorm:"type:text" json:"response_body"` //响应体 + ClientIP string `gorm:"size:64" json:"client_ip"` //调用者IP + StatusCode int `json:"status_code"` //响应状态码 + Duration int64 `json:"duration"` //耗时(ms) + TimeStr string `gorm:"size:100" json:"time_str"` //调用时间字符串 } diff --git a/model/otp.go b/model/otp.go index de85e13..ed50e51 100644 --- a/model/otp.go +++ b/model/otp.go @@ -4,9 +4,9 @@ import "SamWaf/model/baseorm" type Otp struct { baseorm.BaseOrm - UserName string `json:"user_name"` //用户名 - Url string `json:"url"` //URL - Secret string `json:"secret"` //密钥 - Issuer string `json:"issuer"` //发行者标识 - Remarks string `json:"remarks"` //备注 + UserName string `gorm:"size:255" json:"user_name"` //用户名 + Url string `gorm:"size:500" json:"url"` //URL + Secret string `gorm:"size:255" json:"secret"` //密钥 + Issuer string `gorm:"size:255" json:"issuer"` //发行者标识 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/private_group.go b/model/private_group.go index 28d3a84..15914b8 100644 --- a/model/private_group.go +++ b/model/private_group.go @@ -4,6 +4,6 @@ import "SamWaf/model/baseorm" type PrivateGroup struct { baseorm.BaseOrm - PrivateGroupName string `json:"private_group_name"` //分组名称 - PrivateGroupBelongCloud string `json:"private_group_belong_cloud"` //分组归属云 + PrivateGroupName string `gorm:"size:255" json:"private_group_name"` //分组名称 + PrivateGroupBelongCloud string `gorm:"size:100" json:"private_group_belong_cloud"` //分组归属云 } diff --git a/model/private_info.go b/model/private_info.go index 68231b6..2d864c6 100644 --- a/model/private_info.go +++ b/model/private_info.go @@ -4,9 +4,9 @@ import "SamWaf/model/baseorm" type PrivateInfo struct { baseorm.BaseOrm - PrivateKey string `json:"private_key"` //密钥key - PrivateValue string `json:"private_value"` //密钥值 - PrivateGroupName string `json:"private_group_name"` //分组名称 - PrivateGroupBelongCloud string `json:"private_group_belong_cloud"` //分组归属云 - Remarks string `json:"remarks"` //备注 + PrivateKey string `gorm:"size:255" json:"private_key"` //密钥key + PrivateValue string `gorm:"type:text" json:"private_value"` //密钥值 + PrivateGroupName string `gorm:"size:255" json:"private_group_name"` //分组名称 + PrivateGroupBelongCloud string `gorm:"size:100" json:"private_group_belong_cloud"` //分组归属云 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/rules.go b/model/rules.go index 36027bd..f33d873 100644 --- a/model/rules.go +++ b/model/rules.go @@ -6,14 +6,14 @@ import ( type Rules struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //主机唯一码 - RuleCode string `json:"rule_code"` //规则的唯一码 - RuleName string `json:"rule_name"` //规则名称 - RuleContent string `json:"rule_content"` //规则内容 - RuleContentJSON string `json:"rule_content_json"` //规则JSON内容 - RuleVersionName string `json:"rule_version_name"` //规则版本名 - RuleVersion int `json:"rule_version"` //规则版本号 - IsPublicRule int `json:"is_public_rule"` //是否为公共规则 - IsManualRule int `json:"is_manual_rule"` //是否为手工写规则 1:手工编写 0 :UI界面形式 - RuleStatus int `json:"rule_status"` //规则是否开启 1,开启 0,关闭不生效 999 删除 + HostCode string `gorm:"size:64" json:"host_code"` //主机唯一码 + RuleCode string `gorm:"size:64" json:"rule_code"` //规则的唯一码 + RuleName string `gorm:"size:255" json:"rule_name"` //规则名称 + RuleContent string `gorm:"type:text" json:"rule_content"` //规则内容 + RuleContentJSON string `gorm:"type:text" json:"rule_content_json"` //规则JSON内容 + RuleVersionName string `gorm:"size:100" json:"rule_version_name"` //规则版本名 + RuleVersion int `json:"rule_version"` //规则版本号 + IsPublicRule int `json:"is_public_rule"` //是否为公共规则 + IsManualRule int `json:"is_manual_rule"` //是否为手工写规则 1:手工编写 0 :UI界面形式 + RuleStatus int `json:"rule_status"` //规则是否开启 1,开启 0,关闭不生效 999 删除 } diff --git a/model/sensitive.go b/model/sensitive.go index 4d692ed..7202db2 100644 --- a/model/sensitive.go +++ b/model/sensitive.go @@ -4,8 +4,8 @@ import "SamWaf/model/baseorm" type Sensitive struct { baseorm.BaseOrm - CheckDirection string `json:"check_direction"` //敏感词检测方向 in ,out ,all - Action string `json:"action"` //敏感词检测后动作 deny,replace - Content string `json:"content"` //内容 - Remarks string `json:"remarks"` //备注 + CheckDirection string `gorm:"size:50" json:"check_direction"` //敏感词检测方向 in ,out ,all + Action string `gorm:"size:50" json:"action"` //敏感词检测后动作 deny,replace + Content string `gorm:"type:text" json:"content"` //内容 + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/sharedb.go b/model/sharedb.go index f0f5d22..8a2079f 100644 --- a/model/sharedb.go +++ b/model/sharedb.go @@ -12,11 +12,11 @@ import ( */ type ShareDb struct { baseorm.BaseOrm - DbLogicType string `json:"db_logic_type"` //数据库逻辑类型 默认:"log" - StartTime customtype.JsonTime `json:"start_time"` //开始时间 - EndTime customtype.JsonTime `json:"end_time"` //结束时间 - FileName string `json:"file_name"` //文件名:SQLite 下为 .db 文件名;MySQL 下为表名(无后缀) - Cnt int64 `json:"cnt"` //当前数量 + DbLogicType string `gorm:"size:50" json:"db_logic_type"` //数据库逻辑类型 默认:"log" + StartTime customtype.JsonTime `json:"start_time"` //开始时间 + EndTime customtype.JsonTime `json:"end_time"` //结束时间 + FileName string `gorm:"size:255" json:"file_name"` //文件名:SQLite 下为 .db 文件名;MySQL 下为表名(无后缀) + Cnt int64 `json:"cnt"` //当前数量 } // IsTableShard 在 MySQL/SQL Server 模式下返回 true(FileName 存的是表名而非文件名) diff --git a/model/sslconfig.go b/model/sslconfig.go index 2d9a7fd..89ac1d7 100644 --- a/model/sslconfig.go +++ b/model/sslconfig.go @@ -13,16 +13,16 @@ import ( type SslConfig struct { baseorm.BaseOrm - CertContent string `json:"cert_content"` // 证书文件内容 - KeyContent string `json:"key_content"` // 密钥文件内容 - SerialNo string `json:"serial_no"` // 证书序列号 - Subject string `json:"subject"` // 证书主题 - Issuer string `json:"issuer"` // 颁发者 - ValidFrom time.Time `json:"valid_from"` // 证书有效期开始时间 - ValidTo time.Time `json:"valid_to"` // 证书有效期结束时间 - Domains string `json:"domains"` // 证书适用的域名 - KeyPath string `json:"key_path"` //密钥文件位置 - CertPath string `json:"cert_path"` //crt文件配置 + CertContent string `gorm:"type:text" json:"cert_content"` // 证书文件内容 + KeyContent string `gorm:"type:text" json:"key_content"` // 密钥文件内容 + SerialNo string `gorm:"size:255" json:"serial_no"` // 证书序列号 + Subject string `gorm:"size:500" json:"subject"` // 证书主题 + Issuer string `gorm:"size:500" json:"issuer"` // 颁发者 + ValidFrom time.Time `json:"valid_from"` // 证书有效期开始时间 + ValidTo time.Time `json:"valid_to"` // 证书有效期结束时间 + Domains string `gorm:"type:text" json:"domains"` // 证书适用的域名 + KeyPath string `gorm:"size:500" json:"key_path"` //密钥文件位置 + CertPath string `gorm:"size:500" json:"cert_path"` //crt文件配置 } // ExpirationMessage 获取到期提示信息 diff --git a/model/sslexpire.go b/model/sslexpire.go index 1eed273..ce7847a 100644 --- a/model/sslexpire.go +++ b/model/sslexpire.go @@ -8,12 +8,12 @@ import ( type SslExpire struct { baseorm.BaseOrm - Domain string `json:"domain"` // 证书适用的域名 - Port int `json:"port"` // 端口 - ValidTo time.Time `json:"valid_to"` // 证书有效期结束时间 - VisitLog string `json:"visit_log"` //访问日志 - LastDetect time.Time `json:"last_detect"` // 最后检测时间 - Status string `json:"status"` //状态 + Domain string `gorm:"size:255" json:"domain"` // 证书适用的域名 + Port int `json:"port"` // 端口 + ValidTo time.Time `json:"valid_to"` // 证书有效期结束时间 + VisitLog string `gorm:"size:500" json:"visit_log"` //访问日志 + LastDetect time.Time `json:"last_detect"` // 最后检测时间 + Status string `gorm:"size:50" json:"status"` //状态 } // ExpirationMessage 获取到期提示信息 diff --git a/model/sslorder.go b/model/sslorder.go index e9f942d..21a7dea 100644 --- a/model/sslorder.go +++ b/model/sslorder.go @@ -10,26 +10,26 @@ import ( // SslOrder 证书申请 type SslOrder struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - ApplyPlatform string `json:"apply_platform"` //申请平台 - ApplyMethod string `json:"apply_method"` //申请方式http01,dns - SkipDNSVerify int64 `json:"skip_dns_verify"` //dns01是否跳过本地DNS传播校验 0否 1是 - ApplyDns string `json:"apply_dns"` //申请dns服务商 - ApplyEmail string `json:"apply_email"` //申请邮箱 - ApplyKey string `json:"apply_key"` //申请私钥 - ApplyDomain string `json:"apply_domain"` //申请域名 - ApplyStatus string `json:"apply_status"` //申请状态 提交,完成,已续签 - ResultError string `json:"result_error"` //申请错误的详情 - ResultDomain string `json:"result_domain"` //返回结果信息 - ResultCertURL string `json:"result_cert_url"` //证书连接位置 - ResultCertStableURL string `json:"result_cert_stable_url"` //证书固定连接位置 - ResultPrivateKey []byte `json:"result_private_key"` //证书私钥信息 - ResultCertificate []byte `json:"result_certificate"` //证书信息 - ResultIssuerCertificate []byte `json:"result_issuer_certificate"` //证书所属信息 - ResultValidTo time.Time `json:"result_valid_to"` //证书有效期结束时间 - ResultCSR []byte `json:"result_csr"` //csr信息 - Remarks string `json:"remarks"` //备注信息 - PrivateGroupName string `json:"private_group_name"` //密钥分组名称DNS有效 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + ApplyPlatform string `gorm:"size:100" json:"apply_platform"` //申请平台 + ApplyMethod string `gorm:"size:50" json:"apply_method"` //申请方式http01,dns + SkipDNSVerify int64 `json:"skip_dns_verify"` //dns01是否跳过本地DNS传播校验 0否 1是 + ApplyDns string `gorm:"size:100" json:"apply_dns"` //申请dns服务商 + ApplyEmail string `gorm:"size:255" json:"apply_email"` //申请邮箱 + ApplyKey string `gorm:"size:255" json:"apply_key"` //申请私钥 + ApplyDomain string `gorm:"size:255" json:"apply_domain"` //申请域名 + ApplyStatus string `gorm:"size:50" json:"apply_status"` //申请状态 提交,完成,已续签 + ResultError string `gorm:"type:text" json:"result_error"` //申请错误的详情 + ResultDomain string `gorm:"size:255" json:"result_domain"` //返回结果信息 + ResultCertURL string `gorm:"size:500" json:"result_cert_url"` //证书连接位置 + ResultCertStableURL string `gorm:"size:500" json:"result_cert_stable_url"` //证书固定连接位置 + ResultPrivateKey []byte `json:"result_private_key"` //证书私钥信息 + ResultCertificate []byte `json:"result_certificate"` //证书信息 + ResultIssuerCertificate []byte `json:"result_issuer_certificate"` //证书所属信息 + ResultValidTo time.Time `json:"result_valid_to"` //证书有效期结束时间 + ResultCSR []byte `json:"result_csr"` //csr信息 + Remarks string `gorm:"size:500" json:"remarks"` //备注信息 + PrivateGroupName string `gorm:"size:255" json:"private_group_name"` //密钥分组名称DNS有效 } // ExpirationMessage 获取到期提示信息 diff --git a/model/stats.go b/model/stats.go index 859a087..8444569 100644 --- a/model/stats.go +++ b/model/stats.go @@ -22,11 +22,11 @@ type StatsTotal struct { */ type StatsDay struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Day int `json:"day"` //年月日(主要键) - Host string `json:"host"` //域名 - Type string `json:"type"` //类型 放行,阻止 - Count int `json:"count"` //数量 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Day int `json:"day"` //年月日(主要键) + Host string `gorm:"size:255" json:"host"` //域名 + Type string `gorm:"size:32" json:"type"` //类型 放行,阻止 + Count int `json:"count"` //数量 } /* @@ -35,12 +35,12 @@ type StatsDay struct { */ type StatsIPDay struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Day int `json:"day"` //年月日(主要键) - Host string `json:"host"` //域名 - IP string `json:"ip"` //ip - Type string `json:"type"` //类型 放行,阻止 - Count int `json:"count"` //数量 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Day int `json:"day"` //年月日(主要键) + Host string `gorm:"size:255" json:"host"` //域名 + IP string `gorm:"size:64" json:"ip"` //ip + Type string `gorm:"size:32" json:"type"` //类型 放行,阻止 + Count int `json:"count"` //数量 } /* @@ -49,14 +49,14 @@ type StatsIPDay struct { */ type StatsIPCityDay struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Day int `json:"day"` //年月日(主要键) - Host string `json:"host"` //域名 - Country string `json:"country"` //国家 - Province string `json:"province"` //省份 - City string `json:"city"` //城市 - Type string `json:"type"` //类型 放行,阻止 - Count int `json:"count"` //数量 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Day int `json:"day"` //年月日(主要键) + Host string `gorm:"size:255" json:"host"` //域名 + Country string `gorm:"size:100" json:"country"` //国家 + Province string `gorm:"size:100" json:"province"` //省份 + City string `gorm:"size:100" json:"city"` //城市 + Type string `gorm:"size:32" json:"type"` //类型 放行,阻止 + Count int `json:"count"` //数量 } /* @@ -95,15 +95,15 @@ type StatsIPCountMore struct { */ type StatsSiteDay struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - Day int `json:"day"` //年月日(主要键)如 20260310 - Host string `json:"host"` //域名 - TotalCount int64 `json:"total_count"` //总请求数(PV) - AttackCount int64 `json:"attack_count"` //攻击拦截数 - NormalCount int64 `json:"normal_count"` //正常放行数 - TrafficIn int64 `json:"traffic_in"` //入站流量(bytes) - TrafficOut int64 `json:"traffic_out"` //出站流量(bytes) - TotalTimeSpent int64 `json:"total_time_spent"` //总响应时间(ms),用于计算平均值 + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + Day int `json:"day"` //年月日(主要键)如 20260310 + Host string `gorm:"size:255" json:"host"` //域名 + TotalCount int64 `json:"total_count"` //总请求数(PV) + AttackCount int64 `json:"attack_count"` //攻击拦截数 + NormalCount int64 `json:"normal_count"` //正常放行数 + TrafficIn int64 `json:"traffic_in"` //入站流量(bytes) + TrafficOut int64 `json:"traffic_out"` //出站流量(bytes) + TotalTimeSpent int64 `json:"total_time_spent"` //总响应时间(ms),用于计算平均值 } /* @@ -112,15 +112,15 @@ type StatsSiteDay struct { */ type StatsSiteHour struct { baseorm.BaseOrm - HostCode string `json:"host_code"` //网站唯一码(主要键) - HourTime int64 `json:"hour_time"` //整点unix时间戳(秒)(主要键) - Host string `json:"host"` //域名 - TotalCount int64 `json:"total_count"` //总请求数 - AttackCount int64 `json:"attack_count"` //攻击拦截数 - NormalCount int64 `json:"normal_count"` //正常放行数 - TrafficIn int64 `json:"traffic_in"` //入站流量(bytes) - TrafficOut int64 `json:"traffic_out"` //出站流量(bytes) - TotalTimeSpent int64 `json:"total_time_spent"` //总响应时间(ms) + HostCode string `gorm:"size:64" json:"host_code"` //网站唯一码(主要键) + HourTime int64 `json:"hour_time"` //整点unix时间戳(秒)(主要键) + Host string `gorm:"size:255" json:"host"` //域名 + TotalCount int64 `json:"total_count"` //总请求数 + AttackCount int64 `json:"attack_count"` //攻击拦截数 + NormalCount int64 `json:"normal_count"` //正常放行数 + TrafficIn int64 `json:"traffic_in"` //入站流量(bytes) + TrafficOut int64 `json:"traffic_out"` //出站流量(bytes) + TotalTimeSpent int64 `json:"total_time_spent"` //总响应时间(ms) } /* diff --git a/model/system_config.go b/model/system_config.go index 76b91cc..c4f2386 100644 --- a/model/system_config.go +++ b/model/system_config.go @@ -10,12 +10,12 @@ import ( */ type SystemConfig struct { baseorm.BaseOrm - IsSystem string `json:"is_system"` //是否是系统值 - ItemClass string `json:"item_class"` //所属分类 - Item string `json:"item"` - Value string `json:"value"` - ItemType string `json:"item_type"` //配置类型 普通字符串,可选项 - Options string `json:"options"` //如果ItemType == 可选项 这个地方有数据 - HashInfo string `json:"hash_info"` - Remarks string `json:"remarks"` //备注 + IsSystem string `gorm:"size:10" json:"is_system"` //是否是系统值 + ItemClass string `gorm:"size:100" json:"item_class"` //所属分类 + Item string `gorm:"size:255" json:"item"` + Value string `gorm:"type:text" json:"value"` + ItemType string `gorm:"size:50" json:"item_type"` //配置类型 普通字符串,可选项 + Options string `gorm:"type:text" json:"options"` //如果ItemType == 可选项 这个地方有数据 + HashInfo string `gorm:"size:255" json:"hash_info"` + Remarks string `gorm:"size:500" json:"remarks"` //备注 } diff --git a/model/task.go b/model/task.go index b1374ec..77a6d21 100644 --- a/model/task.go +++ b/model/task.go @@ -4,10 +4,10 @@ import "SamWaf/model/baseorm" type Task struct { baseorm.BaseOrm - TaskName string `json:"task_name"` // 任务名称 - TaskUnit string `json:"task_unit"` // 单位 - TaskValue int `json:"task_value"` // 值 - TaskAt string `json:"task_at"` // 特定时间 - TaskMethod string `json:"task_method"` //关联方法 - TaskDaysOfTheWeek string `json:"task_days_of_the_week"` // 周几 在周级别的情况下才起作用 + TaskName string `gorm:"size:255" json:"task_name"` // 任务名称 + TaskUnit string `gorm:"size:50" json:"task_unit"` // 单位 + TaskValue int `json:"task_value"` // 值 + TaskAt string `gorm:"size:100" json:"task_at"` // 特定时间 + TaskMethod string `gorm:"size:255" json:"task_method"` //关联方法 + TaskDaysOfTheWeek string `gorm:"size:100" json:"task_days_of_the_week"` // 周几 在周级别的情况下才起作用 } diff --git a/model/tunnel.go b/model/tunnel.go index 9c9bb74..089d5b1 100644 --- a/model/tunnel.go +++ b/model/tunnel.go @@ -7,25 +7,25 @@ import ( // Tunnel 隧道 type Tunnel struct { baseorm.BaseOrm - Code string `json:"code"` //唯一码 - Name string `json:"name"` //名称 - Port string `json:"port"` //端口可以多个,号隔开 - Protocol string `json:"protocol"` //协议 tcp,udp - RemotePort int `json:"remote_port"` //远端端口 - RemoteIp string `json:"remote_ip"` //远端指定IP - AllowIp string `json:"allow_ip"` //允许访问的IP ,号隔开 - DenyIp string `json:"deny_ip"` //拒绝访问的IP ,号隔开 - StartStatus int `json:"start_status"` //启动状态 如果是1 启动 ; 如果是0 不启动 - ConnTimeout int `json:"conn_timeout"` // 连接超时时间,单位秒 60s - ReadTimeout int `json:"read_timeout"` // 读取超时时间,单位秒 0 表示不限制 - WriteTimeout int `json:"write_timeout"` // 写入超时时间,单位秒 0 表示不限制 - MaxInConnect int `json:"max_in_connect"` // 最大入站连接数 0 表示不限制 - MaxOutConnect int `json:"max_out_connect"` // 最大出站连接数 0 表示不限制 - AllowedTimeRanges string `json:"allowed_time_ranges"` // 允许访问的时间段 格式: 08:00-10:00;11:00-12:00 空表示24小时可访问 - IpVersion string `json:"ip_version"` // IP版本支持: "ipv4" 仅IPv4, "ipv6" 仅IPv6, "both" 同时支持IPv4和IPv6, 默认为"both" - Remark string `json:"remark"` //备注 - SSLStatus int `json:"ssl_status"` // SSL开关 0关闭 1开启 - SSLCertificate string `json:"ssl_certificate"` // SSL证书路径 - SSLCertificateKey string `json:"ssl_certificate_key"` // SSL密钥路径 - SSLProtocols string `json:"ssl_protocols"` // SSL协议版本 如 TLSv1.2 TLSv1.3 + Code string `gorm:"size:64" json:"code"` //唯一码 + Name string `gorm:"size:255" json:"name"` //名称 + Port string `gorm:"size:50" json:"port"` //端口可以多个,号隔开 + Protocol string `gorm:"size:20" json:"protocol"` //协议 tcp,udp + RemotePort int `json:"remote_port"` //远端端口 + RemoteIp string `gorm:"size:64" json:"remote_ip"` //远端指定IP + AllowIp string `gorm:"type:text" json:"allow_ip"` //允许访问的IP ,号隔开 + DenyIp string `gorm:"type:text" json:"deny_ip"` //拒绝访问的IP ,号隔开 + StartStatus int `json:"start_status"` //启动状态 如果是1 启动 ; 如果是0 不启动 + ConnTimeout int `json:"conn_timeout"` // 连接超时时间,单位秒 60s + ReadTimeout int `json:"read_timeout"` // 读取超时时间,单位秒 0 表示不限制 + WriteTimeout int `json:"write_timeout"` // 写入超时时间,单位秒 0 表示不限制 + MaxInConnect int `json:"max_in_connect"` // 最大入站连接数 0 表示不限制 + MaxOutConnect int `json:"max_out_connect"` // 最大出站连接数 0 表示不限制 + AllowedTimeRanges string `gorm:"size:255" json:"allowed_time_ranges"` // 允许访问的时间段 格式: 08:00-10:00;11:00-12:00 空表示24小时可访问 + IpVersion string `gorm:"size:20" json:"ip_version"` // IP版本支持: "ipv4" 仅IPv4, "ipv6" 仅IPv6, "both" 同时支持IPv4和IPv6, 默认为"both" + Remark string `gorm:"size:500" json:"remark"` //备注 + SSLStatus int `json:"ssl_status"` // SSL开关 0关闭 1开启 + SSLCertificate string `gorm:"size:500" json:"ssl_certificate"` // SSL证书路径 + SSLCertificateKey string `gorm:"size:500" json:"ssl_certificate_key"` // SSL密钥路径 + SSLProtocols string `gorm:"size:100" json:"ssl_protocols"` // SSL协议版本 如 TLSv1.2 TLSv1.3 } diff --git a/model/waf_sys.go b/model/waf_sys.go index d5a952c..e1b13e8 100644 --- a/model/waf_sys.go +++ b/model/waf_sys.go @@ -6,6 +6,6 @@ import ( type WafSysLog struct { baseorm.BaseOrm - OpType string `json:"op_type"` //操作类型 - OpContent string `json:"op_content"` //操作内容 + OpType string `gorm:"size:100" json:"op_type"` //操作类型 + OpContent string `gorm:"type:text" json:"op_content"` //操作内容 } diff --git a/wafconfig/wafconfig.go b/wafconfig/wafconfig.go index 4566725..61414f3 100644 --- a/wafconfig/wafconfig.go +++ b/wafconfig/wafconfig.go @@ -210,13 +210,77 @@ func LoadAndInitConfig() { return } fmt.Printf("%s\tINFO\t config updated\n", currentTime) + configChanged = false } fmt.Printf("%s\tINFO\tuser_code:%s ,soft_id:%s\n", currentTime, global.GWAF_USER_CODE, global.GWAF_TENANT_ID) - // 注册数据库方言(默认 SQLite;Milestone 2 起按 GWAF_DB_DRIVER 分流) - dialect.Register(&dialect.SQLiteDialect{}) + // ── 数据库 driver 配置 ────────────────────────────────────────────── + if config.IsSet("database.driver") { + global.GWAF_DB_DRIVER = config.GetString("database.driver") + } else { + config.Set("database.driver", global.GWAF_DB_DRIVER) // 写入默认值 sqlite + configChanged = true + } + + if global.GWAF_DB_DRIVER == "mysql" { + // MySQL 连接参数(有默认值,仅在 config.yml 有对应键时才覆盖) + if config.IsSet("database.mysql.host") { + global.GWAF_MYSQL_HOST = config.GetString("database.mysql.host") + } + if config.IsSet("database.mysql.port") { + global.GWAF_MYSQL_PORT = config.GetInt("database.mysql.port") + } + if config.IsSet("database.mysql.user") { + global.GWAF_MYSQL_USER = config.GetString("database.mysql.user") + } + if config.IsSet("database.mysql.password") { + global.GWAF_MYSQL_PASSWORD = config.GetString("database.mysql.password") + } + if config.IsSet("database.mysql.charset") { + global.GWAF_MYSQL_CHARSET = config.GetString("database.mysql.charset") + } + if config.IsSet("database.mysql.core_db") { + global.GWAF_MYSQL_CORE_DB = config.GetString("database.mysql.core_db") + } + if config.IsSet("database.mysql.log_db") { + global.GWAF_MYSQL_LOG_DB = config.GetString("database.mysql.log_db") + } + if config.IsSet("database.mysql.stats_db") { + global.GWAF_MYSQL_STATS_DB = config.GetString("database.mysql.stats_db") + } + if config.IsSet("database.mysql.max_open_conns") { + global.GWAF_MYSQL_MAX_OPEN_CONNS = config.GetInt("database.mysql.max_open_conns") + } + if config.IsSet("database.mysql.max_idle_conns") { + global.GWAF_MYSQL_MAX_IDLE_CONNS = config.GetInt("database.mysql.max_idle_conns") + } + if config.IsSet("database.mysql.conn_max_lifetime_minutes") { + global.GWAF_MYSQL_CONN_MAX_LIFETIME_MINUTES = config.GetInt("database.mysql.conn_max_lifetime_minutes") + } + } + + // 只有在配置发生变化时才写入文件(提前写,dialect 注册在下方) + if configChanged { + err := config.WriteConfig() + if err != nil { + fmt.Printf("%s\tERROR\twrite config failed:%v\n", currentTime, err) + return + } + fmt.Printf("%s\tINFO\t config updated\n", currentTime) + configChanged = false // 已写,重置标志 + } + + // 注册数据库方言 + switch global.GWAF_DB_DRIVER { + case "mysql": + dialect.Register(&dialect.MySQLDialect{}) + fmt.Printf("%s\tINFO\t数据库驱动: MySQL (%s:%d)\n", + currentTime, global.GWAF_MYSQL_HOST, global.GWAF_MYSQL_PORT) + default: + dialect.Register(&dialect.SQLiteDialect{}) + } } // UpdateIpWhitelist 更新IP白名单配置 diff --git a/wafdb/dialect/mysql_dialect.go b/wafdb/dialect/mysql_dialect.go new file mode 100644 index 0000000..ccd9792 --- /dev/null +++ b/wafdb/dialect/mysql_dialect.go @@ -0,0 +1,169 @@ +package dialect + +import ( + "fmt" + "strings" + "time" + + "gorm.io/gorm" +) + +// MySQLDialect implements DBDialect for MySQL 8.0+ databases. +type MySQLDialect struct{} + +func (d *MySQLDialect) Name() string { return "mysql" } +func (d *MySQLDialect) IsFileBased() bool { return false } +func (d *MySQLDialect) SupportsBackup() bool { return false } +func (d *MySQLDialect) SupportsRepair() bool { return false } + +func (d *MySQLDialect) ForceIndexClause(table, idx string) string { + return fmt.Sprintf("%s FORCE INDEX (%s)", table, idx) +} + +func (d *MySQLDialect) FormatTimeWithOffset(colExpr string, offsetMin int) string { + sign := "+" + h := offsetMin / 60 + m := offsetMin % 60 + if offsetMin < 0 { + sign = "-" + h = -h + m = -m + } + tz := fmt.Sprintf("%s%02d:%02d", sign, h, m) + return fmt.Sprintf("DATE_FORMAT(CONVERT_TZ(%s,'+00:00','%s'),'%%Y-%%m-%%d %%H:%%i:%%s')", colExpr, tz) +} + +func (d *MySQLDialect) RenameTable(db *gorm.DB, src, dst string) error { + return db.Exec(fmt.Sprintf("RENAME TABLE `%s` TO `%s`", src, dst)).Error +} + +func (d *MySQLDialect) ListTables(db *gorm.DB) ([]string, error) { + var tables []string + err := db.Raw( + "SELECT TABLE_NAME FROM information_schema.TABLES " + + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", + ).Scan(&tables).Error + return tables, err +} + +func (d *MySQLDialect) TableExists(db *gorm.DB, name string) bool { + var count int64 + db.Raw( + "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", name, + ).Scan(&count) + return count > 0 +} + +func (d *MySQLDialect) ColumnInfo(db *gorm.DB, table string) ([]ColumnMeta, error) { + rows, err := db.Raw( + "SELECT ORDINAL_POSITION - 1, COLUMN_NAME, DATA_TYPE, "+ + "IF(IS_NULLABLE='NO',1,0), IFNULL(COLUMN_DEFAULT,''), IF(COLUMN_KEY='PRI',1,0) "+ + "FROM information_schema.COLUMNS "+ + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION", + table, + ).Rows() + if err != nil { + return nil, err + } + defer rows.Close() + var cols []ColumnMeta + for rows.Next() { + var c ColumnMeta + var notNull, pk int + if err := rows.Scan(&c.Cid, &c.Name, &c.Type, ¬Null, &c.DefaultVal, &pk); err == nil { + c.NotNull = notNull == 1 + c.PrimaryKey = pk > 0 + cols = append(cols, c) + } + } + return cols, nil +} + +func (d *MySQLDialect) IndexInfo(db *gorm.DB, table string) ([]IndexMeta, error) { + rows, err := db.Raw( + "SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX - 1, COLUMN_NAME "+ + "FROM information_schema.STATISTICS "+ + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? "+ + "ORDER BY INDEX_NAME, SEQ_IN_INDEX", + table, + ).Rows() + if err != nil { + return nil, err + } + defer rows.Close() + + idxMap := make(map[string]*IndexMeta) + var order []string + for rows.Next() { + var idxName, colName string + var nonUnique, seqNo int + if err := rows.Scan(&idxName, &nonUnique, &seqNo, &colName); err != nil { + continue + } + if _, ok := idxMap[idxName]; !ok { + idxMap[idxName] = &IndexMeta{Name: idxName, Unique: nonUnique == 0} + order = append(order, idxName) + } + idxMap[idxName].Columns = append(idxMap[idxName].Columns, IndexColumnMeta{SeqNo: seqNo, Name: colName}) + } + + result := make([]IndexMeta, 0, len(order)) + for _, name := range order { + result = append(result, *idxMap[name]) + } + return result, nil +} + +func (d *MySQLDialect) CollectMetrics(db *gorm.DB, name, path string) (*DBMetrics, error) { + if db == nil { + return nil, fmt.Errorf("database connection is nil: %s", name) + } + m := &DBMetrics{ + DatabaseName: name, + Timestamp: time.Now(), + } + + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("get sql.DB failed: %v", err) + } + stats := sqlDB.Stats() + m.ConnectionCount = stats.OpenConnections + m.MaxConnections = stats.MaxOpenConnections + m.IdleConnections = stats.Idle + m.InUseConnections = stats.InUse + + db.Raw("SELECT @@VERSION").Scan(&m.EngineVersion) + + // Data and index size from information_schema + row := db.Raw( + "SELECT COALESCE(SUM(DATA_LENGTH),0)/1024/1024, COALESCE(SUM(INDEX_LENGTH),0)/1024/1024 " + + "FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()", + ).Row() + _ = row.Scan(&m.DataSizeMB, &m.IndexSizeMB) + + return m, nil +} + +// BuildMySQLDSN returns a MySQL DSN string. Always includes parseTime=True and loc=Local +// so customtype.JsonTime deserialises correctly. +func BuildMySQLDSN(host string, port int, user, password, dbName, charset string) string { + return fmt.Sprintf( + "%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s", + user, password, host, port, dbName, charset, + ) +} + +// BuildMySQLRootDSN returns a DSN that connects to the server without selecting a database, +// used for CREATE DATABASE IF NOT EXISTS. +func BuildMySQLRootDSN(host string, port int, user, password, charset string) string { + return fmt.Sprintf( + "%s:%s@tcp(%s:%d)/?charset=%s&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s", + user, password, host, port, charset, + ) +} + +// mysqlQuote wraps a MySQL identifier in back-ticks. +func mysqlQuote(name string) string { + return "`" + strings.ReplaceAll(name, "`", "``") + "`" +} diff --git a/wafdb/localdb.go b/wafdb/localdb.go index e7f5175..a7491e5 100644 --- a/wafdb/localdb.go +++ b/wafdb/localdb.go @@ -31,6 +31,9 @@ import ( ) func InitCoreDb(currentDir string) (bool, error) { + if dialect.Get().Name() == "mysql" { + return InitCoreDbMySQL() + } if currentDir == "" { currentDir = utils.GetCurrentDir() } @@ -120,6 +123,9 @@ func InitCoreDb(currentDir string) (bool, error) { } func InitLogDb(currentDir string) (bool, error) { + if dialect.Get().Name() == "mysql" { + return InitLogDbMySQL() + } if currentDir == "" { currentDir = utils.GetCurrentDir() } @@ -201,6 +207,10 @@ func InitLogDb(currentDir string) (bool, error) { // 手工切换日志数据源 func InitManaulLogDb(currentDir string, custFileName string) { + if dialect.Get().Name() != "sqlite" { + // MySQL 模式下所有日志写入同一个库,无需手动切换分库 + return + } if currentDir == "" { currentDir = utils.GetCurrentDir() } @@ -248,6 +258,9 @@ func InitManaulLogDb(currentDir string, custFileName string) { } func InitStatsDb(currentDir string) (bool, error) { + if dialect.Get().Name() == "mysql" { + return InitStatsDbMySQL() + } if currentDir == "" { currentDir = utils.GetCurrentDir() } diff --git a/wafdb/migrations_core.go b/wafdb/migrations_core.go index 4138e72..b1b4d76 100644 --- a/wafdb/migrations_core.go +++ b/wafdb/migrations_core.go @@ -152,6 +152,22 @@ func RunCoreDBMigrations(db *gorm.DB) error { ) }, }, + // 迁移1b: 同步 MySQL ip_tags 表结构(补全缺失列 + 修复 longtext → varchar,支持建索引) + { + ID: "202511140001b_fix_mysql_varchar", + Migrate: func(tx *gorm.DB) error { + if tx.Dialector.Name() != "mysql" { + return nil + } + zlog.Info("迁移 202511140001b: 同步 ip_tags 表结构") + if err := tx.AutoMigrate(&model.IPTag{}); err != nil { + return fmt.Errorf("同步 ip_tags 表结构失败: %w", err) + } + zlog.Info("迁移 202511140001b: ip_tags 表结构同步完成") + return nil + }, + Rollback: func(tx *gorm.DB) error { return nil }, + }, // 迁移2: 创建索引(幂等操作) { ID: "202511140002_create_core_indexes", @@ -204,13 +220,16 @@ func RunCoreDBMigrations(db *gorm.DB) error { } // 创建索引 - if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_ip ON firewall_ip_block(ip)").Error; err != nil { + if err := safeCreateIndex(tx, "firewall_ip_block", "idx_firewall_ip_block_ip", + "CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_ip ON firewall_ip_block(ip)"); err != nil { zlog.Warn("创建索引 idx_firewall_ip_block_ip 失败", "error", err.Error()) } - if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_status ON firewall_ip_block(status)").Error; err != nil { + if err := safeCreateIndex(tx, "firewall_ip_block", "idx_firewall_ip_block_status", + "CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_status ON firewall_ip_block(status)"); err != nil { zlog.Warn("创建索引 idx_firewall_ip_block_status 失败", "error", err.Error()) } - if err := tx.Exec("CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_expire_time ON firewall_ip_block(expire_time)").Error; err != nil { + if err := safeCreateIndex(tx, "firewall_ip_block", "idx_firewall_ip_block_expire_time", + "CREATE INDEX IF NOT EXISTS idx_firewall_ip_block_expire_time ON firewall_ip_block(expire_time)"); err != nil { zlog.Warn("创建索引 idx_firewall_ip_block_expire_time 失败", "error", err.Error()) } @@ -955,8 +974,7 @@ func createCoreIndexes(tx *gorm.DB) error { zlog.Info("开始创建索引", "index", idx.Name, "sql", idx.SQL) indexStartTime := time.Now() - if err := tx.Exec(idx.SQL).Error; err != nil { - // 记录详细的错误信息 + if err := safeCreateIndex(tx, "ip_tags", idx.Name, idx.SQL); err != nil { errMsg := fmt.Sprintf("创建索引失败 %s: %v (错误类型: %T)", idx.Name, err, err) zlog.Error("索引创建失败详情", "index", idx.Name, "error", err.Error(), "sql", idx.SQL) return fmt.Errorf("%s", errMsg) @@ -983,7 +1001,7 @@ func cleanupDuplicateIPTags(tx *gorm.DB) error { FROM ip_tags GROUP BY user_code, tenant_id, ip, ip_tag HAVING cnt > 1 - ) + ) AS t `).Scan(&duplicateCount).Error if err != nil { @@ -1019,16 +1037,16 @@ func cleanupDuplicateIPTags(tx *gorm.DB) error { func dropCoreIndexes(tx *gorm.DB) error { zlog.Info("开始删除core索引") - indexes := []string{ - "uni_iptags_full", - "idx_iptag_ip", + indexes := []struct{ table, name string }{ + {"ip_tags", "uni_iptags_full"}, + {"ip_tags", "idx_iptag_ip"}, } - for _, indexName := range indexes { - if err := tx.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)).Error; err != nil { - zlog.Warn("删除索引失败(可能不存在)", "index", indexName, "error", err) + for _, idx := range indexes { + if err := safeDropIndex(tx, idx.table, idx.name); err != nil { + zlog.Warn("删除索引失败(可能不存在)", "index", idx.name, "error", err) } else { - zlog.Info("索引删除成功", "index", indexName) + zlog.Info("索引删除成功", "index", idx.name) } } diff --git a/wafdb/migrations_helper.go b/wafdb/migrations_helper.go new file mode 100644 index 0000000..edb6a42 --- /dev/null +++ b/wafdb/migrations_helper.go @@ -0,0 +1,42 @@ +package wafdb + +import ( + "SamWaf/wafdb/dialect" + "fmt" + "strings" + + "gorm.io/gorm" +) + +// checkIndexExists reports whether indexName exists on tableName. +// Uses GORM's dialect-aware Migrator so it works on SQLite, MySQL, and SQL Server. +func checkIndexExists(db *gorm.DB, tableName, indexName string) bool { + return db.Migrator().HasIndex(tableName, indexName) +} + +// safeCreateIndex creates the named index only when it does not already exist. +// createSQL must be a SQLite-style "CREATE [UNIQUE] INDEX IF NOT EXISTS ..." statement; +// for MySQL the "IF NOT EXISTS" clause is stripped before execution because MySQL +// does not support it on CREATE INDEX. +func safeCreateIndex(tx *gorm.DB, table, indexName, createSQL string) error { + if tx.Migrator().HasIndex(table, indexName) { + return nil + } + execSQL := createSQL + if dialect.Get().Name() == "mysql" { + execSQL = strings.Replace(createSQL, "IF NOT EXISTS ", "", 1) + } + return tx.Exec(execSQL).Error +} + +// safeDropIndex drops the named index only when it exists. +// MySQL requires the table name in the DROP statement; SQLite uses IF NOT EXISTS. +func safeDropIndex(tx *gorm.DB, table, indexName string) error { + if !tx.Migrator().HasIndex(table, indexName) { + return nil + } + if dialect.Get().Name() == "mysql" { + return tx.Exec(fmt.Sprintf("DROP INDEX `%s` ON `%s`", indexName, table)).Error + } + return tx.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)).Error +} diff --git a/wafdb/migrations_log.go b/wafdb/migrations_log.go index 0e317cf..823acf0 100644 --- a/wafdb/migrations_log.go +++ b/wafdb/migrations_log.go @@ -71,6 +71,48 @@ func RunLogDBMigrations(db *gorm.DB) error { ) }, }, + // 迁移1b: 同步 MySQL web_logs 表结构(补全缺失列 + 修复 longtext → varchar,支持建索引) + { + ID: "202511140001b_fix_mysql_varchar", + Migrate: func(tx *gorm.DB) error { + if tx.Dialector.Name() != "mysql" { + return nil + } + zlog.Info("迁移 202511140001b: 同步 web_logs 表结构") + if err := tx.AutoMigrate(&innerbean.WebLog{}); err != nil { + return fmt.Errorf("同步 web_logs 表结构失败: %w", err) + } + zlog.Info("迁移 202511140001b: web_logs 表结构同步完成") + return nil + }, + Rollback: func(tx *gorm.DB) error { return nil }, + }, + // 迁移1c: 确保 guest_identification 列存在(兼容旧表列名拼写错误 guest_id_entification) + { + ID: "202511140001c_fix_guest_identification_col", + Migrate: func(tx *gorm.DB) error { + if tx.Dialector.Name() != "mysql" { + return nil + } + // 检查目标列是否已存在 + var newColCount int64 + tx.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'web_logs' AND column_name = 'guest_identification'").Scan(&newColCount) + if newColCount > 0 { + zlog.Info("迁移 202511140001c: guest_identification 列已存在,跳过") + return nil + } + // 检查旧拼写错误列 guest_id_entification 是否存在 + var oldColCount int64 + tx.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'web_logs' AND column_name = 'guest_id_entification'").Scan(&oldColCount) + if oldColCount > 0 { + zlog.Info("迁移 202511140001c: 将 guest_id_entification 重命名为 guest_identification(保留数据)") + return tx.Exec("ALTER TABLE `web_logs` CHANGE COLUMN `guest_id_entification` `guest_identification` VARCHAR(191) NOT NULL DEFAULT ''").Error + } + zlog.Info("迁移 202511140001c: 添加 guest_identification 列") + return tx.Exec("ALTER TABLE `web_logs` ADD COLUMN `guest_identification` VARCHAR(191) NOT NULL DEFAULT ''").Error + }, + Rollback: func(tx *gorm.DB) error { return nil }, + }, // 迁移2: 创建索引(幂等操作) { ID: "202511140002_create_log_indexes", @@ -278,7 +320,7 @@ func createLogIndexes(tx *gorm.DB) error { }, { Name: "idx_web_guest_id_entification", - SQL: "CREATE INDEX IF NOT EXISTS idx_web_guest_id_entification ON web_logs (guest_id_entification, day, is_bot, host_code)", + SQL: "CREATE INDEX IF NOT EXISTS idx_web_guest_id_entification ON web_logs (guest_identification, day, is_bot, host_code)", }, } @@ -286,8 +328,7 @@ func createLogIndexes(tx *gorm.DB) error { zlog.Info("开始创建索引", "index", idx.Name, "sql", idx.SQL) indexStartTime := time.Now() - if err := tx.Exec(idx.SQL).Error; err != nil { - // 记录详细的错误信息 + if err := safeCreateIndex(tx, "web_logs", idx.Name, idx.SQL); err != nil { errMsg := fmt.Sprintf("创建索引失败 %s: %v (错误类型: %T)", idx.Name, err, err) zlog.Error("索引创建失败详情", "index", idx.Name, "error", err.Error(), "sql", idx.SQL) return fmt.Errorf("%s", errMsg) @@ -317,7 +358,7 @@ func dropLogIndexes(tx *gorm.DB) error { } for _, indexName := range indexes { - if err := tx.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)).Error; err != nil { + if err := safeDropIndex(tx, "web_logs", indexName); err != nil { zlog.Warn("删除索引失败(可能不存在)", "index", indexName, "error", err) } else { zlog.Info("索引删除成功", "index", indexName) diff --git a/wafdb/migrations_stats.go b/wafdb/migrations_stats.go index 8b8bc8e..6f59684 100644 --- a/wafdb/migrations_stats.go +++ b/wafdb/migrations_stats.go @@ -74,6 +74,27 @@ func RunStatsDBMigrations(db *gorm.DB) error { ) }, }, + // 迁移1b: 同步 MySQL stats 各表结构(补全缺失列 + 修复 longtext → varchar,支持建索引) + { + ID: "202511110001b_fix_mysql_varchar", + Migrate: func(tx *gorm.DB) error { + if tx.Dialector.Name() != "mysql" { + return nil + } + zlog.Info("迁移 202511110001b: 同步 stats 各表结构") + if err := tx.AutoMigrate( + &model.StatsDay{}, + &model.StatsIPDay{}, + &model.StatsIPCityDay{}, + &model.IPTag{}, + ); err != nil { + return fmt.Errorf("同步 stats 表结构失败: %w", err) + } + zlog.Info("迁移 202511110001b: stats 表结构同步完成") + return nil + }, + Rollback: func(tx *gorm.DB) error { return nil }, + }, // 迁移2: 创建索引(幂等操作) { ID: "202511110002_create_indexes", @@ -102,12 +123,18 @@ func RunStatsDBMigrations(db *gorm.DB) error { return fmt.Errorf("创建站点统计表失败: %w", err) } zlog.Info("迁移 202603100001: 创建站点统计索引") - sqls := []string{ - "CREATE INDEX IF NOT EXISTS idx_stats_site_days_lookup ON stats_site_days (host_code, day)", - "CREATE INDEX IF NOT EXISTS idx_stats_site_hours_lookup ON stats_site_hours (host_code, hour_time)", + siteIdxs := []struct{ name, table, sql string }{ + { + "idx_stats_site_days_lookup", "stats_site_days", + "CREATE INDEX IF NOT EXISTS idx_stats_site_days_lookup ON stats_site_days (host_code, day)", + }, + { + "idx_stats_site_hours_lookup", "stats_site_hours", + "CREATE INDEX IF NOT EXISTS idx_stats_site_hours_lookup ON stats_site_hours (host_code, hour_time)", + }, } - for _, s := range sqls { - if err := tx.Exec(s).Error; err != nil { + for _, idx := range siteIdxs { + if err := safeCreateIndex(tx, idx.table, idx.name, idx.sql); err != nil { return fmt.Errorf("创建站点统计索引失败: %w", err) } } @@ -134,19 +161,20 @@ func RunStatsDBMigrations(db *gorm.DB) error { }) indexes := []struct { - name string - sql string + name string + table string + sql string }{ - {"idx_stats_ip_days_day", "CREATE INDEX IF NOT EXISTS idx_stats_ip_days_day ON stats_ip_days(day)"}, - {"idx_stats_ip_city_days_day", "CREATE INDEX IF NOT EXISTS idx_stats_ip_city_days_day ON stats_ip_city_days(day)"}, - {"idx_ip_tags_create_time", "CREATE INDEX IF NOT EXISTS idx_ip_tags_create_time ON ip_tags(create_time)"}, - {"idx_ip_tags_update_time", "CREATE INDEX IF NOT EXISTS idx_ip_tags_update_time ON ip_tags(update_time)"}, + {"idx_stats_ip_days_day", "stats_ip_days", "CREATE INDEX IF NOT EXISTS idx_stats_ip_days_day ON stats_ip_days(day)"}, + {"idx_stats_ip_city_days_day", "stats_ip_city_days", "CREATE INDEX IF NOT EXISTS idx_stats_ip_city_days_day ON stats_ip_city_days(day)"}, + {"idx_ip_tags_create_time", "ip_tags", "CREATE INDEX IF NOT EXISTS idx_ip_tags_create_time ON ip_tags(create_time)"}, + {"idx_ip_tags_update_time", "ip_tags", "CREATE INDEX IF NOT EXISTS idx_ip_tags_update_time ON ip_tags(update_time)"}, } for _, idx := range indexes { zlog.Info("创建索引中...", "index", idx.name) start := time.Now() - if err := ddlDB.Exec(idx.sql).Error; err != nil { + if err := safeCreateIndex(ddlDB, idx.table, idx.name, idx.sql); err != nil { return fmt.Errorf("创建清理索引 %s 失败: %w", idx.name, err) } zlog.Info("索引创建完成", "index", idx.name, "耗时", time.Since(start).String()) @@ -218,20 +246,6 @@ func checkStatsIndexesExist(db *gorm.DB) bool { return true } -// checkIndexExists 检查指定表的索引是否存在(SQLite) -func checkIndexExists(db *gorm.DB, tableName, indexName string) bool { - var count int64 - // SQLite 查询索引的方法 - err := db.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name=? AND tbl_name=?", - indexName, tableName).Scan(&count).Error - - if err != nil { - zlog.Error("检查索引失败", "error", err, "table", tableName, "index", indexName) - return false - } - return count > 0 -} - // createStatsIndexes 创建所有stats索引(幂等操作) func createStatsIndexes(tx *gorm.DB) error { zlog.Info("开始创建stats索引(可能需要几分钟)...") @@ -243,28 +257,34 @@ func createStatsIndexes(tx *gorm.DB) error { } indexes := []struct { - Name string - SQL string + Table string + Name string + SQL string }{ { - Name: "idx_stats_days_lookup", - SQL: "CREATE INDEX IF NOT EXISTS idx_stats_days_lookup ON stats_days (tenant_id, user_code, host_code, type, day)", + Table: "stats_days", + Name: "idx_stats_days_lookup", + SQL: "CREATE INDEX IF NOT EXISTS idx_stats_days_lookup ON stats_days (tenant_id, user_code, host_code, type, day)", }, { - Name: "idx_stats_ip_days_lookup", - SQL: "CREATE INDEX IF NOT EXISTS idx_stats_ip_days_lookup ON stats_ip_days (tenant_id, user_code, host_code, ip, type, day)", + Table: "stats_ip_days", + Name: "idx_stats_ip_days_lookup", + SQL: "CREATE INDEX IF NOT EXISTS idx_stats_ip_days_lookup ON stats_ip_days (tenant_id, user_code, host_code, ip, type, day)", }, { - Name: "idx_stats_ip_city_days_lookup", - SQL: "CREATE INDEX IF NOT EXISTS idx_stats_ip_city_days_lookup ON stats_ip_city_days (tenant_id, user_code, host_code, country, province, city, type, day)", + Table: "stats_ip_city_days", + Name: "idx_stats_ip_city_days_lookup", + SQL: "CREATE INDEX IF NOT EXISTS idx_stats_ip_city_days_lookup ON stats_ip_city_days (tenant_id, user_code, host_code, country, province, city, type, day)", }, { - Name: "uni_iptags_full", - SQL: "CREATE UNIQUE INDEX IF NOT EXISTS uni_iptags_full ON ip_tags (user_code, tenant_id, ip, ip_tag)", + Table: "ip_tags", + Name: "uni_iptags_full", + SQL: "CREATE UNIQUE INDEX IF NOT EXISTS uni_iptags_full ON ip_tags (user_code, tenant_id, ip, ip_tag)", }, { - Name: "idx_iptag_ip", - SQL: "CREATE INDEX IF NOT EXISTS idx_iptag_ip ON ip_tags (user_code, tenant_id, ip)", + Table: "ip_tags", + Name: "idx_iptag_ip", + SQL: "CREATE INDEX IF NOT EXISTS idx_iptag_ip ON ip_tags (user_code, tenant_id, ip)", }, } @@ -272,8 +292,7 @@ func createStatsIndexes(tx *gorm.DB) error { zlog.Info("开始创建索引", "index", idx.Name, "sql", idx.SQL) indexStartTime := time.Now() - if err := tx.Exec(idx.SQL).Error; err != nil { - // 记录详细的错误信息 + if err := safeCreateIndex(tx, idx.Table, idx.Name, idx.SQL); err != nil { errMsg := fmt.Sprintf("创建索引失败 %s: %v (错误类型: %T)", idx.Name, err, err) zlog.Error("索引创建失败详情", "index", idx.Name, "error", err.Error(), "sql", idx.SQL) return fmt.Errorf("%s", errMsg) @@ -300,7 +319,7 @@ func cleanupDuplicateIPTagsInStats(tx *gorm.DB) error { FROM ip_tags GROUP BY user_code, tenant_id, ip, ip_tag HAVING cnt > 1 - ) + ) AS t `).Scan(&duplicateCount).Error if err != nil { @@ -336,19 +355,19 @@ func cleanupDuplicateIPTagsInStats(tx *gorm.DB) error { func dropStatsIndexes(tx *gorm.DB) error { zlog.Info("开始删除stats索引") - indexes := []string{ - "idx_stats_days_lookup", - "idx_stats_ip_days_lookup", - "idx_stats_ip_city_days_lookup", - "uni_iptags_full", - "idx_iptag_ip", + indexes := []struct{ table, name string }{ + {"stats_days", "idx_stats_days_lookup"}, + {"stats_ip_days", "idx_stats_ip_days_lookup"}, + {"stats_ip_city_days", "idx_stats_ip_city_days_lookup"}, + {"ip_tags", "uni_iptags_full"}, + {"ip_tags", "idx_iptag_ip"}, } - for _, indexName := range indexes { - if err := tx.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)).Error; err != nil { - zlog.Warn("删除索引失败(可能不存在)", "index", indexName, "error", err) + for _, idx := range indexes { + if err := safeDropIndex(tx, idx.table, idx.name); err != nil { + zlog.Warn("删除索引失败(可能不存在)", "index", idx.name, "error", err) } else { - zlog.Info("索引删除成功", "index", indexName) + zlog.Info("索引删除成功", "index", idx.name) } } diff --git a/wafdb/mysql_localdb.go b/wafdb/mysql_localdb.go new file mode 100644 index 0000000..2bed9a4 --- /dev/null +++ b/wafdb/mysql_localdb.go @@ -0,0 +1,325 @@ +package wafdb + +// MySQL-specific database initialisation. +// This file is compiled for all platforms; the build tag for MySQL driver +// activation is handled via the go.mod dependency on gorm.io/driver/mysql. + +import ( + "SamWaf/common/uuid" + "SamWaf/common/zlog" + "SamWaf/customtype" + "SamWaf/global" + "SamWaf/innerbean" + "SamWaf/model" + "SamWaf/model/baseorm" + "SamWaf/wafdb/dialect" + "fmt" + "strconv" + "strings" + "time" + + mysqldriver "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// mysqlServerInfo holds a parsed MySQL / MariaDB version string. +type mysqlServerInfo struct { + Major int + Minor int + Patch int + IsMariaDB bool + Raw string +} + +// detectServerInfo queries @@VERSION from an open GORM connection. +func detectServerInfo(db *gorm.DB) mysqlServerInfo { + var raw string + db.Raw("SELECT @@VERSION").Scan(&raw) + return parseMySQLVersion(raw) +} + +// parseMySQLVersion parses strings like: +// +// "8.0.32" → MySQL 8.0 +// "5.7.44-log" → MySQL 5.7 +// "10.6.12-MariaDB" → MariaDB 10.6 +func parseMySQLVersion(version string) mysqlServerInfo { + info := mysqlServerInfo{Raw: version} + info.IsMariaDB = strings.Contains(strings.ToLower(version), "mariadb") + parts := strings.SplitN(version, ".", 3) + if len(parts) >= 1 { + info.Major, _ = strconv.Atoi(leadingDigits(parts[0])) + } + if len(parts) >= 2 { + info.Minor, _ = strconv.Atoi(leadingDigits(parts[1])) + } + if len(parts) >= 3 { + info.Patch, _ = strconv.Atoi(leadingDigits(parts[2])) + } + return info +} + +// leadingDigits returns the leading ASCII digit sequence in s. +func leadingDigits(s string) string { + for i := 0; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + return s[:i] + } + } + return s +} + +// bestCollation returns the most appropriate utf8mb4 collation for this server: +// +// MySQL 8.0+ → utf8mb4_0900_ai_ci (default; best Unicode support) +// MySQL 5.x / MariaDB → utf8mb4_general_ci (universally available) +func (v mysqlServerInfo) bestCollation() string { + if !v.IsMariaDB && v.Major >= 8 { + return "utf8mb4_0900_ai_ci" + } + return "utf8mb4_general_ci" +} + +// label returns a human-readable flavour string for log messages. +func (v mysqlServerInfo) label() string { + if v.IsMariaDB { + return fmt.Sprintf("MariaDB %d.%d.%d", v.Major, v.Minor, v.Patch) + } + return fmt.Sprintf("MySQL %d.%d.%d", v.Major, v.Minor, v.Patch) +} + +// ensureMySQLDatabase makes sure dbName exists before we open a GORM connection to it. +// +// Strategy: +// 1. Connect to the server without selecting a database, detect the server +// version, pick the best collation for that version, then issue +// CREATE DATABASE IF NOT EXISTS. Succeeds when the user has CREATE privilege. +// 2. If step 1 fails (permission denied, or server refuses the collation for any +// reason), try connecting directly to the target database. +// • Direct connection OK → database already exists; user just lacks CREATE +// privilege — proceed normally. +// • Direct connection fails → database does not exist and cannot be created; +// log a clear hint with the exact SQL the user must run manually. +func ensureMySQLDatabase(dbName string) error { + rootDSN := dialect.BuildMySQLRootDSN( + global.GWAF_MYSQL_HOST, global.GWAF_MYSQL_PORT, + global.GWAF_MYSQL_USER, global.GWAF_MYSQL_PASSWORD, + global.GWAF_MYSQL_CHARSET, + ) + + // Step 1: connect without a database, detect version, auto-create. + rootDB, connErr := gorm.Open(mysqldriver.Open(rootDSN), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if connErr == nil { + sqlDB, _ := rootDB.DB() + defer sqlDB.Close() + + info := detectServerInfo(rootDB) + collation := info.bestCollation() + createSQL := fmt.Sprintf( + "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE %s", + dbName, collation, + ) + zlog.Info("mysql: 服务器信息", "version", info.label(), "collation", collation) + + if createErr := rootDB.Exec(createSQL).Error; createErr == nil { + zlog.Info("mysql: 数据库已就绪(新建或已存在)", "db", dbName) + return nil + } else { + zlog.Warn("mysql: 自动建库失败,尝试直连确认库是否已存在", + "db", dbName, "error", createErr) + } + } else { + zlog.Warn("mysql: 无 database 根连接失败,尝试直连目标库", + "db", dbName, "error", connErr) + } + + // Step 2: fall back — try connecting directly to the target database. + directDSN := dialect.BuildMySQLDSN( + global.GWAF_MYSQL_HOST, global.GWAF_MYSQL_PORT, + global.GWAF_MYSQL_USER, global.GWAF_MYSQL_PASSWORD, + dbName, global.GWAF_MYSQL_CHARSET, + ) + directDB, directErr := gorm.Open(mysqldriver.Open(directDSN), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if directErr == nil { + directSQLDB, _ := directDB.DB() + directSQLDB.Close() + zlog.Info("mysql: 数据库已存在,跳过自动建库", "db", dbName) + return nil + } + + // Step 3: nothing worked — emit actionable guidance and abort. + // Use utf8mb4_general_ci in the hint: it works on all MySQL/MariaDB versions. + hintSQL := fmt.Sprintf( + "CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci", + dbName, + ) + hint := fmt.Sprintf( + "\n========================================================\n"+ + " 数据库 `%s` 不存在,且当前账户无权自动创建。\n"+ + " 请用 MySQL root 账户执行以下 SQL,然后重启 SamWaf:\n\n"+ + " %s;\n"+ + " GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%%';\n"+ + " FLUSH PRIVILEGES;\n"+ + "========================================================", + dbName, hintSQL, dbName, global.GWAF_MYSQL_USER, + ) + fmt.Println(hint) // also print to stdout so it's visible even without log viewer + zlog.Error("mysql: 无法连接或创建数据库", "db", dbName, "hint", hint) + return fmt.Errorf("mysql: 数据库 `%s` 不存在且权限不足,请手动建库后重启(详见日志)", dbName) +} + +// openMySQLDB opens a GORM connection to the named MySQL database, +// applying connection pool settings from global config. +func openMySQLDB(dbName string) (*gorm.DB, error) { + if err := ensureMySQLDatabase(dbName); err != nil { + return nil, err + } + dsn := dialect.BuildMySQLDSN( + global.GWAF_MYSQL_HOST, global.GWAF_MYSQL_PORT, + global.GWAF_MYSQL_USER, global.GWAF_MYSQL_PASSWORD, + dbName, global.GWAF_MYSQL_CHARSET, + ) + db, err := gorm.Open(mysqldriver.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("mysql: 打开数据库 %s 失败: %w", dbName, err) + } + + sqlDB, _ := db.DB() + sqlDB.SetMaxOpenConns(global.GWAF_MYSQL_MAX_OPEN_CONNS) + sqlDB.SetMaxIdleConns(global.GWAF_MYSQL_MAX_IDLE_CONNS) + sqlDB.SetConnMaxLifetime(time.Duration(global.GWAF_MYSQL_CONN_MAX_LIFETIME_MINUTES) * time.Minute) + // 空闲连接最多存活 3 分钟,远小于 MySQL 默认 wait_timeout(8h), + // 避免 MySQL 主动关闭后 Go 连接池仍持有死连接,导致 "bad connection" + sqlDB.SetConnMaxIdleTime(3 * time.Minute) + + return db, nil +} + +// applyGormLogger attaches the custom GormZLogger (and debug mode) to db. +func applyGormLogger(db *gorm.DB) *gorm.DB { + gormLogger := NewGormZLogger() + if global.GWAF_LOG_DEBUG_DB_ENABLE { + gormLogger = gormLogger.LogMode(logger.Info).(*GormZLogger) + db = db.Session(&gorm.Session{Logger: gormLogger}) + } + return db +} + +// InitCoreDbMySQL initialises the MySQL core database. +func InitCoreDbMySQL() (bool, error) { + if global.GWAF_LOCAL_DB != nil { + return false, nil + } + + dbName := global.GWAF_MYSQL_CORE_DB + zlog.Info("mysql: 初始化核心数据库", "db", dbName) + + db, err := openMySQLDB(dbName) + if err != nil { + return false, err + } + db = applyGormLogger(db) + global.GWAF_LOCAL_DB = db + + zlog.Info("开始执行core数据库迁移...") + if err := RunCoreDBMigrations(db); err != nil { + panic("core database migration failed: " + err.Error()) + } + zlog.Info("开始执行任务初始化迁移...") + if err := RunTaskInitMigrations(db); err != nil { + panic("task initialization migration failed: " + err.Error()) + } + + global.GWAF_LOCAL_DB.Callback().Query().Before("gorm:query").Register("tenant_plugin:before_query", before_query) + global.GWAF_LOCAL_DB.Callback().Query().Before("gorm:update").Register("tenant_plugin:before_update", before_update) + + db.Where("user_code = ? and rule_status = 999", global.GWAF_USER_CODE).Delete(model.Rules{}) + pathCoreSql(db) + return false, nil +} + +// InitLogDbMySQL initialises the MySQL log database. +func InitLogDbMySQL() (bool, error) { + if global.GWAF_LOCAL_LOG_DB != nil { + return false, nil + } + + dbName := global.GWAF_MYSQL_LOG_DB + zlog.Info("mysql: 初始化日志数据库", "db", dbName) + + db, err := openMySQLDB(dbName) + if err != nil { + return false, err + } + db = applyGormLogger(db) + global.GWAF_LOCAL_LOG_DB = db + + zlog.Info("开始执行log数据库迁移...") + if err := RunLogDBMigrations(db); err != nil { + panic("log database migration failed: " + err.Error()) + } + + global.GWAF_LOCAL_LOG_DB.Callback().Query().Before("gorm:query").Register("tenant_plugin:before_query", before_query) + global.GWAF_LOCAL_LOG_DB.Callback().Query().Before("gorm:update").Register("tenant_plugin:before_update", before_update) + + pathLogSql(db) + + // Ensure a ShareDb entry exists for the MySQL log database. + var total int64 + global.GWAF_LOCAL_DB.Model(&model.ShareDb{}).Count(&total) + if total == 0 { + var logTotal int64 + global.GWAF_LOCAL_LOG_DB.Model(&innerbean.WebLog{}).Count(&logTotal) + + sharDbBean := model.ShareDb{ + BaseOrm: baseorm.BaseOrm{ + Id: uuid.GenUUID(), + USER_CODE: global.GWAF_USER_CODE, + Tenant_ID: global.GWAF_TENANT_ID, + CREATE_TIME: customtype.JsonTime(time.Now()), + UPDATE_TIME: customtype.JsonTime(time.Now()), + }, + DbLogicType: "log", + StartTime: customtype.JsonTime(time.Now()), + EndTime: customtype.JsonTime(time.Now()), + FileName: dbName, // MySQL DB name (no .db extension) + Cnt: logTotal, + } + global.GWAF_LOCAL_DB.Create(sharDbBean) + } + + return false, nil +} + +// InitStatsDbMySQL initialises the MySQL statistics database. +func InitStatsDbMySQL() (bool, error) { + if global.GWAF_LOCAL_STATS_DB != nil { + return false, nil + } + + dbName := global.GWAF_MYSQL_STATS_DB + zlog.Info("mysql: 初始化统计数据库", "db", dbName) + + db, err := openMySQLDB(dbName) + if err != nil { + return false, err + } + db = applyGormLogger(db) + global.GWAF_LOCAL_STATS_DB = db + + zlog.Info("开始执行stats数据库迁移...") + if err := RunStatsDBMigrations(db); err != nil { + panic("stats database migration failed: " + err.Error()) + } + + global.GWAF_LOCAL_STATS_DB.Callback().Query().Before("gorm:query").Register("tenant_plugin:before_query", before_query) + global.GWAF_LOCAL_STATS_DB.Callback().Query().Before("gorm:update").Register("tenant_plugin:before_update", before_update) + + pathStatsSql(db) + return false, nil +}