init: web_hgdjc_server
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package coinser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"videoweb/app/service/rechargeser"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
"videoweb/pub/common/useragent"
|
||||
"videoweb/pub/dataReport"
|
||||
"videoweb/pub/models/eventmod"
|
||||
"videoweb/pub/models/rechargepackagemod"
|
||||
"videoweb/pub/models/usermod"
|
||||
"videoweb/pub/models/vipordermod"
|
||||
"videoweb/pub/mongos"
|
||||
"videoweb/pub/pubdef"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// BuyCoinResp 购买金币下单响应
|
||||
type BuyCoinResp struct {
|
||||
TradeNo string `json:"tradeNo"`
|
||||
PayUrl string `json:"payUrl"`
|
||||
PayUrlMode string `json:"payUrlMode"`
|
||||
IsOpenNewBrowser bool `json:"isOpenNewBrowser"`
|
||||
}
|
||||
|
||||
// GetUserInfo 获取用户信息
|
||||
func GetUserInfo(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
||||
return usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: userId})
|
||||
}
|
||||
|
||||
// CreateCoinOrder 创建金币购买订单(统一写入 vip_order 表,OrderType=2)。
|
||||
// 防重复:15 秒内同一用户+同一套餐的 pending 订单直接返回原支付链接。
|
||||
func CreateCoinOrder(ctx context.Context,
|
||||
userId int64, userName string,
|
||||
packageId int64, payMode string,
|
||||
sysType string, devType string, deviceID string, ip string,
|
||||
districtCode string, sourcePageKey string, sourcePageName string,
|
||||
) (*BuyCoinResp, *srvdef.SrvErr) {
|
||||
|
||||
pkg, serr := rechargepackagemod.FindOneRechargePackage(ctx, bson.M{
|
||||
rechargepackagemod.FN.RechargePackage.Id: packageId,
|
||||
rechargepackagemod.FN.RechargePackage.PackageType: rechargepackagemod.PackageTypeCoin,
|
||||
rechargepackagemod.FN.RechargePackage.Status: rechargepackagemod.StatusEnabled,
|
||||
})
|
||||
if serr != nil || pkg == nil {
|
||||
return nil, srvdef.New(srvdef.ErrNoData, "金币套餐不存在或已下架", "")
|
||||
}
|
||||
coinAmount := pkg.BaseCoin + pkg.GiftCoin
|
||||
if coinAmount <= 0 {
|
||||
return nil, srvdef.New(srvdef.ErrParamError, "金币套餐到账数量无效", "")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dupFilter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.CoinCfgId: packageId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
vipordermod.FN.VipOrder.PayMode: payMode,
|
||||
vipordermod.FN.VipOrder.Status: pubdef.VipOrderPending,
|
||||
vipordermod.FN.VipOrder.CreatedAt: bson.M{"$gte": now.Add(-15 * time.Second)},
|
||||
}
|
||||
existing, _ := vipordermod.FindOneVipOrder(ctx, dupFilter)
|
||||
if existing != nil && existing.PayUrl != "" {
|
||||
return &BuyCoinResp{
|
||||
TradeNo: existing.TradeNo,
|
||||
PayUrl: existing.PayUrl,
|
||||
PayUrlMode: existing.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
tradeNo := pubdef.GetTrandNoWithUUID()
|
||||
|
||||
payInfo, serr := rechargeser.SubmitPayOrder(ctx, userId, userName, tradeNo, pkg.Price, payMode, sysType, devType, deviceID, ip)
|
||||
if serr != nil {
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
order := &vipordermod.VipOrder{
|
||||
Id: mongos.Lid.VipOrder.Get(),
|
||||
OrderType: vipordermod.OrderTypeCoin,
|
||||
UserId: userId,
|
||||
CoinCfgId: packageId, // 兼容历史字段,存购买套餐ID
|
||||
Coin: coinAmount,
|
||||
BaseCoin: pkg.BaseCoin,
|
||||
GiftCoin: pkg.GiftCoin,
|
||||
Amount: pkg.Price,
|
||||
PayMode: payMode,
|
||||
TradeNo: tradeNo,
|
||||
Oid: payInfo.Oid,
|
||||
Status: pubdef.VipOrderPending,
|
||||
PayUrl: payInfo.PayUrl,
|
||||
PayUrlMode: payInfo.PayUrlMode,
|
||||
PayChannel: payInfo.PayChannel,
|
||||
PayRate: payInfo.PayRate,
|
||||
IPAddr: ip,
|
||||
SysType: sysType,
|
||||
DistrictCode: strings.TrimSpace(districtCode),
|
||||
Client: vipordermod.NewOrderClient(useragent.FromContext(ctx), sourcePageKey, sourcePageName),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if serr = vipordermod.InsertOneVipOrder(ctx, order); serr != nil {
|
||||
log.Error("coinser:CreateCoinOrder InsertOneVipOrder failed",
|
||||
log.Any("tradeNo", tradeNo), log.E(serr))
|
||||
return nil, serr
|
||||
}
|
||||
|
||||
go reportCoinOrderCreated(order, pkg, ip)
|
||||
|
||||
return &BuyCoinResp{
|
||||
TradeNo: tradeNo,
|
||||
PayUrl: payInfo.PayUrl,
|
||||
PayUrlMode: payInfo.PayUrlMode,
|
||||
IsOpenNewBrowser: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func reportCoinOrderCreated(order *vipordermod.VipOrder, pkg *rechargepackagemod.RechargePackage, ip string) {
|
||||
ctx := context.Background()
|
||||
user, _ := usermod.FindOneUser(ctx, bson.M{usermod.FN.User.Id: order.UserId})
|
||||
districtCode := order.DistrictCode
|
||||
if user != nil && strings.TrimSpace(user.DistrictCode) != "" {
|
||||
districtCode = user.DistrictCode
|
||||
}
|
||||
|
||||
sourceKey, sourceName := orderSource(order)
|
||||
ua := order.Client.UA(order.SysType)
|
||||
eventId := newEventId(pubdef.TrackerAppId(), order.UserId, order.TradeNo, order.CreatedAt.Unix())
|
||||
commonField := dataReport.NewCommonField(eventId, dataReport.EventTypeOrderCreated, districtCode, ua, strconv.FormatInt(order.UserId, 10), ip)
|
||||
commonField.Payload = dataReport.JsonRawMessage(&dataReport.OrderCreatedEvent{
|
||||
OrderID: order.TradeNo,
|
||||
OrderType: "coin_purchase",
|
||||
ProductID: strconv.FormatInt(pkg.Id, 10),
|
||||
ProductName: pkg.Name,
|
||||
Amount: dataReport.JsonNumber(order.Amount),
|
||||
Currency: "CNY",
|
||||
CoinQuantity: dataReport.JsonNumber(order.Coin),
|
||||
SourcePageKey: sourceKey,
|
||||
SourcePageName: sourceName,
|
||||
CreateTime: dataReport.JsonNumber(order.CreatedAt.Unix()),
|
||||
})
|
||||
|
||||
eventData, _ := json.Marshal(commonField)
|
||||
if err := eventmod.InsertOneEvent(ctx, &eventmod.Event{
|
||||
EventId: commonField.EventID,
|
||||
EventType: commonField.Event,
|
||||
Data: string(eventData),
|
||||
Status: eventmod.Default,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
log.Error("reportCoinOrderCreated InsertOneEvent failed",
|
||||
log.Any("tradeNo", order.TradeNo), log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
func orderSource(order *vipordermod.VipOrder) (string, string) {
|
||||
key, name := "deposit", "充值中心"
|
||||
if order != nil && order.Client != nil {
|
||||
if value := strings.TrimSpace(order.Client.SourcePageKey); value != "" {
|
||||
key = value
|
||||
}
|
||||
if value := strings.TrimSpace(order.Client.SourcePageName); value != "" {
|
||||
name = value
|
||||
}
|
||||
}
|
||||
return key, name
|
||||
}
|
||||
|
||||
func newEventId(appId string, uid int64, orderId interface{}, createTime int64) string {
|
||||
input := fmt.Sprintf("%v%v%v%v", appId, uid, orderId, createTime)
|
||||
h := md5.New()
|
||||
h.Write([]byte(input))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// GetCoinOrderHistory 查询用户金币购买记录
|
||||
func GetCoinOrderHistory(ctx context.Context, userId int64, page, pageSize int64) (vipordermod.VipOrderList, int64, *srvdef.SrvErr) {
|
||||
filter := bson.M{
|
||||
vipordermod.FN.VipOrder.UserId: userId,
|
||||
vipordermod.FN.VipOrder.OrderType: vipordermod.OrderTypeCoin,
|
||||
}
|
||||
total, serr := vipordermod.CountVipOrder(ctx, filter)
|
||||
if serr != nil {
|
||||
return nil, 0, serr
|
||||
}
|
||||
skip := (page - 1) * pageSize
|
||||
opts := options.Find().
|
||||
SetSort(bson.M{vipordermod.FN.VipOrder.CreatedAt: -1}).
|
||||
SetSkip(skip).
|
||||
SetLimit(pageSize)
|
||||
list, serr := vipordermod.FindManyVipOrder(ctx, filter, opts)
|
||||
return list, total, serr
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user