| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- package admin
- import (
- "context"
- "fmt"
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // ExportData exports proxy-only data for migration.
- func (h *ProxyHandler) ExportData(c *gin.Context) {
- ctx := c.Request.Context()
- selectedIDs, err := parseProxyIDs(c)
- if err != nil {
- response.BadRequest(c, err.Error())
- return
- }
- var proxies []service.Proxy
- if len(selectedIDs) > 0 {
- proxies, err = h.getProxiesByIDs(ctx, selectedIDs)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- } else {
- protocol := c.Query("protocol")
- status := c.Query("status")
- search := strings.TrimSpace(c.Query("search"))
- sortBy := c.DefaultQuery("sort_by", "id")
- sortOrder := c.DefaultQuery("sort_order", "desc")
- if len(search) > 100 {
- search = search[:100]
- }
- proxies, err = h.listProxiesFiltered(ctx, protocol, status, search, sortBy, sortOrder)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- }
- // 构建 id→name 映射,用于导出备用代理 name
- proxyNameByID := make(map[int64]string, len(proxies))
- for i := range proxies {
- proxyNameByID[proxies[i].ID] = proxies[i].Name
- }
- dataProxies := make([]DataProxy, 0, len(proxies))
- for i := range proxies {
- p := proxies[i]
- key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password)
- var expiresAt *int64
- if p.ExpiresAt != nil {
- v := p.ExpiresAt.Unix()
- expiresAt = &v
- }
- var backupProxyName string
- if p.BackupProxyID != nil {
- backupProxyName = proxyNameByID[*p.BackupProxyID]
- }
- dataProxies = append(dataProxies, DataProxy{
- ProxyKey: key,
- Name: p.Name,
- Protocol: p.Protocol,
- Host: p.Host,
- Port: p.Port,
- Username: p.Username,
- Password: p.Password,
- Status: p.Status,
- ExpiresAt: expiresAt,
- FallbackMode: p.FallbackMode,
- BackupProxyName: backupProxyName,
- ExpiryWarnDays: p.ExpiryWarnDays,
- })
- }
- payload := DataPayload{
- ExportedAt: time.Now().UTC().Format(time.RFC3339),
- Proxies: dataProxies,
- Accounts: []DataAccount{},
- }
- response.Success(c, payload)
- }
- // ImportData imports proxy-only data for migration.
- func (h *ProxyHandler) ImportData(c *gin.Context) {
- type ProxyImportRequest struct {
- Data DataPayload `json:"data"`
- }
- var req ProxyImportRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := validateDataHeader(req.Data); err != nil {
- response.BadRequest(c, err.Error())
- return
- }
- ctx := c.Request.Context()
- result := DataImportResult{}
- existingProxies, err := h.listProxiesFiltered(ctx, "", "", "", "id", "desc")
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- proxyByKey := make(map[string]service.Proxy, len(existingProxies))
- // proxyNameToID 用于 backup_proxy_name 反查:DB 已有 + 本批次新建均会写入
- proxyNameToID := make(map[string]int64, len(existingProxies))
- for i := range existingProxies {
- p := existingProxies[i]
- key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password)
- proxyByKey[key] = p
- if p.Name != "" {
- proxyNameToID[p.Name] = p.ID
- }
- }
- latencyProbeIDs := make([]int64, 0, len(req.Data.Proxies))
- for i := range req.Data.Proxies {
- item := req.Data.Proxies[i]
- key := item.ProxyKey
- if key == "" {
- key = buildProxyKey(item.Protocol, item.Host, item.Port, item.Username, item.Password)
- }
- if err := validateDataProxy(item); err != nil {
- result.ProxyFailed++
- result.Errors = append(result.Errors, DataImportError{
- Kind: "proxy",
- Name: item.Name,
- ProxyKey: key,
- Message: err.Error(),
- })
- continue
- }
- normalizedStatus := normalizeProxyStatus(item.Status)
- if existing, ok := proxyByKey[key]; ok {
- result.ProxyReused++
- if normalizedStatus != "" && normalizedStatus != existing.Status {
- // 已存在代理同步 status 时,同时保留/覆盖导入 item 的完整字段,
- // 避免 UpdateProxy 零值覆盖有效期/fallback 配置。
- var existingExpiresAt *time.Time
- if item.ExpiresAt != nil {
- t := time.Unix(*item.ExpiresAt, 0).UTC()
- existingExpiresAt = &t
- }
- existingFallbackMode := item.FallbackMode
- if existingFallbackMode == "" {
- existingFallbackMode = service.FallbackModeNone
- }
- var existingBackupProxyID *int64
- if item.BackupProxyName != "" {
- if bid, ok := proxyNameToID[item.BackupProxyName]; ok {
- existingBackupProxyID = &bid
- }
- }
- updateInput := &service.UpdateProxyInput{
- Status: normalizedStatus,
- ExpiresAt: existingExpiresAt,
- ClearExpiresAt: existingExpiresAt == nil,
- FallbackMode: existingFallbackMode,
- BackupProxyID: existingBackupProxyID,
- ClearBackupID: existingBackupProxyID == nil,
- ExpiryWarnDays: &item.ExpiryWarnDays,
- // 保留已存在代理的网络配置字段
- Name: existing.Name,
- Protocol: existing.Protocol,
- Host: existing.Host,
- Port: existing.Port,
- Username: existing.Username,
- Password: existing.Password,
- }
- if _, err := h.adminService.UpdateProxy(ctx, existing.ID, updateInput); err != nil {
- result.Errors = append(result.Errors, DataImportError{
- Kind: "proxy",
- Name: item.Name,
- ProxyKey: key,
- Message: "update status failed: " + err.Error(),
- })
- }
- }
- latencyProbeIDs = append(latencyProbeIDs, existing.ID)
- continue
- }
- // 解析 expires_at(unix 秒 → *time.Time)
- var expiresAt *time.Time
- if item.ExpiresAt != nil {
- t := time.Unix(*item.ExpiresAt, 0).UTC()
- expiresAt = &t
- }
- // 解析 backup_proxy_name → backup_proxy_id
- fallbackMode := item.FallbackMode
- var backupProxyID *int64
- if item.BackupProxyName != "" {
- if bid, ok := proxyNameToID[item.BackupProxyName]; ok {
- backupProxyID = &bid
- } else {
- // 查不到备用代理:降级 fallback_mode=none,记录 warning
- fallbackMode = service.FallbackModeNone
- result.Errors = append(result.Errors, DataImportError{
- Kind: "proxy",
- Name: item.Name,
- ProxyKey: key,
- Message: fmt.Sprintf("backup_proxy_name %q not found, fallback_mode downgraded to none", item.BackupProxyName),
- })
- }
- }
- created, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{
- Name: defaultProxyName(item.Name),
- Protocol: item.Protocol,
- Host: item.Host,
- Port: item.Port,
- Username: item.Username,
- Password: item.Password,
- ExpiresAt: expiresAt,
- FallbackMode: fallbackMode,
- BackupProxyID: backupProxyID,
- ExpiryWarnDays: item.ExpiryWarnDays,
- })
- if err != nil {
- result.ProxyFailed++
- result.Errors = append(result.Errors, DataImportError{
- Kind: "proxy",
- Name: item.Name,
- ProxyKey: key,
- Message: err.Error(),
- })
- continue
- }
- result.ProxyCreated++
- proxyByKey[key] = *created
- // 把新建代理的 name 也加入反查表,供后续批内代理引用
- if created.Name != "" {
- proxyNameToID[created.Name] = created.ID
- }
- if normalizedStatus != "" && normalizedStatus != created.Status {
- // 新建后同步 status 时,传入完整字段,避免零值覆盖刚创建的有效期/fallback 配置。
- if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{
- Status: normalizedStatus,
- ExpiresAt: expiresAt,
- ClearExpiresAt: expiresAt == nil,
- FallbackMode: fallbackMode,
- BackupProxyID: backupProxyID,
- ClearBackupID: backupProxyID == nil,
- ExpiryWarnDays: &item.ExpiryWarnDays,
- Name: created.Name,
- Protocol: created.Protocol,
- Host: created.Host,
- Port: created.Port,
- Username: created.Username,
- Password: created.Password,
- }); err != nil {
- result.Errors = append(result.Errors, DataImportError{
- Kind: "proxy",
- Name: item.Name,
- ProxyKey: key,
- Message: "update status failed: " + err.Error(),
- })
- }
- }
- // CreateProxy already triggers a latency probe, avoid double probing here.
- }
- if len(latencyProbeIDs) > 0 {
- ids := append([]int64(nil), latencyProbeIDs...)
- go func() {
- for _, id := range ids {
- _, _ = h.adminService.TestProxy(context.Background(), id)
- }
- }()
- }
- response.Success(c, result)
- }
- func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
- if len(ids) == 0 {
- return []service.Proxy{}, nil
- }
- return h.adminService.GetProxiesByIDs(ctx, ids)
- }
- func parseProxyIDs(c *gin.Context) ([]int64, error) {
- values := c.QueryArray("ids")
- if len(values) == 0 {
- raw := strings.TrimSpace(c.Query("ids"))
- if raw != "" {
- values = []string{raw}
- }
- }
- if len(values) == 0 {
- return nil, nil
- }
- ids := make([]int64, 0, len(values))
- for _, item := range values {
- for _, part := range strings.Split(item, ",") {
- part = strings.TrimSpace(part)
- if part == "" {
- continue
- }
- id, err := strconv.ParseInt(part, 10, 64)
- if err != nil || id <= 0 {
- return nil, fmt.Errorf("invalid proxy id: %s", part)
- }
- ids = append(ids, id)
- }
- }
- return ids, nil
- }
- func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status, search, sortBy, sortOrder string) ([]service.Proxy, error) {
- page := 1
- pageSize := dataPageCap
- var out []service.Proxy
- sortBy = strings.TrimSpace(sortBy)
- useAccountCountSort := strings.EqualFold(sortBy, "account_count")
- for {
- if useAccountCountSort {
- items, total, err := h.adminService.ListProxiesWithAccountCount(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder)
- if err != nil {
- return nil, err
- }
- for i := range items {
- out = append(out, items[i].Proxy)
- }
- if len(out) >= int(total) || len(items) == 0 {
- break
- }
- } else {
- items, total, err := h.adminService.ListProxies(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder)
- if err != nil {
- return nil, err
- }
- out = append(out, items...)
- if len(out) >= int(total) || len(items) == 0 {
- break
- }
- }
- page++
- }
- return out, nil
- }
|