tenant: admin order export csv

This commit is contained in:
2025-12-19 09:11:28 +08:00
parent 549339be74
commit 86a1a0a2cc
9 changed files with 718 additions and 0 deletions

View File

@@ -1,7 +1,9 @@
package services
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
@@ -26,6 +28,158 @@ import (
"go.ipao.vip/gen/types"
)
// AdminOrderExportCSV 租户管理员导出订单列表CSV 文本)。
func (s *order) AdminOrderExportCSV(ctx context.Context, tenantID int64, filter *dto.AdminOrderListFilter) (*dto.AdminOrderExportResponse, error) {
if tenantID <= 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("tenant_id must be > 0")
}
if filter == nil {
filter = &dto.AdminOrderListFilter{}
}
// 导出属于高消耗操作:限制最大行数,避免拖垮数据库。
const maxRows = 5000
logrus.WithFields(logrus.Fields{
"tenant_id": tenantID,
"max_rows": maxRows,
"user_id": lo.FromPtr(filter.UserID),
"username": filter.UsernameTrimmed(),
"content_id": lo.FromPtr(filter.ContentID),
"content_title": filter.ContentTitleTrimmed(),
"type": lo.FromPtr(filter.Type),
"status": lo.FromPtr(filter.Status),
}).Info("services.order.admin.export_csv")
tbl, query := models.OrderQuery.QueryContext(ctx)
conds := []gen.Condition{tbl.TenantID.Eq(tenantID)}
if filter.UserID != nil {
conds = append(conds, tbl.UserID.Eq(*filter.UserID))
}
if filter.Type != nil {
conds = append(conds, tbl.Type.Eq(*filter.Type))
}
if filter.Status != nil {
conds = append(conds, tbl.Status.Eq(*filter.Status))
}
if filter.CreatedAtFrom != nil {
conds = append(conds, tbl.CreatedAt.Gte(*filter.CreatedAtFrom))
}
if filter.CreatedAtTo != nil {
conds = append(conds, tbl.CreatedAt.Lte(*filter.CreatedAtTo))
}
if filter.PaidAtFrom != nil {
conds = append(conds, tbl.PaidAt.Gte(*filter.PaidAtFrom))
}
if filter.PaidAtTo != nil {
conds = append(conds, tbl.PaidAt.Lte(*filter.PaidAtTo))
}
if filter.AmountPaidMin != nil {
conds = append(conds, tbl.AmountPaid.Gte(*filter.AmountPaidMin))
}
if filter.AmountPaidMax != nil {
conds = append(conds, tbl.AmountPaid.Lte(*filter.AmountPaidMax))
}
if username := filter.UsernameTrimmed(); username != "" {
uTbl, _ := models.UserQuery.QueryContext(ctx)
query = query.LeftJoin(uTbl, uTbl.ID.EqCol(tbl.UserID))
conds = append(conds, uTbl.Username.Like(database.WrapLike(username)))
}
needItemJoin := (filter.ContentID != nil && *filter.ContentID > 0) || filter.ContentTitleTrimmed() != ""
if needItemJoin {
oiTbl, _ := models.OrderItemQuery.QueryContext(ctx)
query = query.LeftJoin(oiTbl, oiTbl.OrderID.EqCol(tbl.ID))
if filter.ContentID != nil && *filter.ContentID > 0 {
conds = append(conds, oiTbl.ContentID.Eq(*filter.ContentID))
}
if title := filter.ContentTitleTrimmed(); title != "" {
cTbl, _ := models.ContentQuery.QueryContext(ctx)
query = query.LeftJoin(cTbl, cTbl.ID.EqCol(oiTbl.ContentID))
conds = append(conds, cTbl.Title.Like(database.WrapLike(title)))
}
query = query.Group(tbl.ID)
}
// 排序:复用 AdminOrderPage 的白名单,避免任意字段导致注入/慢查询。
orderBys := make([]field.Expr, 0, 4)
allowedAsc := map[string]field.Expr{
"id": tbl.ID.Asc(),
"created_at": tbl.CreatedAt.Asc(),
"paid_at": tbl.PaidAt.Asc(),
"amount_paid": tbl.AmountPaid.Asc(),
}
allowedDesc := map[string]field.Expr{
"id": tbl.ID.Desc(),
"created_at": tbl.CreatedAt.Desc(),
"paid_at": tbl.PaidAt.Desc(),
"amount_paid": tbl.AmountPaid.Desc(),
}
for _, f := range filter.AscFields() {
f = strings.TrimSpace(f)
if f == "" {
continue
}
if ob, ok := allowedAsc[f]; ok {
orderBys = append(orderBys, ob)
}
}
for _, f := range filter.DescFields() {
f = strings.TrimSpace(f)
if f == "" {
continue
}
if ob, ok := allowedDesc[f]; ok {
orderBys = append(orderBys, ob)
}
}
if len(orderBys) == 0 {
orderBys = append(orderBys, tbl.ID.Desc())
} else {
orderBys = append(orderBys, tbl.ID.Desc())
}
items, err := query.Where(conds...).Order(orderBys...).Limit(maxRows).Find()
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
w := csv.NewWriter(buf)
_ = w.Write([]string{"id", "tenant_id", "user_id", "type", "status", "amount_paid", "paid_at", "created_at"})
for _, it := range items {
if it == nil {
continue
}
paidAt := ""
if !it.PaidAt.IsZero() {
paidAt = it.PaidAt.UTC().Format(time.RFC3339)
}
_ = w.Write([]string{
fmt.Sprintf("%d", it.ID),
fmt.Sprintf("%d", it.TenantID),
fmt.Sprintf("%d", it.UserID),
string(it.Type),
string(it.Status),
fmt.Sprintf("%d", it.AmountPaid),
paidAt,
it.CreatedAt.UTC().Format(time.RFC3339),
})
}
w.Flush()
if err := w.Error(); err != nil {
return nil, err
}
filename := fmt.Sprintf("tenant_%d_orders_%s.csv", tenantID, time.Now().UTC().Format("20060102_150405"))
return &dto.AdminOrderExportResponse{
Filename: filename,
ContentType: "text/csv",
CSV: buf.String(),
}, nil
}
// PurchaseOrderSnapshot 为“内容购买订单”的下单快照(用于历史展示与争议审计)。
type PurchaseOrderSnapshot struct {
// ContentID 内容ID。