proxy_data.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package admin
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. )
  12. // ExportData exports proxy-only data for migration.
  13. func (h *ProxyHandler) ExportData(c *gin.Context) {
  14. ctx := c.Request.Context()
  15. selectedIDs, err := parseProxyIDs(c)
  16. if err != nil {
  17. response.BadRequest(c, err.Error())
  18. return
  19. }
  20. var proxies []service.Proxy
  21. if len(selectedIDs) > 0 {
  22. proxies, err = h.getProxiesByIDs(ctx, selectedIDs)
  23. if err != nil {
  24. response.ErrorFrom(c, err)
  25. return
  26. }
  27. } else {
  28. protocol := c.Query("protocol")
  29. status := c.Query("status")
  30. search := strings.TrimSpace(c.Query("search"))
  31. sortBy := c.DefaultQuery("sort_by", "id")
  32. sortOrder := c.DefaultQuery("sort_order", "desc")
  33. if len(search) > 100 {
  34. search = search[:100]
  35. }
  36. proxies, err = h.listProxiesFiltered(ctx, protocol, status, search, sortBy, sortOrder)
  37. if err != nil {
  38. response.ErrorFrom(c, err)
  39. return
  40. }
  41. }
  42. // 构建 id→name 映射,用于导出备用代理 name
  43. proxyNameByID := make(map[int64]string, len(proxies))
  44. for i := range proxies {
  45. proxyNameByID[proxies[i].ID] = proxies[i].Name
  46. }
  47. dataProxies := make([]DataProxy, 0, len(proxies))
  48. for i := range proxies {
  49. p := proxies[i]
  50. key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password)
  51. var expiresAt *int64
  52. if p.ExpiresAt != nil {
  53. v := p.ExpiresAt.Unix()
  54. expiresAt = &v
  55. }
  56. var backupProxyName string
  57. if p.BackupProxyID != nil {
  58. backupProxyName = proxyNameByID[*p.BackupProxyID]
  59. }
  60. dataProxies = append(dataProxies, DataProxy{
  61. ProxyKey: key,
  62. Name: p.Name,
  63. Protocol: p.Protocol,
  64. Host: p.Host,
  65. Port: p.Port,
  66. Username: p.Username,
  67. Password: p.Password,
  68. Status: p.Status,
  69. ExpiresAt: expiresAt,
  70. FallbackMode: p.FallbackMode,
  71. BackupProxyName: backupProxyName,
  72. ExpiryWarnDays: p.ExpiryWarnDays,
  73. })
  74. }
  75. payload := DataPayload{
  76. ExportedAt: time.Now().UTC().Format(time.RFC3339),
  77. Proxies: dataProxies,
  78. Accounts: []DataAccount{},
  79. }
  80. response.Success(c, payload)
  81. }
  82. // ImportData imports proxy-only data for migration.
  83. func (h *ProxyHandler) ImportData(c *gin.Context) {
  84. type ProxyImportRequest struct {
  85. Data DataPayload `json:"data"`
  86. }
  87. var req ProxyImportRequest
  88. if err := c.ShouldBindJSON(&req); err != nil {
  89. response.BadRequest(c, "Invalid request: "+err.Error())
  90. return
  91. }
  92. if err := validateDataHeader(req.Data); err != nil {
  93. response.BadRequest(c, err.Error())
  94. return
  95. }
  96. ctx := c.Request.Context()
  97. result := DataImportResult{}
  98. existingProxies, err := h.listProxiesFiltered(ctx, "", "", "", "id", "desc")
  99. if err != nil {
  100. response.ErrorFrom(c, err)
  101. return
  102. }
  103. proxyByKey := make(map[string]service.Proxy, len(existingProxies))
  104. // proxyNameToID 用于 backup_proxy_name 反查:DB 已有 + 本批次新建均会写入
  105. proxyNameToID := make(map[string]int64, len(existingProxies))
  106. for i := range existingProxies {
  107. p := existingProxies[i]
  108. key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password)
  109. proxyByKey[key] = p
  110. if p.Name != "" {
  111. proxyNameToID[p.Name] = p.ID
  112. }
  113. }
  114. latencyProbeIDs := make([]int64, 0, len(req.Data.Proxies))
  115. for i := range req.Data.Proxies {
  116. item := req.Data.Proxies[i]
  117. key := item.ProxyKey
  118. if key == "" {
  119. key = buildProxyKey(item.Protocol, item.Host, item.Port, item.Username, item.Password)
  120. }
  121. if err := validateDataProxy(item); err != nil {
  122. result.ProxyFailed++
  123. result.Errors = append(result.Errors, DataImportError{
  124. Kind: "proxy",
  125. Name: item.Name,
  126. ProxyKey: key,
  127. Message: err.Error(),
  128. })
  129. continue
  130. }
  131. normalizedStatus := normalizeProxyStatus(item.Status)
  132. if existing, ok := proxyByKey[key]; ok {
  133. result.ProxyReused++
  134. if normalizedStatus != "" && normalizedStatus != existing.Status {
  135. // 已存在代理同步 status 时,同时保留/覆盖导入 item 的完整字段,
  136. // 避免 UpdateProxy 零值覆盖有效期/fallback 配置。
  137. var existingExpiresAt *time.Time
  138. if item.ExpiresAt != nil {
  139. t := time.Unix(*item.ExpiresAt, 0).UTC()
  140. existingExpiresAt = &t
  141. }
  142. existingFallbackMode := item.FallbackMode
  143. if existingFallbackMode == "" {
  144. existingFallbackMode = service.FallbackModeNone
  145. }
  146. var existingBackupProxyID *int64
  147. if item.BackupProxyName != "" {
  148. if bid, ok := proxyNameToID[item.BackupProxyName]; ok {
  149. existingBackupProxyID = &bid
  150. }
  151. }
  152. updateInput := &service.UpdateProxyInput{
  153. Status: normalizedStatus,
  154. ExpiresAt: existingExpiresAt,
  155. ClearExpiresAt: existingExpiresAt == nil,
  156. FallbackMode: existingFallbackMode,
  157. BackupProxyID: existingBackupProxyID,
  158. ClearBackupID: existingBackupProxyID == nil,
  159. ExpiryWarnDays: &item.ExpiryWarnDays,
  160. // 保留已存在代理的网络配置字段
  161. Name: existing.Name,
  162. Protocol: existing.Protocol,
  163. Host: existing.Host,
  164. Port: existing.Port,
  165. Username: existing.Username,
  166. Password: existing.Password,
  167. }
  168. if _, err := h.adminService.UpdateProxy(ctx, existing.ID, updateInput); err != nil {
  169. result.Errors = append(result.Errors, DataImportError{
  170. Kind: "proxy",
  171. Name: item.Name,
  172. ProxyKey: key,
  173. Message: "update status failed: " + err.Error(),
  174. })
  175. }
  176. }
  177. latencyProbeIDs = append(latencyProbeIDs, existing.ID)
  178. continue
  179. }
  180. // 解析 expires_at(unix 秒 → *time.Time)
  181. var expiresAt *time.Time
  182. if item.ExpiresAt != nil {
  183. t := time.Unix(*item.ExpiresAt, 0).UTC()
  184. expiresAt = &t
  185. }
  186. // 解析 backup_proxy_name → backup_proxy_id
  187. fallbackMode := item.FallbackMode
  188. var backupProxyID *int64
  189. if item.BackupProxyName != "" {
  190. if bid, ok := proxyNameToID[item.BackupProxyName]; ok {
  191. backupProxyID = &bid
  192. } else {
  193. // 查不到备用代理:降级 fallback_mode=none,记录 warning
  194. fallbackMode = service.FallbackModeNone
  195. result.Errors = append(result.Errors, DataImportError{
  196. Kind: "proxy",
  197. Name: item.Name,
  198. ProxyKey: key,
  199. Message: fmt.Sprintf("backup_proxy_name %q not found, fallback_mode downgraded to none", item.BackupProxyName),
  200. })
  201. }
  202. }
  203. created, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{
  204. Name: defaultProxyName(item.Name),
  205. Protocol: item.Protocol,
  206. Host: item.Host,
  207. Port: item.Port,
  208. Username: item.Username,
  209. Password: item.Password,
  210. ExpiresAt: expiresAt,
  211. FallbackMode: fallbackMode,
  212. BackupProxyID: backupProxyID,
  213. ExpiryWarnDays: item.ExpiryWarnDays,
  214. })
  215. if err != nil {
  216. result.ProxyFailed++
  217. result.Errors = append(result.Errors, DataImportError{
  218. Kind: "proxy",
  219. Name: item.Name,
  220. ProxyKey: key,
  221. Message: err.Error(),
  222. })
  223. continue
  224. }
  225. result.ProxyCreated++
  226. proxyByKey[key] = *created
  227. // 把新建代理的 name 也加入反查表,供后续批内代理引用
  228. if created.Name != "" {
  229. proxyNameToID[created.Name] = created.ID
  230. }
  231. if normalizedStatus != "" && normalizedStatus != created.Status {
  232. // 新建后同步 status 时,传入完整字段,避免零值覆盖刚创建的有效期/fallback 配置。
  233. if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{
  234. Status: normalizedStatus,
  235. ExpiresAt: expiresAt,
  236. ClearExpiresAt: expiresAt == nil,
  237. FallbackMode: fallbackMode,
  238. BackupProxyID: backupProxyID,
  239. ClearBackupID: backupProxyID == nil,
  240. ExpiryWarnDays: &item.ExpiryWarnDays,
  241. Name: created.Name,
  242. Protocol: created.Protocol,
  243. Host: created.Host,
  244. Port: created.Port,
  245. Username: created.Username,
  246. Password: created.Password,
  247. }); err != nil {
  248. result.Errors = append(result.Errors, DataImportError{
  249. Kind: "proxy",
  250. Name: item.Name,
  251. ProxyKey: key,
  252. Message: "update status failed: " + err.Error(),
  253. })
  254. }
  255. }
  256. // CreateProxy already triggers a latency probe, avoid double probing here.
  257. }
  258. if len(latencyProbeIDs) > 0 {
  259. ids := append([]int64(nil), latencyProbeIDs...)
  260. go func() {
  261. for _, id := range ids {
  262. _, _ = h.adminService.TestProxy(context.Background(), id)
  263. }
  264. }()
  265. }
  266. response.Success(c, result)
  267. }
  268. func (h *ProxyHandler) getProxiesByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
  269. if len(ids) == 0 {
  270. return []service.Proxy{}, nil
  271. }
  272. return h.adminService.GetProxiesByIDs(ctx, ids)
  273. }
  274. func parseProxyIDs(c *gin.Context) ([]int64, error) {
  275. values := c.QueryArray("ids")
  276. if len(values) == 0 {
  277. raw := strings.TrimSpace(c.Query("ids"))
  278. if raw != "" {
  279. values = []string{raw}
  280. }
  281. }
  282. if len(values) == 0 {
  283. return nil, nil
  284. }
  285. ids := make([]int64, 0, len(values))
  286. for _, item := range values {
  287. for _, part := range strings.Split(item, ",") {
  288. part = strings.TrimSpace(part)
  289. if part == "" {
  290. continue
  291. }
  292. id, err := strconv.ParseInt(part, 10, 64)
  293. if err != nil || id <= 0 {
  294. return nil, fmt.Errorf("invalid proxy id: %s", part)
  295. }
  296. ids = append(ids, id)
  297. }
  298. }
  299. return ids, nil
  300. }
  301. func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status, search, sortBy, sortOrder string) ([]service.Proxy, error) {
  302. page := 1
  303. pageSize := dataPageCap
  304. var out []service.Proxy
  305. sortBy = strings.TrimSpace(sortBy)
  306. useAccountCountSort := strings.EqualFold(sortBy, "account_count")
  307. for {
  308. if useAccountCountSort {
  309. items, total, err := h.adminService.ListProxiesWithAccountCount(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder)
  310. if err != nil {
  311. return nil, err
  312. }
  313. for i := range items {
  314. out = append(out, items[i].Proxy)
  315. }
  316. if len(out) >= int(total) || len(items) == 0 {
  317. break
  318. }
  319. } else {
  320. items, total, err := h.adminService.ListProxies(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder)
  321. if err != nil {
  322. return nil, err
  323. }
  324. out = append(out, items...)
  325. if len(out) >= int(total) || len(items) == 0 {
  326. break
  327. }
  328. }
  329. page++
  330. }
  331. return out, nil
  332. }