326 lines
8.5 KiB
Go
326 lines
8.5 KiB
Go
package srv_contentexcel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"videoweb/pub/common/srvdef"
|
|
"videoweb/pub/models/videoinformationmod"
|
|
"videoweb/pub/pubdef"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
)
|
|
|
|
const queryBatchSize = 500
|
|
|
|
var importAddedTimeStart = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local)
|
|
|
|
func randomImportAddedTime(r *rand.Rand) time.Time {
|
|
now := time.Now()
|
|
if !now.After(importAddedTimeStart) {
|
|
return now
|
|
}
|
|
if r == nil {
|
|
r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
}
|
|
delta := now.UnixNano() - importAddedTimeStart.UnixNano()
|
|
if delta <= 0 {
|
|
return now
|
|
}
|
|
return importAddedTimeStart.Add(time.Duration(r.Int63n(delta + 1)))
|
|
}
|
|
|
|
// Export 按条件导出视频 SEO 字段为 Excel。
|
|
func Export(ctx context.Context, req *ExportReq) ([]byte, string, *srvdef.SrvErr) {
|
|
contentType, serr := normalizeContentType(req.Type)
|
|
if serr != nil {
|
|
return nil, "", serr
|
|
}
|
|
|
|
dateFilter, serr := buildTimeRangeFilter(req.StartTime, req.EndTime)
|
|
if serr != nil {
|
|
return nil, "", serr
|
|
}
|
|
|
|
ids, serr := normalizeExportIDs(req.Ids)
|
|
if serr != nil {
|
|
return nil, "", serr
|
|
}
|
|
|
|
var rows []ContentRow
|
|
var err error
|
|
switch contentType {
|
|
case ContentTypeVideo:
|
|
rows, err = queryVideoRows(ctx, dateFilter, req.Status, ids)
|
|
}
|
|
if err != nil {
|
|
return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
|
|
}
|
|
|
|
data, err := buildWorkbook(rows)
|
|
if err != nil {
|
|
return nil, "", srvdef.New(srvdef.ExcelExportErr, err.Error())
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s_content_%s.xlsx", contentType, time.Now().Format("20060102150405"))
|
|
return data, filename, nil
|
|
}
|
|
|
|
// Import 读取 Excel 并按 ID 批量更新标题、描述、关键词。
|
|
func Import(ctx context.Context, contentType string, reader io.Reader) (*ImportResult, *srvdef.SrvErr) {
|
|
normalizedType, serr := normalizeContentType(contentType)
|
|
if serr != nil {
|
|
return nil, serr
|
|
}
|
|
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "读取上传文件失败")
|
|
}
|
|
|
|
rows, err := parseWorkbook(data)
|
|
if err != nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, err.Error())
|
|
}
|
|
|
|
result := &ImportResult{Total: len(rows)}
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
for _, row := range rows {
|
|
var updateErr *srvdef.SrvErr
|
|
switch normalizedType {
|
|
case ContentTypeVideo:
|
|
updateErr = updateVideoRow(ctx, row, r)
|
|
}
|
|
if updateErr != nil {
|
|
result.Failed++
|
|
result.Errors = append(result.Errors, fmt.Sprintf("ID=%d: %s", row.ID, updateErr.Tips))
|
|
continue
|
|
}
|
|
result.Success++
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func normalizeContentType(raw string) (string, *srvdef.SrvErr) {
|
|
switch stringsTrimLower(raw) {
|
|
case ContentTypeVideo:
|
|
return stringsTrimLower(raw), nil
|
|
default:
|
|
return "", srvdef.New(srvdef.ErrParamError, "type 仅支持 video")
|
|
}
|
|
}
|
|
|
|
func stringsTrimLower(s string) string {
|
|
return strings.ToLower(strings.TrimSpace(s))
|
|
}
|
|
|
|
func buildTimeRangeFilter(startTime, endTime string) (bson.M, *srvdef.SrvErr) {
|
|
filter := bson.M{}
|
|
start, err := parseOptionalTime(startTime, false)
|
|
if err != nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "startTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
|
|
}
|
|
end, err := parseOptionalTime(endTime, true)
|
|
if err != nil {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "endTime 格式错误,支持 2006-01-02 或 2006-01-02 15:04:05")
|
|
}
|
|
if start != nil && end != nil && start.After(*end) {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "startTime 不能晚于 endTime")
|
|
}
|
|
if start != nil {
|
|
filter["$gte"] = *start
|
|
}
|
|
if end != nil {
|
|
filter["$lte"] = *end
|
|
}
|
|
if len(filter) == 0 {
|
|
return nil, nil
|
|
}
|
|
return filter, nil
|
|
}
|
|
|
|
func parseOptionalTime(raw string, endOfDay bool) (*time.Time, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
layouts := []string{
|
|
"2006-01-02 15:04:05",
|
|
"2006-01-02T15:04:05",
|
|
time.RFC3339,
|
|
}
|
|
for _, layout := range layouts {
|
|
if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil {
|
|
return &t, nil
|
|
}
|
|
}
|
|
if t, err := time.ParseInLocation("2006-01-02", raw, time.Local); err == nil {
|
|
if endOfDay {
|
|
end := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
|
|
return &end, nil
|
|
}
|
|
return &t, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid time: %s", raw)
|
|
}
|
|
|
|
func applyTimeFilter(base bson.M, field string, rangeFilter bson.M) bson.M {
|
|
if len(rangeFilter) == 0 {
|
|
return base
|
|
}
|
|
base[field] = rangeFilter
|
|
return base
|
|
}
|
|
|
|
func applyStatusFilter(base bson.M, field string, status *pubdef.InformationStatus) bson.M {
|
|
if status == nil {
|
|
return base
|
|
}
|
|
base[field] = *status
|
|
return base
|
|
}
|
|
|
|
func applyIDsFilter(base bson.M, field string, ids []int64) bson.M {
|
|
if len(ids) == 0 {
|
|
return base
|
|
}
|
|
base[field] = bson.M{"$in": ids}
|
|
return base
|
|
}
|
|
|
|
// ParseExportIDs 解析导出接口的 ids 查询参数,支持 ids=1&ids=2 或 ids=1,2,3。
|
|
func ParseExportIDs(raw []string) ([]int64, *srvdef.SrvErr) {
|
|
if len(raw) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
ids := make([]int64, 0, len(raw))
|
|
seen := make(map[int64]struct{}, len(raw))
|
|
for _, item := range raw {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
for _, part := range strings.Split(item, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
id, err := strconv.ParseInt(part, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func normalizeExportIDs(raw []int64) ([]int64, *srvdef.SrvErr) {
|
|
if len(raw) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
ids := make([]int64, 0, len(raw))
|
|
seen := make(map[int64]struct{}, len(raw))
|
|
for _, id := range raw {
|
|
if id <= 0 {
|
|
return nil, srvdef.New(srvdef.ErrParamError, "ids 格式错误,需为正整数")
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
ids = append(ids, id)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func queryVideoRows(ctx context.Context, dateFilter bson.M, status *pubdef.InformationStatus, ids []int64) ([]ContentRow, error) {
|
|
filter := applyIDsFilter(applyStatusFilter(applyTimeFilter(bson.M{
|
|
videoinformationmod.FN.VideoInformation.MediaType: bson.M{
|
|
"$in": []videoinformationmod.MediaType{
|
|
videoinformationmod.MediaType_Video,
|
|
videoinformationmod.MediaType_AV,
|
|
},
|
|
},
|
|
}, videoinformationmod.FN.VideoInformation.CreatedAt, dateFilter),
|
|
videoinformationmod.FN.VideoInformation.Status, status),
|
|
videoinformationmod.FN.VideoInformation.Id, ids)
|
|
|
|
return queryRowsInBatches(ctx, func(skip int64) ([]ContentRow, bool, error) {
|
|
opt := options.Find().
|
|
SetSkip(skip).
|
|
SetLimit(queryBatchSize).
|
|
SetSort(bson.M{videoinformationmod.FN.VideoInformation.Id: 1}).
|
|
SetProjection(bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: 1,
|
|
videoinformationmod.FN.VideoInformation.Title: 1,
|
|
videoinformationmod.FN.VideoInformation.Desc: 1,
|
|
videoinformationmod.FN.VideoInformation.Keyworld: 1,
|
|
})
|
|
|
|
list, serr := videoinformationmod.FindManyVideoInformations(ctx, filter, opt)
|
|
if serr != nil {
|
|
return nil, false, fmt.Errorf("%s", serr.Tips)
|
|
}
|
|
rows := make([]ContentRow, 0, len(list))
|
|
for _, item := range list {
|
|
if item == nil {
|
|
continue
|
|
}
|
|
rows = append(rows, ContentRow{
|
|
ID: item.Id,
|
|
Title: item.Title,
|
|
Desc: item.Desc,
|
|
Keyworld: item.Keyworld,
|
|
})
|
|
}
|
|
return rows, int64(len(list)) == queryBatchSize, nil
|
|
})
|
|
}
|
|
|
|
func queryRowsInBatches(ctx context.Context, fetch func(skip int64) ([]ContentRow, bool, error)) ([]ContentRow, error) {
|
|
all := make([]ContentRow, 0)
|
|
var skip int64
|
|
for {
|
|
rows, hasMore, err := fetch(skip)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all = append(all, rows...)
|
|
if !hasMore {
|
|
break
|
|
}
|
|
skip += queryBatchSize
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
func updateVideoRow(ctx context.Context, row ContentRow, r *rand.Rand) *srvdef.SrvErr {
|
|
now := time.Now()
|
|
addedTime := randomImportAddedTime(r)
|
|
update := bson.M{
|
|
"$set": bson.M{
|
|
videoinformationmod.FN.VideoInformation.Title: row.Title,
|
|
videoinformationmod.FN.VideoInformation.Desc: row.Desc,
|
|
videoinformationmod.FN.VideoInformation.Keyworld: row.Keyworld,
|
|
videoinformationmod.FN.VideoInformation.Status: pubdef.InformationStatusVerifyOK,
|
|
videoinformationmod.FN.VideoInformation.AddedTime: addedTime,
|
|
videoinformationmod.FN.VideoInformation.UpdatedAt: now,
|
|
},
|
|
}
|
|
return videoinformationmod.UpdateOneVideoInformation(ctx, bson.M{
|
|
videoinformationmod.FN.VideoInformation.Id: row.ID,
|
|
}, update)
|
|
}
|