client_groups.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package service
  2. import (
  3. "encoding/json"
  4. "sort"
  5. "strings"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  9. )
  10. type GroupSummary struct {
  11. Name string `json:"name"`
  12. ClientCount int `json:"clientCount"`
  13. TrafficUsed int64 `json:"trafficUsed"`
  14. Up int64 `json:"up"`
  15. Down int64 `json:"down"`
  16. }
  17. func (s *ClientService) ListGroups() ([]GroupSummary, error) {
  18. db := database.GetDB()
  19. // email is unique in both clients and client_traffics, so the LEFT JOIN
  20. // never double-counts a client's traffic.
  21. var derived []GroupSummary
  22. if err := db.Table("clients AS c").
  23. Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used, COALESCE(SUM(ct.up), 0) AS up, COALESCE(SUM(ct.down), 0) AS down").
  24. Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
  25. Where("c.group_name <> ''").
  26. Group("c.group_name").
  27. Scan(&derived).Error; err != nil {
  28. return nil, err
  29. }
  30. var stored []model.ClientGroup
  31. if err := db.Find(&stored).Error; err != nil {
  32. return nil, err
  33. }
  34. type groupAgg struct {
  35. count int
  36. up int64
  37. down int64
  38. }
  39. baseUp := make(map[string]int64, len(stored))
  40. baseDown := make(map[string]int64, len(stored))
  41. merged := make(map[string]groupAgg, len(derived)+len(stored))
  42. for _, g := range stored {
  43. merged[g.Name] = groupAgg{}
  44. baseUp[g.Name] = g.ResetUp
  45. baseDown[g.Name] = g.ResetDown
  46. }
  47. for _, g := range derived {
  48. merged[g.Name] = groupAgg{count: g.ClientCount, up: g.Up, down: g.Down}
  49. }
  50. out := make([]GroupSummary, 0, len(merged))
  51. for name, agg := range merged {
  52. up := agg.up - baseUp[name]
  53. if up < 0 {
  54. up = 0
  55. }
  56. down := agg.down - baseDown[name]
  57. if down < 0 {
  58. down = 0
  59. }
  60. out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: up + down, Up: up, Down: down})
  61. }
  62. sort.Slice(out, func(i, j int) bool {
  63. return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
  64. })
  65. return out, nil
  66. }
  67. func (s *ClientService) EmailsByGroup(name string) ([]string, error) {
  68. name = strings.TrimSpace(name)
  69. if name == "" {
  70. return []string{}, nil
  71. }
  72. db := database.GetDB()
  73. var emails []string
  74. if err := db.Model(&model.ClientRecord{}).
  75. Where("group_name = ?", name).
  76. Order("email ASC").
  77. Pluck("email", &emails).Error; err != nil {
  78. return nil, err
  79. }
  80. if emails == nil {
  81. emails = []string{}
  82. }
  83. return emails, nil
  84. }
  85. func (s *ClientService) ResetGroupTraffic(name string) error {
  86. name = strings.TrimSpace(name)
  87. if name == "" {
  88. return common.NewError("group name is required")
  89. }
  90. db := database.GetDB()
  91. var agg struct {
  92. Up int64
  93. Down int64
  94. }
  95. if err := db.Table("clients AS c").
  96. Select("COALESCE(SUM(ct.up), 0) AS up, COALESCE(SUM(ct.down), 0) AS down").
  97. Joins("LEFT JOIN client_traffics ct ON ct.email = c.email").
  98. Where("c.group_name = ?", name).
  99. Scan(&agg).Error; err != nil {
  100. return err
  101. }
  102. var count int64
  103. if err := db.Model(&model.ClientGroup{}).Where("name = ?", name).Count(&count).Error; err != nil {
  104. return err
  105. }
  106. if count == 0 {
  107. return db.Create(&model.ClientGroup{Name: name, ResetUp: agg.Up, ResetDown: agg.Down}).Error
  108. }
  109. return db.Model(&model.ClientGroup{}).Where("name = ?", name).
  110. Updates(map[string]any{"reset_up": agg.Up, "reset_down": agg.Down}).Error
  111. }
  112. func (s *ClientService) CreateGroup(name string) error {
  113. name = strings.TrimSpace(name)
  114. if name == "" {
  115. return common.NewError("group name is required")
  116. }
  117. db := database.GetDB()
  118. var count int64
  119. if err := db.Model(&model.ClientGroup{}).Where("name = ?", name).Count(&count).Error; err != nil {
  120. return err
  121. }
  122. if count > 0 {
  123. return common.NewError("group already exists")
  124. }
  125. return db.Create(&model.ClientGroup{Name: name}).Error
  126. }
  127. func (s *ClientService) RenameGroup(oldName, newName string) (int, error) {
  128. oldName = strings.TrimSpace(oldName)
  129. newName = strings.TrimSpace(newName)
  130. if oldName == "" {
  131. return 0, common.NewError("old group name is required")
  132. }
  133. if newName == "" {
  134. return 0, common.NewError("new group name is required")
  135. }
  136. if oldName == newName {
  137. return 0, nil
  138. }
  139. return s.replaceGroupValue(oldName, newName)
  140. }
  141. func (s *ClientService) DeleteGroup(name string) (int, error) {
  142. name = strings.TrimSpace(name)
  143. if name == "" {
  144. return 0, common.NewError("group name is required")
  145. }
  146. return s.replaceGroupValue(name, "")
  147. }
  148. func (s *ClientService) RemoveFromGroup(emails []string) (int, error) {
  149. return s.AddToGroup(emails, "")
  150. }
  151. func (s *ClientService) AddToGroup(emails []string, group string) (int, error) {
  152. group = strings.TrimSpace(group)
  153. if len(emails) == 0 {
  154. return 0, nil
  155. }
  156. db := database.GetDB()
  157. if group != "" {
  158. var exists int64
  159. if err := db.Model(&model.ClientGroup{}).Where("name = ?", group).Count(&exists).Error; err != nil {
  160. return 0, err
  161. }
  162. if exists == 0 {
  163. var derived int64
  164. if err := db.Model(&model.ClientRecord{}).Where("group_name = ?", group).Count(&derived).Error; err != nil {
  165. return 0, err
  166. }
  167. if derived == 0 {
  168. if err := db.Create(&model.ClientGroup{Name: group}).Error; err != nil {
  169. return 0, err
  170. }
  171. }
  172. }
  173. }
  174. var records []model.ClientRecord
  175. for _, batch := range chunkStrings(emails, sqlInChunk) {
  176. var rows []model.ClientRecord
  177. if err := db.Where("email IN ?", batch).Find(&rows).Error; err != nil {
  178. return 0, err
  179. }
  180. records = append(records, rows...)
  181. }
  182. if len(records) == 0 {
  183. return 0, nil
  184. }
  185. affectedEmails := make([]string, 0, len(records))
  186. for _, r := range records {
  187. affectedEmails = append(affectedEmails, r.Email)
  188. }
  189. tx := db.Begin()
  190. for _, batch := range chunkStrings(affectedEmails, sqlInChunk) {
  191. if err := tx.Model(&model.ClientRecord{}).
  192. Where("email IN ?", batch).
  193. UpdateColumn("group_name", group).Error; err != nil {
  194. tx.Rollback()
  195. return 0, err
  196. }
  197. }
  198. var inboundIDs []int
  199. inboundIDSeen := make(map[int]struct{})
  200. for _, batch := range chunkStrings(affectedEmails, sqlInChunk) {
  201. var ids []int
  202. if err := tx.Table("client_inbounds").
  203. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  204. Where("clients.email IN ?", batch).
  205. Distinct("client_inbounds.inbound_id").
  206. Pluck("inbound_id", &ids).Error; err != nil {
  207. tx.Rollback()
  208. return 0, err
  209. }
  210. for _, id := range ids {
  211. if _, ok := inboundIDSeen[id]; !ok {
  212. inboundIDSeen[id] = struct{}{}
  213. inboundIDs = append(inboundIDs, id)
  214. }
  215. }
  216. }
  217. emailSet := make(map[string]struct{}, len(affectedEmails))
  218. for _, e := range affectedEmails {
  219. emailSet[e] = struct{}{}
  220. }
  221. for _, ibID := range inboundIDs {
  222. var ib model.Inbound
  223. if err := tx.First(&ib, ibID).Error; err != nil {
  224. tx.Rollback()
  225. return 0, err
  226. }
  227. var settings map[string]any
  228. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  229. continue
  230. }
  231. clients, ok := settings["clients"].([]any)
  232. if !ok {
  233. continue
  234. }
  235. modified := false
  236. for i := range clients {
  237. cm, ok := clients[i].(map[string]any)
  238. if !ok {
  239. continue
  240. }
  241. email, _ := cm["email"].(string)
  242. if _, hit := emailSet[email]; !hit {
  243. continue
  244. }
  245. if group == "" {
  246. delete(cm, "group")
  247. } else {
  248. cm["group"] = group
  249. }
  250. clients[i] = cm
  251. modified = true
  252. }
  253. if modified {
  254. settings["clients"] = clients
  255. newSettings, err := json.Marshal(settings)
  256. if err != nil {
  257. continue
  258. }
  259. ib.Settings = string(newSettings)
  260. if err := tx.Save(&ib).Error; err != nil {
  261. tx.Rollback()
  262. return 0, err
  263. }
  264. }
  265. }
  266. if err := tx.Commit().Error; err != nil {
  267. return 0, err
  268. }
  269. return len(records), nil
  270. }
  271. func (s *ClientService) replaceGroupValue(oldName, newName string) (int, error) {
  272. db := database.GetDB()
  273. if newName == "" {
  274. if err := db.Where("name = ?", oldName).Delete(&model.ClientGroup{}).Error; err != nil {
  275. return 0, err
  276. }
  277. } else {
  278. if err := db.Model(&model.ClientGroup{}).Where("name = ?", oldName).Update("name", newName).Error; err != nil {
  279. return 0, err
  280. }
  281. }
  282. var records []model.ClientRecord
  283. if err := db.Where("group_name = ?", oldName).Find(&records).Error; err != nil {
  284. return 0, err
  285. }
  286. if len(records) == 0 {
  287. return 0, nil
  288. }
  289. affectedEmails := make([]string, 0, len(records))
  290. for _, r := range records {
  291. affectedEmails = append(affectedEmails, r.Email)
  292. }
  293. tx := db.Begin()
  294. if err := tx.Model(&model.ClientRecord{}).
  295. Where("group_name = ?", oldName).
  296. UpdateColumn("group_name", newName).Error; err != nil {
  297. tx.Rollback()
  298. return 0, err
  299. }
  300. var inboundIDs []int
  301. inboundIDSeen := make(map[int]struct{})
  302. for _, batch := range chunkStrings(affectedEmails, sqlInChunk) {
  303. var ids []int
  304. if err := tx.Table("client_inbounds").
  305. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  306. Where("clients.email IN ?", batch).
  307. Distinct("client_inbounds.inbound_id").
  308. Pluck("inbound_id", &ids).Error; err != nil {
  309. tx.Rollback()
  310. return 0, err
  311. }
  312. for _, id := range ids {
  313. if _, ok := inboundIDSeen[id]; !ok {
  314. inboundIDSeen[id] = struct{}{}
  315. inboundIDs = append(inboundIDs, id)
  316. }
  317. }
  318. }
  319. for _, ibID := range inboundIDs {
  320. var ib model.Inbound
  321. if err := tx.First(&ib, ibID).Error; err != nil {
  322. tx.Rollback()
  323. return 0, err
  324. }
  325. var settings map[string]any
  326. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  327. continue
  328. }
  329. clients, ok := settings["clients"].([]any)
  330. if !ok {
  331. continue
  332. }
  333. modified := false
  334. for i := range clients {
  335. cm, ok := clients[i].(map[string]any)
  336. if !ok {
  337. continue
  338. }
  339. if g, ok := cm["group"].(string); ok && g == oldName {
  340. if newName == "" {
  341. delete(cm, "group")
  342. } else {
  343. cm["group"] = newName
  344. }
  345. clients[i] = cm
  346. modified = true
  347. }
  348. }
  349. if modified {
  350. settings["clients"] = clients
  351. newSettings, err := json.Marshal(settings)
  352. if err != nil {
  353. continue
  354. }
  355. ib.Settings = string(newSettings)
  356. if err := tx.Save(&ib).Error; err != nil {
  357. tx.Rollback()
  358. return 0, err
  359. }
  360. }
  361. }
  362. if err := tx.Commit().Error; err != nil {
  363. return 0, err
  364. }
  365. return len(records), nil
  366. }