package console import ( "context" "database/sql" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" _ "github.com/go-sql-driver/mysql" // 注册 database/sql 的 "mysql" 驱动 _ "github.com/jackc/pgx/v5/stdlib" // 注册 database/sql 的 "pgx" 驱动 ) // appsTest 注册应用前的连通性自检: // - 应用地址(base_url):HTTP 探测,用于事件通知通道(配置更新等)是否可达; // - 业务库 DSN(service_dsn):连库 Ping,用于读取业务数据是否可访问。 // // 两项都通过,前端才放行保存。 func (this *serverComp) appsTest(c *gin.Context) { var req struct { BaseUrl string `json:"base_url"` ServiceDsn string `json:"service_dsn"` } _ = c.ShouldBindJSON(&req) apiOk, apiMsg := testAPIReachable(req.BaseUrl) dbOk, dbMsg := testDBReachable(req.ServiceDsn) writeOK(c, gin.H{ "ok": apiOk && dbOk, "api_ok": apiOk, "api_msg": apiMsg, "db_ok": dbOk, "db_msg": dbMsg, }) } // checkDeployment 部署行(app_registry)落库前校验,按 Status 分流;返回非空=不通过原因。 // 会就地把空 Status 规整为 "existing"。 // - existing:把已部署的应用区域加进来 → 复检地址可达 + 业务库可连(checkAppConn)。 // - pending :选一个部署目标环境待部署 → 只需选中环境(EnvId),允许 DSN/地址暂空(部署后回填)。 func checkDeployment(m *AppRegistry) string { if m.Status == "" { m.Status = "existing" } switch m.Status { case "pending": if m.EnvId == 0 { return "部署目标环境必填" } return "" case "existing": return checkAppConn(m) default: return "未知的部署状态: " + m.Status } } // checkAppConn 保存前服务端复检:应用地址可达 + 业务库可连,任一不过返回原因;空串表示通过。 // 与前端「测试连接」同一套逻辑,防止绕过前端直接调接口写入不可用的注册项。 func checkAppConn(m *AppRegistry) string { if ok, msg := testAPIReachable(m.BaseUrl); !ok { return "应用地址检测失败: " + msg } if ok, msg := testDBReachable(m.ServiceDsn); !ok { return "业务库检测失败: " + msg } return "" } // testAPIReachable HTTP 探测应用地址:能建立连接拿到任意响应即视为可达(含 4xx/5xx, // 说明服务在线,只是该路径不一定接 GET)。仅判断「主机/服务是否在线」。 func testAPIReachable(rawurl string) (bool, string) { rawurl = strings.TrimSpace(rawurl) if rawurl == "" { return false, "应用地址为空" } if !strings.HasPrefix(rawurl, "http://") && !strings.HasPrefix(rawurl, "https://") { return false, "地址需以 http:// 或 https:// 开头" } client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get(rawurl) if err != nil { return false, "不可达: " + err.Error() } defer resp.Body.Close() return true, "可达 (HTTP " + strconv.Itoa(resp.StatusCode) + ")" } // testDBReachable 用 database/sql 连业务库并 Ping(5s 超时),测完即关、不留连接。 func testDBReachable(dsn string) (bool, string) { dsn = strings.TrimSpace(dsn) if dsn == "" { return false, "DSN 为空" } driver := "mysql" if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { driver = "pgx" } db, err := sql.Open(driver, dsn) if err != nil { return false, "DSN 解析失败: " + err.Error() } defer db.Close() db.SetMaxOpenConns(1) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { return false, "连接失败: " + err.Error() } return true, "连接成功" }