来源:hgdjnet-server 9bfbafe10d417deea959bd53117bc88a10e60a83 项目身份 HKDJ / JHA-216,独立官网仓库,Jenkins 守卫键 hkdjnet 测试目录 /server/hkdj_net,App/Web API 30331/30332 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
255 lines
7.8 KiB
Go
255 lines
7.8 KiB
Go
package coinser
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/csv"
|
|
"hkdj-web-server/app/api/proto"
|
|
"hkdj-web-server/pub/common/response"
|
|
"hkdj-web-server/pub/common/srvdef"
|
|
"hkdj-web-server/pub/common/utils"
|
|
"hkdj-web-server/pub/models/cointransmod"
|
|
"hkdj-web-server/pub/pubdef"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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)
|
|
}
|