初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
)
|
||||
|
||||
// geoProbeTimeout 覆盖单次探测(含 fallback)的总时长。
|
||||
const geoProbeTimeout = 20 * time.Second
|
||||
|
||||
// geoResult 是代理出口地理探测结果;空值表示探测失败(前端显示「未知」)。
|
||||
type geoResult struct {
|
||||
Country string
|
||||
City string
|
||||
}
|
||||
|
||||
// geoEndpoint 是单个出口地理 API 的地址与解析器。
|
||||
type geoEndpoint struct {
|
||||
url string
|
||||
parse func([]byte) (geoResult, bool)
|
||||
}
|
||||
|
||||
// geoEndpoints 依次尝试的免费出口地理 API,第一个成功即返回。
|
||||
// 主选 ip-api.com:免费 45 req/min(按出口 IP 计)、直接返回中文,但仅提供 http;
|
||||
// 备选 ipwho.is:https、无显式限频,仅英文。均不带 IP 参数,即查代理出口本身。
|
||||
var geoEndpoints = []geoEndpoint{
|
||||
{url: "http://ip-api.com/json/?fields=status,country,city&lang=zh-CN", parse: parseIPAPI},
|
||||
{url: "https://ipwho.is/?fields=success,country,city", parse: parseIPWhois},
|
||||
}
|
||||
|
||||
// parseIPAPI 解析 ip-api.com 响应。
|
||||
func parseIPAPI(b []byte) (geoResult, bool) {
|
||||
var r struct {
|
||||
Status string `json:"status"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
if json.Unmarshal(b, &r) != nil || r.Status != "success" || r.Country == "" {
|
||||
return geoResult{}, false
|
||||
}
|
||||
return geoResult{Country: r.Country, City: r.City}, true
|
||||
}
|
||||
|
||||
// parseIPWhois 解析 ipwho.is 响应。
|
||||
func parseIPWhois(b []byte) (geoResult, bool) {
|
||||
var r struct {
|
||||
Success bool `json:"success"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
if json.Unmarshal(b, &r) != nil || !r.Success || r.Country == "" {
|
||||
return geoResult{}, false
|
||||
}
|
||||
return geoResult{Country: r.Country, City: r.City}, true
|
||||
}
|
||||
|
||||
// probeGeoOnce 经指定 client 请求单个 geo API;任何失败返回 false。
|
||||
func probeGeoOnce(ctx context.Context, hc *http.Client, url string, parse func([]byte) (geoResult, bool)) (geoResult, bool) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return geoResult{}, false
|
||||
}
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return geoResult{}, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return geoResult{}, false
|
||||
}
|
||||
return parse(body)
|
||||
}
|
||||
|
||||
// probeGeoAsync 后台探测出口地理并回填;创建 / 更新后调用,Wait 收敛。
|
||||
func (s *ProxyService) probeGeoAsync(id uint) {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), geoProbeTimeout)
|
||||
defer cancel()
|
||||
s.probeGeo(ctx, id)
|
||||
}()
|
||||
}
|
||||
|
||||
// probeGeo 经该代理链路依次尝试 geo API 并写回地区字段;
|
||||
// 请求全部失败也写空值+探测时间(前端显示「未知」),避免主机变更后残留旧地区;
|
||||
// client 构造失败(spec 非法)则不动数据。
|
||||
func (s *ProxyService) probeGeo(ctx context.Context, id uint) {
|
||||
spec, err := s.SpecOf(ctx, &id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hc := s.geoClientFor(spec)
|
||||
if hc == nil {
|
||||
return
|
||||
}
|
||||
res := geoResult{}
|
||||
for _, ep := range geoEndpoints {
|
||||
if r, ok := probeGeoOnce(ctx, hc, ep.url, ep.parse); ok {
|
||||
res = r
|
||||
break
|
||||
}
|
||||
}
|
||||
var row model.Proxy
|
||||
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row.Country, row.City, row.GeoAt = res.Country, res.City, &now
|
||||
s.db.WithContext(ctx).Save(&row)
|
||||
}
|
||||
|
||||
// Probe 同步重测代理出口地理并返回最新视图;供手动「重测」入口调用。
|
||||
func (s *ProxyService) Probe(ctx context.Context, id uint) (ProxyView, error) {
|
||||
var row model.Proxy
|
||||
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
return ProxyView{}, fmt.Errorf("find proxy %d: %w", id, err)
|
||||
}
|
||||
pctx, cancel := context.WithTimeout(ctx, geoProbeTimeout)
|
||||
defer cancel()
|
||||
s.probeGeo(pctx, id)
|
||||
if err := s.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
return ProxyView{}, fmt.Errorf("find proxy %d: %w", id, err)
|
||||
}
|
||||
return s.viewOf(ctx, &row)
|
||||
}
|
||||
Reference in New Issue
Block a user