synthesis.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  1. package service
  2. import (
  3. "context"
  4. "math"
  5. "sort"
  6. "time"
  7. mhayaTime "github.com/mhaya/extend/time"
  8. cutils "github.com/mhaya/extend/utils"
  9. "github.com/mhaya/game/game_cluster/internal/code"
  10. "github.com/mhaya/game/game_cluster/internal/constant"
  11. "github.com/mhaya/game/game_cluster/internal/mdb"
  12. "github.com/mhaya/game/game_cluster/internal/mdb/eventmodels"
  13. "github.com/mhaya/game/game_cluster/internal/mdb/models"
  14. "github.com/mhaya/game/game_cluster/nodes/webadmin/common"
  15. "github.com/mhaya/game/game_cluster/nodes/webadmin/entity"
  16. "github.com/mhaya/game/game_cluster/nodes/webadmin/model"
  17. mhayaLogger "github.com/mhaya/logger"
  18. "github.com/spf13/cast"
  19. "go.mongodb.org/mongo-driver/bson"
  20. "go.mongodb.org/mongo-driver/bson/primitive"
  21. "go.mongodb.org/mongo-driver/mongo"
  22. "go.mongodb.org/mongo-driver/mongo/options"
  23. "gorm.io/gorm"
  24. )
  25. type Synthesis struct {
  26. nodeId string
  27. db *mongo.Database
  28. }
  29. func NewSynthesis(nodeId string) *Synthesis {
  30. return &Synthesis{
  31. nodeId: nodeId,
  32. db: mdb.MDB,
  33. }
  34. }
  35. // 综合统计
  36. func (s *Synthesis) Overview(req entity.OverviewReq) (*entity.OverviewResp, *code.Result) {
  37. page, pageSize := checkPageParam(req.Page, req.Size)
  38. // 构建查询条件
  39. filter := bson.M{}
  40. if req.UserName != "" {
  41. filter["userName"] = req.UserName
  42. }
  43. // 设置分页选项
  44. findOptions := options.Find()
  45. findOptions.SetSkip(int64((page - 1) * pageSize))
  46. findOptions.SetLimit(int64(pageSize))
  47. findOptions.SetSort(bson.D{{"createTime", -1}})
  48. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  49. defer cancel()
  50. collection := mdb.MDB.Collection(constant.CNamePlayer)
  51. // 查询数据
  52. var results []*entity.OverviewDetail
  53. cursor, err := collection.Find(ctx, filter, findOptions)
  54. if err != nil {
  55. mhayaLogger.Warnf("Overview Find error:%v", err)
  56. return nil, common.NewResult(code.InternalError)
  57. }
  58. defer cursor.Close(ctx)
  59. // 解析结果
  60. for cursor.Next(ctx) {
  61. var result entity.OverviewDetail
  62. if err := cursor.Decode(&result); err != nil {
  63. mhayaLogger.Warnf("Overview Decode error:%v", err)
  64. return nil, common.NewResult(code.InternalError)
  65. }
  66. results = append(results, &result)
  67. }
  68. if err := cursor.Err(); err != nil {
  69. mhayaLogger.Warnf("Overview cursor error:%v", err)
  70. return nil, common.NewResult(code.InternalError)
  71. }
  72. // TODO 综合统计相关信息
  73. // TODO 获取总数total
  74. var count int64
  75. return &entity.OverviewResp{
  76. Details: results,
  77. Total: count,
  78. }, nil
  79. }
  80. // 统计用户相关信息
  81. func (s *Synthesis) UserList(req entity.UserListReq) (*entity.UserListResp, *code.Result) {
  82. page, pageSize := checkPageParam(req.Page, req.Size)
  83. // 构建查询条件
  84. filter := bson.M{}
  85. if req.UserName != "" {
  86. filter["userName"] = req.UserName
  87. }
  88. // 设置分页选项
  89. findOptions := options.Find()
  90. findOptions.SetSkip(int64((page - 1) * pageSize))
  91. findOptions.SetLimit(int64(pageSize))
  92. findOptions.SetSort(bson.D{{"createTime", -1}})
  93. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  94. defer cancel()
  95. collection := mdb.MDB.Collection(constant.CNamePlayer)
  96. // 查询数据
  97. var results []*entity.UserListDetail
  98. cursor, err := collection.Find(ctx, filter, findOptions)
  99. if err != nil {
  100. mhayaLogger.Warnf("UserList Find error:%v", err)
  101. return nil, common.NewResult(code.InternalError)
  102. }
  103. defer cursor.Close(ctx)
  104. // 解析结果
  105. for cursor.Next(ctx) {
  106. var result entity.UserListDetail
  107. if err := cursor.Decode(&result); err != nil {
  108. mhayaLogger.Warnf("UserList Decode error:%v", err)
  109. return nil, common.NewResult(code.InternalError)
  110. }
  111. results = append(results, &result)
  112. }
  113. if err := cursor.Err(); err != nil {
  114. mhayaLogger.Warnf("UserList cursor error:%v", err)
  115. return nil, common.NewResult(code.InternalError)
  116. }
  117. // TODO UserList 统计用户相关信息
  118. // TODO 获取总数total
  119. var count int64
  120. return &entity.UserListResp{
  121. Details: results,
  122. Total: count,
  123. }, nil
  124. }
  125. func (s *Synthesis) FindMDBUserLogDaily(req entity.UserLogDailyReq) (*entity.UserLogDailyResp, *code.Result) {
  126. page, pageSize := checkPageParam(req.Page, req.Size)
  127. // 定义上下文
  128. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  129. defer cancel()
  130. // 指定集合
  131. collection := mdb.MDB.Collection(constant.CNamePlayerDailyRecord)
  132. // 构建查询条件 - 如果查询值为空那就不添加查询条件
  133. filter := bson.M{}
  134. if req.StartTime != 0 {
  135. filter["daily"] = bson.M{
  136. "$gte": req.StartTime,
  137. "$lte": req.EndTime,
  138. }
  139. }
  140. if req.Platform != "" && req.Platform != "all" {
  141. filter["platform"] = req.Platform
  142. }
  143. if req.Channel != "" {
  144. filter["channel"] = req.Channel
  145. }
  146. // 计算总数
  147. count, err := collection.CountDocuments(ctx, filter)
  148. if err != nil {
  149. mhayaLogger.Warnf("FindMDBUserLogDaily CountDocuments error:%v", err)
  150. return nil, common.NewResult(code.InternalError)
  151. }
  152. // 执行查询
  153. opts := options.Find()
  154. opts.SetSkip(int64((page - 1) * pageSize))
  155. opts.SetLimit(int64(pageSize))
  156. opts.SetSort(bson.D{{"daily", -1}})
  157. cursor, err := collection.Find(ctx, filter, opts)
  158. if err != nil {
  159. mhayaLogger.Warnf("FindMDBUserLogDaily Find error:%v", err)
  160. return nil, common.NewResult(code.InternalError)
  161. }
  162. defer cursor.Close(ctx)
  163. // 解析查询结果
  164. var results []*entity.UserLogDailyDetail
  165. for cursor.Next(ctx) {
  166. var result *entity.UserLogDailyDetail
  167. err := cursor.Decode(&result)
  168. if err != nil {
  169. mhayaLogger.Warnf("FindMDBUserLogDaily Decode error:%v", err)
  170. return nil, common.NewResult(code.InternalError)
  171. }
  172. results = append(results, result)
  173. }
  174. // 同一天的数据全部放到platform= ALl 且数据累加
  175. // 如果没有platform=all 的那就新增一个 同一个时间段只能有一个 platform=all
  176. // 将同一天的数据累加到platform=all的记录中
  177. allPlatformRecordMap := make(map[int64]*entity.UserLogDailyDetail)
  178. for _, result := range results {
  179. allPlatformRecordMap[result.Timestamp] = &entity.UserLogDailyDetail{
  180. Timestamp: result.Timestamp,
  181. Platform: "all",
  182. }
  183. }
  184. for _, v := range results {
  185. if v.Timestamp == allPlatformRecordMap[v.Timestamp].Timestamp {
  186. allPlatformRecordMap[v.Timestamp].Registered += v.Registered
  187. allPlatformRecordMap[v.Timestamp].LoggedIn += v.LoggedIn
  188. allPlatformRecordMap[v.Timestamp].NewActive += v.NewActive
  189. allPlatformRecordMap[v.Timestamp].OldActive += v.OldActive
  190. allPlatformRecordMap[v.Timestamp].TotalPoints += v.TotalPoints
  191. allPlatformRecordMap[v.Timestamp].UProduced += v.UProduced
  192. allPlatformRecordMap[v.Timestamp].UCashout += v.UCashout
  193. allPlatformRecordMap[v.Timestamp].NewLogin += v.NewLogin
  194. allPlatformRecordMap[v.Timestamp].OldLogin += v.OldLogin
  195. }
  196. }
  197. // 替换原有结果
  198. for _, record := range allPlatformRecordMap {
  199. results = append(results, record)
  200. }
  201. return &entity.UserLogDailyResp{
  202. Details: results,
  203. Total: count,
  204. }, nil
  205. }
  206. // FindWithdrawal 根据请求查询提现记录
  207. func (s *Synthesis) FindWithdrawal(req entity.UserWithdrawalReq) (*entity.UserWithdrawalResp, *code.Result) {
  208. page, pageSize := checkPageParam(req.Page, req.Size)
  209. if !constant.CurrencyValid(req.Currency) {
  210. mhayaLogger.Warnf("FindWithdrawal unknow currency:%v", req.Currency)
  211. return nil, common.NewResult(code.ParamError)
  212. }
  213. var records []*eventmodels.UserWithdrawEventContent
  214. where := &eventmodels.UserWithdrawEventContent{
  215. UserBasic: eventmodels.UserBasic{
  216. UserId: req.UserName,
  217. UserName: req.NickName,
  218. },
  219. EventBasic: eventmodels.EventBasic{
  220. ServerId: s.nodeId,
  221. Status: req.Withdrawal,
  222. },
  223. WithdrawId: req.ID,
  224. Currency: req.Currency,
  225. Address: req.Address,
  226. State: req.State,
  227. }
  228. db := mdb.LogstashDB.Model(&eventmodels.UserWithdrawEventContent{}).Where(where).Order("create_at")
  229. if req.StartTime > 0 && req.EndTime > 0 && req.StartTime <= req.EndTime {
  230. db = db.Where("create_at >= ? and create_at <= ?", req.StartTime, req.EndTime)
  231. }
  232. if req.AmountMin > 0 && req.AmountMax > 0 && req.AmountMin <= req.AmountMax {
  233. db = db.Where("amount >= ? and amount <= ?", req.AmountMin, req.AmountMax)
  234. }
  235. if req.AfterAmountMin > 0 && req.AfterAmountMax > 0 && req.AfterAmountMin <= req.AfterAmountMax {
  236. db = db.Where("after_balance >= ? and after_balance <= ?", req.AfterAmountMin, req.AfterAmountMax)
  237. }
  238. pages := Paginate(db, page, pageSize)
  239. err := db.Scopes(pages.Limit).Find(&records).Error
  240. if err != nil && err != gorm.ErrRecordNotFound {
  241. mhayaLogger.Warnf("FindWithdrawal Find error:%v", err)
  242. return nil, common.NewResult(code.InternalError)
  243. }
  244. var results []*entity.UserWithdrawalDetail
  245. for _, v := range records {
  246. results = append(results, &entity.UserWithdrawalDetail{
  247. Id: v.WithdrawId,
  248. UserName: v.UserId,
  249. NickName: v.UserName,
  250. OpenId: v.TgId,
  251. Status: v.State,
  252. Reason: v.Reason,
  253. Withdrawal: v.Status,
  254. Amount: int(v.Amount),
  255. AfterAmount: int(v.AfterBalance),
  256. Type: v.Currency,
  257. Address: v.Address,
  258. CreateAt: v.CreateAt,
  259. UpdateAt: v.CreateAt,
  260. })
  261. }
  262. return &entity.UserWithdrawalResp{
  263. Details: results,
  264. Total: pages.Count,
  265. }, nil
  266. }
  267. // 导出提现记录
  268. func (s *Synthesis) WithdrawalExport(req entity.UserWithdrawalExportReq) (*entity.UserWithdrawalResp, *code.Result) {
  269. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  270. defer cancel()
  271. collection := mdb.MDB.Collection(constant.CNameCashOutRecord)
  272. // 构建过滤器
  273. filter := bson.M{}
  274. if req.UserName != "" {
  275. filter["userName"] = req.UserName
  276. }
  277. if req.NickName != "" {
  278. filter["nickName"] = req.NickName
  279. }
  280. if req.ID != "" {
  281. filter["_id"], _ = primitive.ObjectIDFromHex(req.ID)
  282. }
  283. if req.Address != "" {
  284. filter["address"] = req.Address
  285. }
  286. if req.State > 0 {
  287. filter["state"] = req.State
  288. }
  289. if req.Withdrawal > 0 {
  290. filter["withdrawal"] = req.Withdrawal
  291. }
  292. if req.StartTime > 0 && req.EndTime > 0 && req.StartTime <= req.EndTime {
  293. filter["createAt"] = bson.M{
  294. "$gte": req.StartTime,
  295. "$lte": req.EndTime,
  296. }
  297. }
  298. if req.AmountMin > 0 && req.AmountMax > 0 && req.AmountMin <= req.AmountMax {
  299. filter["amount"] = bson.M{
  300. "$gte": req.AmountMin,
  301. "$lte": req.AmountMax,
  302. }
  303. }
  304. if req.AfterAmountMin > 0 && req.AfterAmountMax > 0 && req.AfterAmountMin <= req.AfterAmountMax {
  305. filter["after_amount"] = bson.M{
  306. "$gte": req.AfterAmountMin,
  307. "$lte": req.AfterAmountMax,
  308. }
  309. }
  310. findOptions := options.Find()
  311. findOptions.SetSort(bson.D{{"createAt", -1}})
  312. // 获取总数total
  313. count, err := collection.CountDocuments(ctx, filter)
  314. if err != nil {
  315. mhayaLogger.Warnf("WithdrawalExportData CountDocuments error:%v", err)
  316. return nil, common.NewResult(code.InternalError)
  317. }
  318. // 查询数据
  319. var results []*entity.UserWithdrawalDetail
  320. cursor, err := collection.Find(ctx, filter, findOptions)
  321. if err != nil {
  322. mhayaLogger.Warnf("WithdrawalExportData Find error:%v", err)
  323. return nil, common.NewResult(code.InternalError)
  324. }
  325. defer cursor.Close(ctx)
  326. // 解析结果
  327. for cursor.Next(ctx) {
  328. var result entity.UserWithdrawalDetail
  329. if err := cursor.Decode(&result); err != nil {
  330. mhayaLogger.Warnf("WithdrawalExportData Decode error:%v", err)
  331. return nil, common.NewResult(code.InternalError)
  332. }
  333. results = append(results, &result)
  334. }
  335. if err := cursor.Err(); err != nil {
  336. mhayaLogger.Warnf("WithdrawalExportData cursor error:%v", err)
  337. return nil, common.NewResult(code.InternalError)
  338. }
  339. return &entity.UserWithdrawalResp{
  340. Details: results,
  341. Total: count,
  342. }, nil
  343. }
  344. // WithdrawalStatus 更新提现状态
  345. func (s *Synthesis) WithdrawalStatus(req entity.UserWithdrawalStatus) *code.Result {
  346. // ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  347. // defer cancel()
  348. collection := mdb.MDB.Collection(constant.CNameCashOutRecord)
  349. // 更新条件
  350. updateCondition := bson.M{"userName": req.UserName}
  351. // 更新内容
  352. updateContent := bson.M{"$set": bson.M{"status": req.Status}}
  353. // 设置更新选项
  354. updateOptions := options.Update() // 设置 upsert 选项
  355. // 执行更新操作
  356. _, err := collection.UpdateOne(context.TODO(), updateCondition, updateContent, updateOptions)
  357. if err != nil {
  358. mhayaLogger.Warnf("WithdrawalStatus UpdateOne error:%v", err)
  359. return common.NewResult(code.InternalError)
  360. }
  361. return nil
  362. }
  363. // WithdrawalStatusBatch 更新提现状态
  364. func (s *Synthesis) WithdrawalStatusBatch(req entity.UserWithdrawalStatusBatch) *code.Result {
  365. collection := mdb.MDB.Collection(constant.CNameCashOutRecord)
  366. if len(req.ID) == 0 {
  367. return common.NewResult(code.ParamError)
  368. }
  369. for _, id := range req.ID {
  370. objID := primitive.ObjectID{}
  371. objID, _ = primitive.ObjectIDFromHex(id)
  372. updateCondition := bson.M{"_id": objID}
  373. updateContent := bson.M{}
  374. withdrawal := models.CashOutRecord{}
  375. err := collection.FindOne(context.TODO(), updateCondition).Decode(&withdrawal)
  376. if err != nil {
  377. mhayaLogger.Warnf("WithdrawalStatusBatch FindOne error:%v", err)
  378. continue
  379. }
  380. if req.Withdrawal != 0 {
  381. if withdrawal.Status == 1 {
  382. updateContent = bson.M{"$set": bson.M{"withdrawal": req.Withdrawal}}
  383. } else {
  384. continue
  385. }
  386. }
  387. if req.Status > 0 {
  388. if withdrawal.Status != 0 {
  389. continue
  390. }
  391. updateContent = bson.M{"$set": bson.M{"status": req.Status}}
  392. }
  393. updateOptions := options.Update().SetUpsert(true)
  394. _, err = collection.UpdateOne(context.TODO(), updateCondition, updateContent, updateOptions)
  395. if err != nil {
  396. mhayaLogger.Warnf("WithdrawalStatusBatch UpdateOne error:%v", err)
  397. continue
  398. }
  399. }
  400. return nil
  401. }
  402. // FindUserCountryCount 查询用户国家分布
  403. //
  404. // 返回值为 UserCountryResp 的切片和错误。
  405. func (s *Synthesis) FindUserCountryCount() (*entity.UserCountryResp, *code.Result) {
  406. // 选择数据库和集合
  407. collection := mdb.MDB.Collection(constant.CNamePlayerCountryByIPStat)
  408. // 定义聚合管道
  409. // 定义聚合管道
  410. pipeline := []bson.D{
  411. {
  412. {Key: "$project", Value: bson.D{
  413. {Key: "playerRegisterCountry", Value: bson.D{{Key: "$objectToArray", Value: "$playerRegisterCountry"}}},
  414. }},
  415. },
  416. {
  417. {Key: "$unwind", Value: "$playerRegisterCountry"},
  418. },
  419. {
  420. {Key: "$group", Value: bson.D{
  421. {Key: "_id", Value: "$playerRegisterCountry.k"},
  422. {Key: "totalValue", Value: bson.D{{Key: "$sum", Value: "$playerRegisterCountry.v"}}},
  423. }},
  424. },
  425. {
  426. {Key: "$project", Value: bson.D{
  427. {Key: "_id", Value: 0},
  428. {Key: "countryKey", Value: "$_id"},
  429. {Key: "totalValue", Value: 1},
  430. }},
  431. },
  432. }
  433. // 执行聚合查询
  434. cursor, err := collection.Aggregate(context.TODO(), pipeline)
  435. if err != nil {
  436. mhayaLogger.Warnf("FindUserCountryCount Aggregate error:%v", err)
  437. return nil, common.NewResult(code.InternalError)
  438. }
  439. defer cursor.Close(context.TODO())
  440. // 遍历查询结果
  441. var results []bson.M
  442. if err := cursor.All(context.TODO(), &results); err != nil {
  443. mhayaLogger.Warnf("FindUserCountryCount All error:%v", err)
  444. return nil, common.NewResult(code.InternalError)
  445. }
  446. var totalIPCount int64
  447. var data []*entity.UserCountryDetail
  448. // 将结果转换为 UserCountryDetail
  449. for _, r := range results {
  450. var resp entity.UserCountryDetail
  451. resp.Country = r["countryKey"].(string)
  452. resp.IPCount = int(r["totalValue"].(int32))
  453. totalIPCount += int64(resp.IPCount)
  454. data = append(data, &resp)
  455. }
  456. for _, v := range data {
  457. // 保留小数点后两位
  458. v.Percentage = math.Round(float64(v.IPCount)/float64(totalIPCount)*10000) / 100
  459. }
  460. // 根据阈值过滤结果
  461. otherCount := 0
  462. otherPercentage := 0.00
  463. filteredResults := make([]*entity.UserCountryDetail, 0)
  464. threshold := 1.00
  465. for _, r := range data {
  466. if r.Percentage >= threshold {
  467. filteredResults = append(filteredResults, r)
  468. // 保留小数点后两位
  469. r.Percentage = math.Round(r.Percentage*100) / 100
  470. otherPercentage += r.Percentage
  471. } else {
  472. otherCount += r.IPCount
  473. }
  474. }
  475. // 将其他国家添加到过滤后的结果中
  476. if otherCount > 0 {
  477. p := 100.00 - math.Round(otherPercentage*100)/100
  478. filteredResults = append(filteredResults, &entity.UserCountryDetail{
  479. Country: "other",
  480. IPCount: otherCount,
  481. Percentage: math.Round(p*100) / 100,
  482. })
  483. }
  484. return &entity.UserCountryResp{
  485. Details: filteredResults,
  486. }, nil
  487. }
  488. // FindUserRetention UserRetentionResp 用户留存率
  489. // 1. 获取指定日期范围内的注册用户数量
  490. // 2. 获取指定日期范围内的活跃用户数量
  491. // 3. 计算留存率
  492. // 4. 返回结果
  493. func (s *Synthesis) FindUserRetention(req entity.UserRetentionReq) (*entity.UserRetentionResp, *code.Result) {
  494. playerPreserve := models.GetPlayerPreserve(req.StartTime, req.EndTime)
  495. // 查询数据
  496. var results []*entity.UserRetentionDetail
  497. for key, v := range playerPreserve {
  498. var retention entity.Retention
  499. for _, vv := range v {
  500. if vv.ID == 1 {
  501. retention.Day1 = entity.DayRetention{
  502. LoggedIn: vv.Ratio,
  503. LoginDate: key,
  504. }
  505. }
  506. if vv.ID == 3 {
  507. retention.Day3 = entity.DayRetention{
  508. LoggedIn: vv.Ratio,
  509. LoginDate: key,
  510. }
  511. }
  512. if vv.ID == 7 {
  513. retention.Day7 = entity.DayRetention{
  514. LoggedIn: vv.Ratio,
  515. LoginDate: key,
  516. }
  517. }
  518. if vv.ID == 14 {
  519. retention.Day14 = entity.DayRetention{
  520. LoggedIn: vv.Ratio,
  521. LoginDate: key,
  522. }
  523. }
  524. if vv.ID == 30 {
  525. retention.Day30 = entity.DayRetention{
  526. LoggedIn: vv.Ratio,
  527. LoginDate: key,
  528. }
  529. }
  530. }
  531. results = append(results, &entity.UserRetentionDetail{
  532. RegistrationDate: key,
  533. RetentionData: retention,
  534. })
  535. }
  536. return &entity.UserRetentionResp{
  537. Details: results,
  538. }, nil
  539. }
  540. // FindUserLevel 用户等级统计
  541. func (s *Synthesis) FindUserLevel() (*entity.UserLevelCountResp, *code.Result) {
  542. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  543. defer cancel()
  544. collection := mdb.MDB.Collection(constant.CNamePlayerLevelStat)
  545. // 查询所有文档
  546. cursor, err := collection.Find(ctx, bson.M{})
  547. if err != nil {
  548. mhayaLogger.Warnf("FindUserLevel Find error:%v", err)
  549. return nil, common.NewResult(code.InternalError)
  550. }
  551. defer cursor.Close(ctx)
  552. var results []*entity.UserLevelCountDetail
  553. var reData []*models.PlayerLevelStat
  554. for cursor.Next(ctx) {
  555. var result models.PlayerLevelStat
  556. if err := cursor.Decode(&result); err != nil {
  557. mhayaLogger.Warnf("FindUserLevel Decode error:%v", err)
  558. return nil, common.NewResult(code.InternalError)
  559. }
  560. reData = append(reData, &result)
  561. }
  562. if err := cursor.Err(); err != nil {
  563. mhayaLogger.Warnf("FindUserLevel cursor error:%v", err)
  564. return nil, common.NewResult(code.InternalError)
  565. }
  566. dataMap := make(map[string]int)
  567. for _, v := range reData {
  568. for key, v1 := range v.ServerLevel {
  569. dataMap[key] += v1
  570. }
  571. }
  572. keys := make([]string, 0, len(dataMap))
  573. for k := range dataMap {
  574. keys = append(keys, k)
  575. }
  576. sort.Strings(keys)
  577. for _, v := range keys {
  578. results = append(results, &entity.UserLevelCountDetail{
  579. Level: cast.ToInt(v),
  580. UserCount: dataMap[v],
  581. })
  582. }
  583. return &entity.UserLevelCountResp{
  584. Details: results,
  585. }, nil
  586. }
  587. func (s *Synthesis) InsertRecord(param model.UserOperationLog) {
  588. mhayaLogger.Warnf("InsertRecord param:%#v", param)
  589. record := new(model.UserOperationLog)
  590. collection := mdb.MDB.Collection(record.TableName())
  591. insertData := bson.M{}
  592. insertData["user_name"] = param.Username
  593. insertData["role_id"] = param.RoleId
  594. insertData["url"] = param.Path
  595. insertData["method"] = param.Method
  596. insertData["status_code"] = param.StatusCode
  597. insertData["dur"] = param.Dur
  598. insertData["client_ip"] = param.ClientIP
  599. insertData["error_message"] = param.ErrorMessage
  600. insertData["created_at"] = mhayaTime.Now().Unix()
  601. _, err := collection.InsertOne(context.Background(), insertData)
  602. if err != nil {
  603. mhayaLogger.Warnf("InsertRecord InsertOne error:%v", err)
  604. return
  605. }
  606. }
  607. func (s *Synthesis) RecordList(req entity.RecordListReq) (*entity.RecordListResp, *code.Result) {
  608. page, pageSize := checkPageParam(req.Page, req.Size)
  609. var records []*eventmodels.BackendOperationEventContent
  610. where := &eventmodels.BackendOperationEventContent{
  611. UserBasic: eventmodels.UserBasic{
  612. UserName: req.UserName,
  613. },
  614. EventBasic: eventmodels.EventBasic{
  615. ServerId: s.nodeId,
  616. },
  617. RoleId: req.RoleId,
  618. Path: req.Path,
  619. StatusCode: req.StatusCode,
  620. ClientIP: req.ClientIP,
  621. }
  622. db := mdb.LogstashDB.Model(&eventmodels.BackendOperationEventContent{}).Where(where).Order("create_at")
  623. if req.StartTime > 0 && req.EndTime > 0 && req.StartTime <= req.EndTime {
  624. db = db.Where("create_at >= ? and create_at <= ?", req.StartTime, req.EndTime)
  625. }
  626. if req.DurMin > 0 && req.DurMax > 0 && req.DurMin <= req.DurMax {
  627. db = db.Where("dur >= ? and dur <= ?", req.DurMin, req.DurMax)
  628. }
  629. pages := Paginate(db, page, pageSize)
  630. err := db.Scopes(pages.Limit).Find(&records).Error
  631. if err != nil && err != gorm.ErrRecordNotFound {
  632. mhayaLogger.Warnf("RecordList Find error:%v", err)
  633. return nil, common.NewResult(code.InternalError)
  634. }
  635. var results []*entity.RecordListDetail
  636. for _, v := range records {
  637. results = append(results, &entity.RecordListDetail{
  638. Username: v.UserName,
  639. RoleId: v.RoleId,
  640. Path: v.Path,
  641. Method: v.Method,
  642. StatusCode: v.StatusCode,
  643. Dur: v.Dur,
  644. ClientIP: v.ClientIP,
  645. ErrorMessage: v.ErrorMessage,
  646. CreatedAt: v.CreateAt,
  647. })
  648. }
  649. return &entity.RecordListResp{
  650. Details: results,
  651. Total: pages.Count,
  652. }, nil
  653. }
  654. // 转盘统计
  655. func (s *Synthesis) Turntable(req entity.TurntableReq) (*entity.TurntableResp, *code.Result) {
  656. page, pageSize := checkPageParam(req.Page, req.Size)
  657. var records []*eventmodels.TurntableEventContent
  658. where := &eventmodels.TurntableEventContent{
  659. UserBasic: eventmodels.UserBasic{
  660. UserId: req.UserName,
  661. },
  662. }
  663. db := mdb.LogstashDB.Model(&eventmodels.TurntableEventContent{}).Where(where).Order("create_at")
  664. if req.StartTime > 0 && req.EndTime > 0 && req.StartTime <= req.EndTime {
  665. db = db.Where("create_at >= ? and create_at <= ?", req.StartTime, req.EndTime)
  666. }
  667. pages := Paginate(db, page, pageSize)
  668. err := db.Scopes(pages.Limit).Find(&records).Error
  669. if err != nil && err != gorm.ErrRecordNotFound {
  670. mhayaLogger.Warnf("Turntable Find error:%v", err)
  671. return nil, common.NewResult(code.InternalError)
  672. }
  673. var results []*entity.TurntableDetail
  674. for _, v := range records {
  675. results = append(results, &entity.TurntableDetail{
  676. UserName: v.UserName,
  677. OpenId: v.TgId,
  678. TurntableExecutionTime: v.CreateAt,
  679. PrizeName: v.PrizeName,
  680. PrizeNumber: v.PrizeNumber,
  681. })
  682. }
  683. return &entity.TurntableResp{
  684. Details: results,
  685. Total: pages.Count,
  686. }, nil
  687. }
  688. // 资产统计
  689. func (s *Synthesis) Assets(req entity.AssetsReq) (*entity.AssetsResp, *code.Result) {
  690. page, pageSize := checkPageParam(req.Page, req.Size)
  691. playerMgr := NewPlayerManage()
  692. // 根据条件查询
  693. if req.UserName != "" {
  694. registerRecord, codeResult := playerMgr.GetRegisterRecord(req.UserName)
  695. if codeResult != nil {
  696. mhayaLogger.Warnf("Assets GetRegisterRecord error:%v", codeResult)
  697. return nil, codeResult
  698. }
  699. if registerRecord == nil {
  700. mhayaLogger.Warnf("Assets registerRecord:%v", registerRecord)
  701. return nil, nil
  702. }
  703. var results []*entity.AssetsDetail
  704. detail, codeResult := s.getAsset(req.UserName)
  705. if codeResult != nil {
  706. mhayaLogger.Warnf("Assets getAsset error:%v", codeResult)
  707. return nil, codeResult
  708. }
  709. results = append(results, detail)
  710. return &entity.AssetsResp{
  711. Details: results,
  712. Total: 1,
  713. }, nil
  714. }
  715. // 查询列表
  716. listResp, codeResult := playerMgr.List(context.Background(), entity.PlayerListReq{
  717. Page: page,
  718. Size: pageSize,
  719. })
  720. if codeResult != nil {
  721. mhayaLogger.Warnf("Assets List error:%v", codeResult)
  722. return nil, codeResult
  723. }
  724. var results []*entity.AssetsDetail
  725. for _, detail := range listResp.Details {
  726. ret, codeResult := s.getAsset(detail.UserName)
  727. if codeResult != nil {
  728. mhayaLogger.Warnf("Assets getAsset error:%v", codeResult)
  729. return nil, codeResult
  730. }
  731. results = append(results, ret)
  732. }
  733. count, codeResult := playerMgr.GetPlayerTotalCount()
  734. if codeResult != nil {
  735. mhayaLogger.Warnf("Assets GetPlayerTotalCount codeResult:%v", codeResult)
  736. return nil, codeResult
  737. }
  738. return &entity.AssetsResp{
  739. Details: results,
  740. Total: int64(count),
  741. }, nil
  742. }
  743. func (s *Synthesis) getAsset(userName string) (*entity.AssetsDetail, *code.Result) {
  744. var totalUsdtAmount int64
  745. var totalTonAmount int64
  746. where := &eventmodels.AssetsEventContent{
  747. UserBasic: eventmodels.UserBasic{
  748. UserId: userName,
  749. },
  750. EventBasic: eventmodels.EventBasic{
  751. ServerId: s.nodeId,
  752. },
  753. }
  754. where.Currency = string(constant.UsdtCurrency)
  755. err := mdb.LogstashDB.Model(&eventmodels.AssetsEventContent{}).Where(where).Pluck("SUM(amount) as total_amount", &totalUsdtAmount).Error
  756. if err != nil {
  757. mhayaLogger.Warnf("getAsset Pluck UsdtCurrency error:%v", err)
  758. return nil, common.NewResult(code.InternalError)
  759. }
  760. where.Currency = string(constant.TonCurrency)
  761. err = mdb.LogstashDB.Model(&eventmodels.AssetsEventContent{}).Where(where).Pluck("SUM(amount) as total_amount", &totalTonAmount).Error
  762. if err != nil {
  763. mhayaLogger.Warnf("getAsset Pluck TonCurrency error:%v", err)
  764. return nil, common.NewResult(code.InternalError)
  765. }
  766. return &entity.AssetsDetail{
  767. UserName: userName,
  768. TonValue: cutils.QuoInt64ByRatioToFloat64(totalTonAmount, constant.MoneyRatio),
  769. UsdtValue: cutils.QuoInt64ByRatioToFloat64(totalUsdtAmount, constant.MoneyRatio),
  770. StatisticalDate: mhayaTime.Now().Unix(),
  771. }, nil
  772. }
  773. // 资产变动记录
  774. func (s *Synthesis) AssetsRecord(req entity.AssetsRecordReq) (*entity.AssetsRecordResp, *code.Result) {
  775. page, pageSize := checkPageParam(req.Page, req.Size)
  776. if !constant.CurrencyValid(req.Currency) {
  777. mhayaLogger.Warnf("AssetsRecord unknow currency:%v", req.Currency)
  778. return nil, common.NewResult(code.ParamError)
  779. }
  780. if req.OperationType != string(constant.IncreaseOp) && req.OperationType != string(constant.DecreaseOp) {
  781. mhayaLogger.Warnf("AssetsRecord unknow OperationType:%v", req.OperationType)
  782. return nil, common.NewResult(code.ParamError)
  783. }
  784. var records []*eventmodels.AssetsEventContent
  785. where := &eventmodels.AssetsEventContent{
  786. UserBasic: eventmodels.UserBasic{
  787. UserId: req.UserName,
  788. },
  789. EventBasic: eventmodels.EventBasic{
  790. ServerId: s.nodeId,
  791. },
  792. OperationType: req.OperationType,
  793. Currency: req.Currency,
  794. Amount: req.Amount,
  795. }
  796. db := mdb.LogstashDB.Model(&eventmodels.AssetsEventContent{}).Where(where).Order("create_at")
  797. if req.StartTime > 0 && req.EndTime > 0 && req.StartTime <= req.EndTime {
  798. db = db.Where("create_at >= ? and create_at <= ?", req.StartTime, req.EndTime)
  799. }
  800. pages := Paginate(db, page, pageSize)
  801. err := db.Scopes(pages.Limit).Find(&records).Error
  802. if err != nil && err != gorm.ErrRecordNotFound {
  803. mhayaLogger.Warnf("Invite Find error:%v", err)
  804. return nil, common.NewResult(code.InternalError)
  805. }
  806. var results []*entity.AssetsRecordDetail
  807. for _, v := range records {
  808. results = append(results, &entity.AssetsRecordDetail{
  809. UserName: v.UserName,
  810. AssetType: string(v.Currency),
  811. AssetsGrowth: v.Amount,
  812. AcquisitionTime: v.CreateAt,
  813. SourceOfAssetAcquisition: v.Reason,
  814. })
  815. }
  816. return &entity.AssetsRecordResp{
  817. Details: results,
  818. Total: pages.Count,
  819. }, nil
  820. }
  821. // 邀请统计
  822. func (s *Synthesis) Invite(req entity.InviteReq) (*entity.InviteResp, *code.Result) {
  823. page, pageSize := checkPageParam(req.Page, req.Size)
  824. var records []*eventmodels.InviteEventContent
  825. where := &eventmodels.InviteEventContent{
  826. UserBasic: eventmodels.UserBasic{
  827. UserId: req.UserName,
  828. },
  829. }
  830. db := mdb.LogstashDB.Model(&eventmodels.InviteEventContent{}).Where(where).Order("create_at")
  831. pages := Paginate(db, page, pageSize)
  832. err := db.Scopes(pages.Limit).Find(&records).Error
  833. if err != nil && err != gorm.ErrRecordNotFound {
  834. mhayaLogger.Warnf("Invite Find error:%v", err)
  835. return nil, common.NewResult(code.InternalError)
  836. }
  837. var results []*entity.InviteDetail
  838. for _, v := range records {
  839. results = append(results, &entity.InviteDetail{
  840. UserName: v.UserName,
  841. OpenId: v.TgId,
  842. InviteID: v.UserId,
  843. IfInviteVIP: v.IfInviteUserTgVip,
  844. InviteTime: v.CreateAt,
  845. SuccessfulInvitationsLV2: int64(v.InviteUserLevel2),
  846. CumulativesuccessfulInvitations: int64(v.ContinuousInviteUserCount),
  847. InvitationChannel: v.Channel,
  848. })
  849. }
  850. return &entity.InviteResp{
  851. Details: results,
  852. Total: pages.Count,
  853. }, nil
  854. }
  855. // 活跃统计
  856. func (s *Synthesis) Active(req entity.ActiveReq) (*entity.ActiveResp, *code.Result) {
  857. page, pageSize := checkPageParam(req.Page, req.Size)
  858. playerMgr := NewPlayerManage()
  859. // 根据条件查询
  860. if req.UserName != "" {
  861. detail, codeResult := s.getActive(req.UserName)
  862. if codeResult != nil {
  863. mhayaLogger.Warnf("Active getActive error:%v", codeResult)
  864. return nil, codeResult
  865. }
  866. if detail == nil {
  867. return nil, nil
  868. }
  869. var results []*entity.ActiveDetail
  870. results = append(results, detail)
  871. return &entity.ActiveResp{
  872. Details: results,
  873. Total: 1,
  874. }, nil
  875. }
  876. // 查询列表
  877. listResp, codeResult := playerMgr.List(context.Background(), entity.PlayerListReq{
  878. Page: page,
  879. Size: pageSize,
  880. })
  881. if codeResult != nil {
  882. mhayaLogger.Warnf("Active List error:%v", codeResult)
  883. return nil, codeResult
  884. }
  885. var results []*entity.ActiveDetail
  886. for _, detail := range listResp.Details {
  887. detail, codeResult := s.getActive(detail.UserName)
  888. if codeResult != nil {
  889. mhayaLogger.Warnf("Active getActive error:%v", codeResult)
  890. return nil, codeResult
  891. }
  892. results = append(results, detail)
  893. }
  894. count, codeResult := playerMgr.GetPlayerTotalCount()
  895. if codeResult != nil {
  896. mhayaLogger.Warnf("Active GetPlayerTotalCount codeResult:%v", codeResult)
  897. return nil, codeResult
  898. }
  899. return &entity.ActiveResp{
  900. Details: results,
  901. Total: count,
  902. }, nil
  903. }
  904. func (s *Synthesis) getActive(userName string) (*entity.ActiveDetail, *code.Result) {
  905. playerMgr := NewPlayerManage()
  906. // 获取注册记录
  907. registerRecord, codeResult := playerMgr.GetRegisterRecord(userName)
  908. if codeResult != nil {
  909. mhayaLogger.Warnf("getActive GetRegisterRecord error:%v", codeResult)
  910. return nil, codeResult
  911. }
  912. if registerRecord == nil {
  913. mhayaLogger.Warnf("getActive registerRecord:%v", registerRecord)
  914. return nil, nil
  915. }
  916. // 获取最新的用户登录记录
  917. loginRecord, codeResult := playerMgr.GetPlayerLastestLoginRecord(userName)
  918. if codeResult != nil {
  919. mhayaLogger.Warnf("getActive GetPlayerLastestLoginRecord error:%v", codeResult)
  920. return nil, codeResult
  921. }
  922. if loginRecord == nil {
  923. mhayaLogger.Warnf("getActive loginRecord:%v", loginRecord)
  924. return nil, nil
  925. }
  926. // 邀请成功人数
  927. inviteCount, codeResult := playerMgr.GetSuccessfulInvitations(userName, 0)
  928. if codeResult != nil {
  929. mhayaLogger.Warnf("getActive GetSuccessfulInvitations error:%v", codeResult)
  930. return nil, codeResult
  931. }
  932. // 提现次数
  933. withdrawCount, codeResult := playerMgr.GetWithdrawalCount(userName, -1)
  934. if codeResult != nil {
  935. mhayaLogger.Warnf("getActive GetWithdrawalCount error:%v", codeResult)
  936. return nil, codeResult
  937. }
  938. // 累计提现金额
  939. // TODO 暂定USDT
  940. cumulativeWithdrawalAmount, codeResult := playerMgr.GetCumulativeWithdrawalAmount(userName, string(constant.UsdtCurrency), -1)
  941. if codeResult != nil {
  942. mhayaLogger.Warnf("getActive GetCumulativeWithdrawalAmount error:%v", codeResult)
  943. return nil, codeResult
  944. }
  945. // 转盘实际抽奖次数
  946. turntableRuns, codeResult := playerMgr.GetTurntableRuns(userName)
  947. if codeResult != nil {
  948. mhayaLogger.Warnf("getActive GetTurntableRuns error:%v", codeResult)
  949. return nil, codeResult
  950. }
  951. return &entity.ActiveDetail{
  952. UserName: userName,
  953. IfUserVip: loginRecord.IfTgVip,
  954. CreateTime: registerRecord.CreateAt,
  955. LastLoginTime: loginRecord.CreateAt,
  956. MaxSuccessions: loginRecord.ContinuousDaysMax,
  957. SuccessfulInvitations: inviteCount,
  958. Withdrawals: withdrawCount,
  959. CumulativeWithdrawalAmount: cumulativeWithdrawalAmount,
  960. TurntableRuns: turntableRuns,
  961. }, nil
  962. }
  963. // 任务完成度统计
  964. func (s *Synthesis) TaskCompletion(req entity.TaskCompletionReq) (*entity.TaskCompletionResp, *code.Result) {
  965. page, pageSize := checkPageParam(req.Page, req.Size)
  966. // 构建查询条件
  967. filter := bson.M{}
  968. if req.UserName != "" {
  969. filter["userName"] = req.UserName
  970. }
  971. // 设置分页选项
  972. findOptions := options.Find()
  973. findOptions.SetSkip(int64((page - 1) * pageSize))
  974. findOptions.SetLimit(int64(pageSize))
  975. findOptions.SetSort(bson.D{{"createTime", -1}})
  976. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  977. defer cancel()
  978. collection := mdb.MDB.Collection(constant.CNamePlayer)
  979. // 查询数据
  980. var results []*entity.TaskCompletionDetail
  981. cursor, err := collection.Find(ctx, filter, findOptions)
  982. if err != nil {
  983. mhayaLogger.Warnf("TaskCompletion Find error:%v", err)
  984. return nil, common.NewResult(code.InternalError)
  985. }
  986. defer cursor.Close(ctx)
  987. // 解析结果
  988. for cursor.Next(ctx) {
  989. var result entity.TaskCompletionDetail
  990. if err := cursor.Decode(&result); err != nil {
  991. mhayaLogger.Warnf("TaskCompletion Decode error:%v", err)
  992. return nil, common.NewResult(code.InternalError)
  993. }
  994. results = append(results, &result)
  995. }
  996. if err := cursor.Err(); err != nil {
  997. mhayaLogger.Warnf("TaskCompletion cursor error:%v", err)
  998. return nil, common.NewResult(code.InternalError)
  999. }
  1000. // TODO 任务完成度统计相关信息
  1001. // TODO 获取总数total
  1002. var count int64
  1003. return &entity.TaskCompletionResp{
  1004. Details: results,
  1005. Total: count,
  1006. }, nil
  1007. }
  1008. // 用户行为检测
  1009. func (s *Synthesis) BehaviorMonitoring(req entity.BehaviorMonitoringReq) (*entity.BehaviorMonitoringResp, *code.Result) {
  1010. page, pageSize := checkPageParam(req.Page, req.Size)
  1011. // 构建查询条件
  1012. filter := bson.M{}
  1013. if req.UserName != "" {
  1014. filter["userName"] = req.UserName
  1015. }
  1016. // 设置分页选项
  1017. findOptions := options.Find()
  1018. findOptions.SetSkip(int64((page - 1) * pageSize))
  1019. findOptions.SetLimit(int64(pageSize))
  1020. findOptions.SetSort(bson.D{{"createTime", -1}})
  1021. ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
  1022. defer cancel()
  1023. collection := mdb.MDB.Collection(constant.CNamePlayer)
  1024. // 查询数据
  1025. var results []*entity.BehaviorMonitoringDetail
  1026. cursor, err := collection.Find(ctx, filter, findOptions)
  1027. if err != nil {
  1028. mhayaLogger.Warnf("BehaviorMonitoring Find error:%v", err)
  1029. return nil, common.NewResult(code.InternalError)
  1030. }
  1031. defer cursor.Close(ctx)
  1032. // 解析结果
  1033. for cursor.Next(ctx) {
  1034. var result entity.BehaviorMonitoringDetail
  1035. if err := cursor.Decode(&result); err != nil {
  1036. mhayaLogger.Warnf("BehaviorMonitoring Decode error:%v", err)
  1037. return nil, common.NewResult(code.InternalError)
  1038. }
  1039. results = append(results, &result)
  1040. }
  1041. if err := cursor.Err(); err != nil {
  1042. mhayaLogger.Warnf("BehaviorMonitoring cursor error:%v", err)
  1043. return nil, common.NewResult(code.InternalError)
  1044. }
  1045. // TODO 用户行为检测相关信息
  1046. // TODO 获取总数total
  1047. var count int64
  1048. return &entity.BehaviorMonitoringResp{
  1049. Details: results,
  1050. Total: count,
  1051. }, nil
  1052. }