package coinser import ( "bytes" "context" "encoding/csv" "strconv" "time" "videoweb/app/api/proto" "videoweb/pub/common/response" "videoweb/pub/common/srvdef" "videoweb/pub/common/utils" "videoweb/pub/models/cointransmod" "videoweb/pub/pubdef" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) const ( // coinTransDefaultPageSize 金币余额明细默认每页数量 coinTransDefaultPageSize int64 = 10 // coinTransMaxPageSize 金币余额明细最大每页数量,避免单次请求拉取过多流水 coinTransMaxPageSize int64 = 100 // coinTransTypeAll 全部交易 coinTransTypeAll int64 = 0 // coinTransTypeRecharge 充值交易 coinTransTypeRecharge int64 = 1 // coinTransTypeConsume 消费交易 coinTransTypeConsume int64 = 2 // coinTransTypeReward 奖励交易 coinTransTypeReward int64 = 3 ) var ( // coinTransListProjection 余额明细只读取列表展示需要的字段,降低Mongo读取成本 coinTransListProjection = bson.D{ {Key: cointransmod.FN.CoinTrans.Id, Value: 1}, {Key: cointransmod.FN.CoinTrans.FlowDir, Value: 1}, {Key: cointransmod.FN.CoinTrans.TranType, Value: 1}, {Key: cointransmod.FN.CoinTrans.Amount, Value: 1}, {Key: cointransmod.FN.CoinTrans.BalanceBefore, Value: 1}, {Key: cointransmod.FN.CoinTrans.BalanceAfter, Value: 1}, {Key: cointransmod.FN.CoinTrans.OrderNo, Value: 1}, {Key: cointransmod.FN.CoinTrans.RelatedId, Value: 1}, {Key: cointransmod.FN.CoinTrans.Remark, Value: 1}, {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: 1}, } // coinTransListSort 按交易时间倒序展示余额明细 coinTransListSort = bson.D{ {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1}, } // coinTransListHint 匹配 coin_trans 的 userId+createdAt 复合索引 coinTransListHint = bson.D{ {Key: cointransmod.FN.CoinTrans.UserId, Value: 1}, {Key: cointransmod.FN.CoinTrans.CreatedAt, Value: -1}, } ) // GetCoinTransList 查询当前用户金币余额明细 // // 只查询当前登录用户的流水,按交易时间倒序分页,pageSize 最大限制为100 func GetCoinTransList(ctx context.Context, userId int64, page, pageSize int64, transType int64) (*response.BasePageResp, *srvdef.SrvErr) { if userId <= 0 { return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "") } page, pageSize = normalizeCoinTransPage(page, pageSize) filter := bson.M{ cointransmod.FN.CoinTrans.UserId: userId, } if err := applyCoinTransTypeFilter(filter, transType); err != nil { return nil, err } total, err := cointransmod.CountCoinTrans(ctx, filter) if err != nil { return nil, err } if total <= 0 { return &response.BasePageResp{ TotalCount: 0, PageSize: int(pageSize), PageNum: int(page), List: make([]*proto.CoinTransListItemResp, 0), }, nil } list, err1 := cointransmod.FindManyCoinTrans(ctx, filter, options.Find(). SetProjection(coinTransListProjection). SetSort(coinTransListSort). SetHint(coinTransListHint). SetSkip((page-1)*pageSize). SetLimit(pageSize)) if err1 != nil { return nil, err1 } resp := make([]*proto.CoinTransListItemResp, 0, len(list)) for _, item := range list { resp = append(resp, coinTransToListItemResp(item)) } return &response.BasePageResp{ TotalCount: total, PageSize: int(pageSize), PageNum: int(page), List: resp, }, nil } // ExportCoinTrans 导出当前用户金币余额明细CSV func ExportCoinTrans(ctx context.Context, userId int64, transType int64) ([]byte, string, *srvdef.SrvErr) { if userId <= 0 { return nil, "", srvdef.New(srvdef.ErrUserNotExsit, "", "") } filter := bson.M{ cointransmod.FN.CoinTrans.UserId: userId, } if err := applyCoinTransTypeFilter(filter, transType); err != nil { return nil, "", err } list, err := cointransmod.FindManyCoinTrans(ctx, filter, options.Find(). SetProjection(coinTransListProjection). SetSort(coinTransListSort). SetHint(coinTransListHint)) if err != nil { return nil, "", err } data, serr := buildCoinTransCSV(list) if serr != nil { return nil, "", serr } return data, coinTransExportFileName(transType), nil } // buildCoinTransCSV 构建金币流水CSV文件内容 func buildCoinTransCSV(list cointransmod.CoinTransList) ([]byte, *srvdef.SrvErr) { var buf bytes.Buffer buf.WriteString("\xEF\xBB\xBF") writer := csv.NewWriter(&buf) if err := writer.Write([]string{"类型", "说明", "时间", "金币变动"}); err != nil { return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误") } for _, item := range list { resp := coinTransToListItemResp(item) if resp == nil { continue } if err := writer.Write([]string{ resp.TranTypeName, resp.Title, resp.CreatedAt, formatCoinTransChange(resp.ChangeAmount), }); err != nil { return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误") } } writer.Flush() if err := writer.Error(); err != nil { return nil, srvdef.New(srvdef.ExcelExportErr, "文件导出错误", "文件导出错误") } return buf.Bytes(), nil } // coinTransExportFileName 生成金币流水导出文件名 func coinTransExportFileName(transType int64) string { name := "交易明细" switch transType { case coinTransTypeRecharge: name = "充值明细" case coinTransTypeConsume: name = "消费明细" case coinTransTypeReward: name = "奖励明细" } return name + "_" + time.Now().In(utils.LocBJ).Format("20060102150405") + ".csv" } // formatCoinTransChange 格式化金币变动值 func formatCoinTransChange(change int64) string { if change > 0 { return "+" + strconv.FormatInt(change, 10) } return strconv.FormatInt(change, 10) } // applyCoinTransTypeFilter 按交易明细Tab追加筛选条件 func applyCoinTransTypeFilter(filter bson.M, transType int64) *srvdef.SrvErr { switch transType { case coinTransTypeAll: return nil case coinTransTypeRecharge: filter[cointransmod.FN.CoinTrans.TranType] = cointransmod.TranTypeCoinRecharge case coinTransTypeConsume: filter[cointransmod.FN.CoinTrans.FlowDir] = cointransmod.FlowOut case coinTransTypeReward: filter[cointransmod.FN.CoinTrans.FlowDir] = cointransmod.FlowIn filter[cointransmod.FN.CoinTrans.TranType] = bson.M{"$ne": cointransmod.TranTypeCoinRecharge} default: return srvdef.New(srvdef.ErrParamError, "类型错误", "类型错误") } return nil } // normalizeCoinTransPage 规范化金币流水分页参数 // // page 小于等于0时按第1页处理;pageSize 未传时使用默认值,超过上限时截断 func normalizeCoinTransPage(page, pageSize int64) (int64, int64) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = coinTransDefaultPageSize } if pageSize > coinTransMaxPageSize { pageSize = coinTransMaxPageSize } return page, pageSize } // coinTransToListItemResp 转换金币流水响应项 // // amount 保持数据库中的正数金额,changeAmount 根据收支方向转换为带正负号的变动金额 func coinTransToListItemResp(item *cointransmod.CoinTrans) *proto.CoinTransListItemResp { title := item.Remark if title == "" { title = item.TranType.Desc() } changeAmount := item.Amount if item.FlowDir == cointransmod.FlowOut { changeAmount = -item.Amount } return &proto.CoinTransListItemResp{ Id: item.Id, Title: title, FlowDir: item.FlowDir, FlowDirName: item.FlowDir.Desc(), TranType: item.TranType, TranTypeName: item.TranType.Desc(), Amount: item.Amount, ChangeAmount: changeAmount, BalanceBefore: item.BalanceBefore, BalanceAfter: item.BalanceAfter, OrderNo: item.OrderNo, RelatedId: item.RelatedId, Remark: item.Remark, CreatedAt: formatCoinTransTime(item.CreatedAt), } } func formatCoinTransTime(t time.Time) string { if t.IsZero() { return "" } return t.In(utils.LocBJ).Format(pubdef.TimeISO) }