559 lines
17 KiB
Go
559 lines
17 KiB
Go
package exportctrl
|
|
|
|
import (
|
|
"91porn-server/common/timeutil"
|
|
"91porn-server/models/v/export_task_mod"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"91porn-server/common"
|
|
"91porn-server/common/log"
|
|
"91porn-server/common/stderr"
|
|
"91porn-server/common/timeutil/timerange"
|
|
"91porn-server/models/commod"
|
|
"91porn-server/models/l/playlgmod"
|
|
"91porn-server/models/s/statordermod"
|
|
"91porn-server/models/s/statvidmod"
|
|
"91porn-server/models/v/exchcodemod"
|
|
"91porn-server/models/v/rchgordmod"
|
|
"91porn-server/models/v/txnmod"
|
|
"91porn-server/models/v/usermod"
|
|
"91porn-server/models/v/vidmod"
|
|
"91porn-server/models/v/wdordmod"
|
|
"91porn-server/web/service/exchcodeser"
|
|
"91porn-server/web/service/exportser"
|
|
"91porn-server/web/service/withdrawser"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shopspring/decimal"
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
)
|
|
|
|
type userListReq struct {
|
|
usermod.UserListSelector
|
|
common.StandQuery
|
|
IsPretendAcc *int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号
|
|
}
|
|
|
|
// Export doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/:fileType [get]
|
|
func Export(c *gin.Context) {
|
|
typeFile := c.Param("fileType")
|
|
t, err := strconv.ParseInt(typeFile, 10, 64)
|
|
if typeFile == "" || err != nil {
|
|
common.ServeJSON(c, stderr.ErrParamError, err.Error())
|
|
return
|
|
}
|
|
fpath, err := exportser.Export(t)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(c, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
c.Writer.WriteHeader(http.StatusOK)
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
c.File(fpath)
|
|
}
|
|
|
|
func getMuchUser(arg userListReq) []*usermod.User {
|
|
uInfo := []*usermod.User{}
|
|
cond, opt := common.StandQueryMap(arg.StandQuery, arg.UserListSelector)
|
|
//马甲账户
|
|
if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 {
|
|
cond["devID"] = bson.M{"$regex": usermod.SystemDevIDPrex, "$options": "i"}
|
|
}
|
|
//total, err := usermod.GetTotalCnt(cond, opt)
|
|
//if err != nil {
|
|
// return uInfo
|
|
//}
|
|
|
|
// 每次最多导出1000条
|
|
uInfo, _ = usermod.FindMany(cond, opt, int64(arg.PageNumber), int64(1000))
|
|
return uInfo
|
|
|
|
//if total < 1000 {
|
|
// uInfo, err = usermod.FindMany(cond, opt, int64(0), int64(1000))
|
|
// return uInfo
|
|
//}
|
|
////创建多个协程去拉取
|
|
//const goRoutineCnt int = 60
|
|
//var wg sync.WaitGroup
|
|
//size := total / int64(goRoutineCnt)
|
|
//wg.Add(goRoutineCnt)
|
|
//var mData [goRoutineCnt][]*usermod.User
|
|
//for i := 0; i < goRoutineCnt-1; i++ {
|
|
// common.GoParam(i, func(i int) {
|
|
// defer wg.Done()
|
|
// skip := int64(i) * size
|
|
// mData[i], err = usermod.FindMany(cond, opt, int64(skip), int64(size))
|
|
// })
|
|
//}
|
|
//common.Go(func() {
|
|
// defer wg.Done()
|
|
// skip := int64(goRoutineCnt-1) * size
|
|
// mData[goRoutineCnt-1], err = usermod.FindMany(cond, opt, int64(skip), int64(size+size))
|
|
//})
|
|
//wg.Wait()
|
|
//for i := 0; i < goRoutineCnt; i++ {
|
|
// uInfo = append(uInfo, mData[i]...)
|
|
//}
|
|
//return uInfo
|
|
}
|
|
|
|
func setArray2Map(datas []playlgmod.UIDCount) map[uint64]int {
|
|
m := make(map[uint64]int)
|
|
for _, d := range datas {
|
|
m[d.UID] = d.Count
|
|
}
|
|
return m
|
|
}
|
|
|
|
func calWatchedVideoCnt(uids []uint64) []playlgmod.UIDCount {
|
|
le := len(uids)
|
|
if le < 1000 {
|
|
datas, _ := playlgmod.HasWatchedVideoCnt(uids)
|
|
return datas
|
|
}
|
|
//创建多个协程去拉取
|
|
const goRoutineCnt = 10
|
|
var wg sync.WaitGroup
|
|
var mData [goRoutineCnt][]playlgmod.UIDCount
|
|
size := le / goRoutineCnt
|
|
wg.Add(goRoutineCnt)
|
|
for i := 0; i < goRoutineCnt-1; i++ {
|
|
common.GoParam(i, func(i int) {
|
|
defer wg.Done()
|
|
begin := i * size
|
|
end := begin + size - 1
|
|
mData[i], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end])
|
|
})
|
|
}
|
|
common.Go(func() {
|
|
defer wg.Done()
|
|
begin := (goRoutineCnt - 1) * size
|
|
end := le - 1
|
|
mData[goRoutineCnt-1], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end])
|
|
})
|
|
wg.Wait()
|
|
datas := []playlgmod.UIDCount{}
|
|
for i := 0; i < goRoutineCnt; i++ {
|
|
datas = append(datas, mData[i]...)
|
|
}
|
|
return datas
|
|
}
|
|
|
|
//
|
|
//func encodeUsers(users []*usermod.User, videoCntMap map[uint64]int, unDealVideoCntMap map[uint64]int) []usermod.ExportUser {
|
|
// usersLen := len(users)
|
|
// uids := make([]uint64, 0, usersLen)
|
|
// uInfos := make([]usermod.ExportUser, 0, usersLen)
|
|
// for _, u := range users {
|
|
// if u != nil {
|
|
// user := usermod.ExportUser{
|
|
// UID: u.UID,
|
|
// DevID: u.DevID,
|
|
// DevType: u.DevType,
|
|
// RegisterIP: u.RegisterIP,
|
|
// Mobile: u.Mobile,
|
|
// Gender: u.Gender,
|
|
// Channel: u.DistrictCode,
|
|
// Name: u.Name,
|
|
// PromotionCode: u.PromCode,
|
|
// Summary: u.Summary,
|
|
// Region: u.Region,
|
|
// Birthday: u.Birthday,
|
|
// VipLevel: u.VipLevel,
|
|
// VipExpireDate: u.VipExpireDate,
|
|
// CreatedAt: u.CreatedAt,
|
|
// MobileBindAt: u.MobileBindAt,
|
|
// HasLocked: u.HasLocked,
|
|
// HasBanned: u.HasBanned,
|
|
// TotalVideoCnt: videoCntMap[u.UID],
|
|
// UndealVideoCnt: unDealVideoCntMap[u.UID],
|
|
// LastVisitAt: u.LastVisitAt,
|
|
// }
|
|
// uInfos = append(uInfos, user)
|
|
// uids = append(uids, u.UID)
|
|
// }
|
|
// }
|
|
// wCnt := setArray2Map(calWatchedVideoCnt(uids))
|
|
// for i, u := range uInfos {
|
|
// uInfos[i].WatchCount = wCnt[u.UID]
|
|
// }
|
|
// return uInfos
|
|
//}
|
|
|
|
func getUIDs(users []*usermod.User) []uint64 {
|
|
uids := make([]uint64, 0, len(users))
|
|
for _, v := range users {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
uids = append(uids, v.UID)
|
|
}
|
|
return uids
|
|
}
|
|
|
|
// Export doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/:fileType [get]
|
|
func UserList(c *gin.Context) {
|
|
manager, err := common.GetAdminAct(c)
|
|
if err != nil {
|
|
common.ServeJSON(c, stderr.AdminIDErr, err.Error())
|
|
return
|
|
}
|
|
var arg userListReq
|
|
if err := c.ShouldBind(&arg); err != nil {
|
|
common.ServeJSON(c, stderr.ErrParamError, "user List arg error "+err.Error())
|
|
return
|
|
}
|
|
if code := IsWithinWeek(arg.StartTime, arg.EndTime); code != stderr.Success {
|
|
common.ServeJSON(c, code, code)
|
|
return
|
|
}
|
|
err = export_task_mod.CreateTask(export_task_mod.ExportUserTask, manager, arg)
|
|
if err != nil {
|
|
common.ServeJSON(c, stderr.ErrExportFileFail, "")
|
|
return
|
|
}
|
|
common.ServeJSON(c, stderr.Success, "成功")
|
|
}
|
|
|
|
// IsWithinWeek 判断是否是一周内
|
|
func IsWithinWeek(start *string, end *string) (code stderr.Code) {
|
|
if start == nil || end == nil {
|
|
return stderr.ErrTimeIsEmpty
|
|
}
|
|
startTime := timeutil.StrTimeToTime(*start)
|
|
endTime := timeutil.StrTimeToTime(*end)
|
|
hours := endTime.Sub(startTime).Hours()
|
|
if hours > 24*7 {
|
|
return stderr.ErrTimeWithinWeek
|
|
}
|
|
return stderr.Success
|
|
}
|
|
|
|
// RechargeOrder doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/rechargeOrder [get]
|
|
func RechargeOrder(ctx *gin.Context) {
|
|
var arg struct {
|
|
common.StandQuery
|
|
rchgordmod.RchgQueryReq
|
|
}
|
|
if err := ctx.ShouldBind(&arg); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, err)
|
|
return
|
|
}
|
|
//导出条数写死
|
|
arg.StandQuery.PageNumber = 1
|
|
arg.StandQuery.PageSize = 100000
|
|
_, data, err := rchgordmod.FindRechargesOrders(common.StandQueryMap(arg.StandQuery, arg.RchgQueryReq))
|
|
if err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrDbQueryError, "")
|
|
return
|
|
}
|
|
exptData := make([]*rchgordmod.RchargeOrderExport, len(data))
|
|
_100 := decimal.NewFromInt(100)
|
|
for i, v := range data {
|
|
exptData[i] = &rchgordmod.RchargeOrderExport{
|
|
ID: v.ID,
|
|
UID: v.UID,
|
|
DevID: v.DevID,
|
|
OID: v.OID,
|
|
UserIP: v.UserIP,
|
|
Name: v.Name,
|
|
Tel: v.Tel,
|
|
PayAct: v.PayAct,
|
|
DevType: v.DevType,
|
|
Amount: decimal.NewFromInt(v.Amount).Div(_100).StringFixed(4),
|
|
Money: decimal.NewFromInt(v.Money).Div(_100).StringFixed(4),
|
|
PayMoney: decimal.NewFromInt(v.PayMoney).Div(_100).StringFixed(4),
|
|
RechargeType: v.RechargeType,
|
|
ProductID: v.ProductID,
|
|
VipID: v.VipID,
|
|
Channel: v.Channel,
|
|
Status: v.Status,
|
|
StatusDesc: v.StatusDesc,
|
|
ProgressAt: v.ProgressAt,
|
|
FailureAt: v.FailureAt,
|
|
SuccessAt: v.SuccessAt,
|
|
PaymentAt: v.PaymentAt,
|
|
CreatedAt: v.CreatedAt,
|
|
UpdatedAt: v.UpdatedAt,
|
|
SourcePage: v.SourcePage,
|
|
SourceRef: v.SourceRef,
|
|
VideoID: v.VideoID,
|
|
ActivityID: v.ActivityID,
|
|
ExperimentID: v.ExperimentID,
|
|
ExperimentVariant: v.ExperimentVariant,
|
|
SessionID: v.SessionID,
|
|
}
|
|
}
|
|
fpath, err := exportser.RechargeOrderExport(exptData)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|
|
|
|
// WithdrawOrder doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/withdrawOrder [get]
|
|
func WithdrawOrder(ctx *gin.Context) {
|
|
var arg struct {
|
|
common.StandQuery
|
|
wdordmod.WithdrawWebQueryReq
|
|
}
|
|
if err := ctx.ShouldBind(&arg); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, err)
|
|
return
|
|
}
|
|
//导出条数写死
|
|
arg.StandQuery.PageNumber = 1
|
|
arg.StandQuery.PageSize = 100000
|
|
_, data, err := withdrawser.GetAllOrders(common.StandQueryMap(arg.StandQuery, arg.WithdrawWebQueryReq))
|
|
if err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrDbQueryError, "")
|
|
return
|
|
}
|
|
fpath, err := exportser.WithdrawOrderExport(data)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|
|
|
|
// GoldTurnover doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/withdrawOrder [get]
|
|
func GoldTurnover(ctx *gin.Context) {
|
|
req := txnmod.CoinLogReq{}
|
|
if err := ctx.ShouldBind(&req); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, nil)
|
|
return
|
|
}
|
|
m := make(map[string]interface{})
|
|
if req.UID > 0 {
|
|
m["uid"] = req.UID
|
|
}
|
|
if req.Balance == 1 {
|
|
i := make(map[string]interface{})
|
|
i["$gt"] = 0
|
|
m["amount"] = i
|
|
}
|
|
if req.Balance == 2 {
|
|
i := make(map[string]interface{})
|
|
i["$lt"] = 0
|
|
m["amount"] = i
|
|
}
|
|
if !req.End.IsZero() {
|
|
i := make(map[string]interface{})
|
|
i["$gte"] = req.Start
|
|
i["$lt"] = req.End
|
|
m["createdAt"] = i
|
|
}
|
|
if len(req.TranType) != 0 {
|
|
m["tranType"] = req.TranType
|
|
}
|
|
infos, _, _, _ := txnmod.GetCoinLogs(1, 100000, m)
|
|
fpath, err := exportser.GoldTurnoverExport(infos)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|
|
|
|
// VideoIncome doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/withdrawOrder [get]
|
|
func VideoIncome(ctx *gin.Context) {
|
|
type Query struct {
|
|
StartTime *time.Time `form:"startTime" json:"startTime" binding:""` //开始时间
|
|
EndTime *time.Time `form:"endTime" json:"endTime" binding:""` //结束时间
|
|
UID *uint64 `form:"uid" json:"uid" binding:""` //uid
|
|
Vid *string `form:"vid" json:"vid" binding:""` //vid
|
|
}
|
|
var arg struct {
|
|
commod.Page
|
|
Query
|
|
}
|
|
if err := ctx.ShouldBind(&arg); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, "stat VideoEarnStats arg error "+err.Error())
|
|
return
|
|
}
|
|
endTime := time.Now()
|
|
if arg.EndTime != nil {
|
|
endTime = timerange.LocDayRange(*arg.EndTime).Head
|
|
}
|
|
startTime := time.Time{}
|
|
if arg.StartTime != nil {
|
|
startTime = timerange.LocDayRange(*arg.StartTime).Head
|
|
}
|
|
var vid *primitive.ObjectID
|
|
if arg.Vid != nil {
|
|
id, _ := primitive.ObjectIDFromHex(*arg.Vid)
|
|
vid = &id
|
|
}
|
|
var vids []primitive.ObjectID
|
|
if arg.UID != nil {
|
|
list, _ := vidmod.FindMany((&vidmod.PublisherIDMatch{PublisherID: arg.UID}).New())
|
|
vids = list.IDs()
|
|
}
|
|
limit := int64(100000)
|
|
skip := int64(0)
|
|
page, _ := statvidmod.VideoStatPages(skip, limit,
|
|
statvidmod.SumDateGTEAndLTMatch{GTE: &startTime, LT: &endTime},
|
|
statvidmod.VidMatch{Vid: vid},
|
|
statvidmod.VidInMatch{Vids: vids},
|
|
)
|
|
fpath, err := exportser.VideoIncomeExport(page.List)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|
|
|
|
// OrderStat doc
|
|
// @Summary 导出文件
|
|
// @Description 导出excel文件
|
|
// @Tags Web-Export
|
|
// @Accept mpfd,json
|
|
// @Produce json,html
|
|
// @Param fileType query integer true "导出文件的类型"
|
|
// @Success 200 {string} json "{"msg": "操作成功", "data":[]}"
|
|
// @Failure 400 {string} json "{"msg": "操作失败"}"
|
|
// @Router /web/export/withdrawOrder [get]
|
|
func OrderStat(ctx *gin.Context) {
|
|
var args struct {
|
|
Type *int `form:"type" json:"type"` //订单类型
|
|
Channel *string `form:"channel" json:"channel"`
|
|
Start *time.Time `form:"start" json:"start"`
|
|
End *time.Time `form:"end" json:"end"`
|
|
}
|
|
if err := ctx.ShouldBind(&args); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, nil)
|
|
return
|
|
}
|
|
endTime := time.Now()
|
|
if args.End != nil {
|
|
endTime = *args.End
|
|
}
|
|
var startTime time.Time
|
|
if args.Start != nil {
|
|
startTime = *args.Start
|
|
}
|
|
skip := int64(0)
|
|
limit := int64(100000)
|
|
mats := []statordermod.Matcher{
|
|
(&statordermod.ChannelMatch{}).New(),
|
|
(&statordermod.TypeMatch{}).New(),
|
|
(&statordermod.SumDateGTEAndLTMatch{GTE: &startTime, LT: &endTime}).New(),
|
|
}
|
|
list, err := statordermod.List(nil, &skip, &limit, mats...)
|
|
if err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrDbQueryError, "")
|
|
return
|
|
}
|
|
fpath, err := exportser.OrderStatExport(list)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|
|
|
|
// CodeList 导出兑换码
|
|
func CodeList(ctx *gin.Context) {
|
|
var param = exchcodemod.ListReqParam{}
|
|
if err := ctx.ShouldBind(¶m); err != nil {
|
|
common.ServeJSON(ctx, stderr.ErrParamError, err)
|
|
return
|
|
}
|
|
data := exchcodeser.ExchangeCodes(param)
|
|
fpath, err := exportser.ExchangeCodeExport(data)
|
|
if err != nil {
|
|
log.Warn("request multipart wrong ", log.E(err))
|
|
common.ServeJSON(ctx, stderr.ErrExportFileFail, err.Error())
|
|
return
|
|
}
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(fpath)))
|
|
ctx.Header("Content-Type", "application/octet-stream")
|
|
ctx.File(fpath)
|
|
}
|