All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
600 lines
17 KiB
Go
600 lines
17 KiB
Go
package exporter
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
)
|
|
|
|
const (
|
|
orderExportBaseHeaderCount = 11
|
|
orderExportCardGroupSize = 2
|
|
orderExportMinCardGroups = 1
|
|
)
|
|
|
|
// OrderDataSource 订单导出数据源。
|
|
type OrderDataSource struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewOrderDataSource 创建订单导出数据源。
|
|
func NewOrderDataSource(db *gorm.DB) *OrderDataSource {
|
|
return &OrderDataSource{db: db}
|
|
}
|
|
|
|
// Scene 返回导出场景编码。
|
|
func (s *OrderDataSource) Scene() string {
|
|
return constants.ExportTaskSceneOrder
|
|
}
|
|
|
|
// Count 统计订单导出行数。
|
|
func (s *OrderDataSource) Count(ctx context.Context, params ExportParams) (int, error) {
|
|
query, err := s.applyFilters(ctx, s.baseQuery(ctx), params)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
return int(total), nil
|
|
}
|
|
|
|
// Headers 返回订单导出表头。
|
|
func (s *OrderDataSource) Headers(ctx context.Context, params ExportParams) ([]string, error) {
|
|
cardGroups, err := s.resolveCardGroupCount(ctx, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buildOrderExportHeaders(cardGroups), nil
|
|
}
|
|
|
|
// Fetch 按 offset/limit 查询订单导出数据。
|
|
func (s *OrderDataSource) Fetch(ctx context.Context, params ExportParams, offset, limit int) ([][]string, error) {
|
|
if limit <= 0 {
|
|
return [][]string{}, nil
|
|
}
|
|
|
|
cardGroups := orderCardGroupCountFromHeaders(params.ResolvedHeaders)
|
|
if cardGroups <= 0 {
|
|
resolved, err := s.resolveCardGroupCount(ctx, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cardGroups = resolved
|
|
}
|
|
|
|
orders, err := s.fetchOrders(ctx, params, offset, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(orders) == 0 {
|
|
return [][]string{}, nil
|
|
}
|
|
|
|
orderIDs := make([]uint, 0, len(orders))
|
|
deviceIDs := make([]uint, 0, len(orders))
|
|
for _, item := range orders {
|
|
orderIDs = append(orderIDs, item.ID)
|
|
if item.DeviceID != nil {
|
|
deviceIDs = append(deviceIDs, *item.DeviceID)
|
|
}
|
|
}
|
|
|
|
itemMap, err := s.fetchOrderItems(ctx, orderIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cardMap, err := s.fetchBoundCards(ctx, deviceIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
paymentMap, err := s.fetchPreferredPayments(ctx, orderIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rows := make([][]string, 0, len(orders))
|
|
for _, item := range orders {
|
|
var cards []orderExportCardRow
|
|
if item.DeviceID != nil {
|
|
cards = cardMap[*item.DeviceID]
|
|
}
|
|
rows = append(rows, buildOrderExportRow(item, itemMap[item.ID], paymentMap[item.ID], cards, cardGroups))
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) baseQuery(ctx context.Context) *gorm.DB {
|
|
return s.db.WithContext(ctx).
|
|
Table("tb_order AS o").
|
|
Where("o.deleted_at IS NULL")
|
|
}
|
|
|
|
// applyFilters 应用与 /api/admin/orders 当前实际行为一致的筛选条件。
|
|
// is_expired 在接口 Service 中未进入 Store 筛选,这里按当前接口行为忽略。
|
|
func (s *OrderDataSource) applyFilters(ctx context.Context, query *gorm.DB, params ExportParams) (*gorm.DB, error) {
|
|
query = s.applyOrderScope(query, params)
|
|
|
|
if params.UserType == constants.UserTypeAgent {
|
|
if params.CreatorShopID != nil {
|
|
query = query.Where("o.buyer_type = ? AND o.buyer_id = ?", model.BuyerTypeAgent, *params.CreatorShopID)
|
|
} else {
|
|
query = query.Where("o.buyer_type = ?", model.BuyerTypeAgent)
|
|
}
|
|
}
|
|
if paymentStatus, ok := filterInt(params.Filters, "payment_status"); ok {
|
|
query = query.Where("o.payment_status = ?", paymentStatus)
|
|
}
|
|
if paymentMethod, ok := filterString(params.Filters, "payment_method"); ok {
|
|
query = query.Where("o.payment_method = ?", paymentMethod)
|
|
}
|
|
if orderType, ok := filterString(params.Filters, "order_type"); ok {
|
|
query = query.Where("o.order_type = ?", orderType)
|
|
}
|
|
if orderNo, ok := filterString(params.Filters, "order_no"); ok {
|
|
query = query.Where("o.order_no = ?", orderNo)
|
|
}
|
|
if purchaseRole, ok := filterString(params.Filters, "purchase_role"); ok {
|
|
query = query.Where("o.purchase_role = ?", purchaseRole)
|
|
}
|
|
if sellerShopID, ok := filterUint(params.Filters, "seller_shop_id"); ok {
|
|
query = query.Where("o.seller_shop_id = ?", sellerShopID)
|
|
}
|
|
if start, ok := filterTime(params.Filters, "start_time"); ok {
|
|
query = query.Where("o.created_at >= ?", start)
|
|
}
|
|
if end, ok := filterTime(params.Filters, "end_time"); ok {
|
|
query = query.Where("o.created_at <= ?", end)
|
|
}
|
|
if buyerPhone, ok := filterString(params.Filters, "buyer_phone"); ok {
|
|
query = query.Where("o.buyer_phone = ?", buyerPhone)
|
|
}
|
|
if identifier, ok := filterString(params.Filters, "identifier"); ok {
|
|
iotCardID, deviceID, err := s.resolveOrderListAssetIdentifier(ctx, params, identifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assetQuery := s.db.Where("o.asset_identifier = ?", identifier)
|
|
if iotCardID != nil {
|
|
assetQuery = assetQuery.Or("o.iot_card_id = ?", *iotCardID)
|
|
}
|
|
if deviceID != nil {
|
|
assetQuery = assetQuery.Or("o.device_id = ?", *deviceID)
|
|
}
|
|
query = query.Where(assetQuery)
|
|
}
|
|
return query, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) applyOrderScope(query *gorm.DB, params ExportParams) *gorm.DB {
|
|
switch params.UserType {
|
|
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
|
|
return query
|
|
case constants.UserTypeAgent:
|
|
if len(params.ScopeShopIDs) == 0 {
|
|
return query.Where("1 = 0")
|
|
}
|
|
return query.Where(
|
|
s.db.Where("o.buyer_type = ? AND o.buyer_id IN ?", model.BuyerTypeAgent, params.ScopeShopIDs).
|
|
Or("o.seller_shop_id IN ?", params.ScopeShopIDs).
|
|
Or(s.db.Where("o.seller_shop_id IS NULL AND o.operator_type = ? AND o.operator_id IN ?", model.OperatorAccountTypeAgent, params.ScopeShopIDs)),
|
|
)
|
|
default:
|
|
return query.Where("1 = 0")
|
|
}
|
|
}
|
|
|
|
func (s *OrderDataSource) resolveCardGroupCount(ctx context.Context, params ExportParams) (int, error) {
|
|
filtered, err := s.applyFilters(ctx, s.baseQuery(ctx), params)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var count int64
|
|
query := filtered.
|
|
Select("COALESCE(MAX(bound_cards.card_count), 0)").
|
|
Joins(`
|
|
LEFT JOIN LATERAL (
|
|
SELECT COUNT(*) AS card_count
|
|
FROM tb_device_sim_binding AS b
|
|
WHERE b.device_id = o.device_id
|
|
AND b.bind_status = ?
|
|
AND b.deleted_at IS NULL
|
|
) AS bound_cards ON TRUE
|
|
`, constants.BindStatusBound)
|
|
if err := query.Scan(&count).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
if count < orderExportMinCardGroups {
|
|
return orderExportMinCardGroups, nil
|
|
}
|
|
return int(count), nil
|
|
}
|
|
|
|
func (s *OrderDataSource) fetchOrders(ctx context.Context, params ExportParams, offset, limit int) ([]orderExportRow, error) {
|
|
var orders []orderExportRow
|
|
filtered, err := s.applyFilters(ctx, s.baseQuery(ctx), params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := filtered.
|
|
Joins("LEFT JOIN tb_shop AS sh ON sh.id = o.seller_shop_id").
|
|
Select(`
|
|
o.id,
|
|
o.order_no,
|
|
o.asset_identifier,
|
|
o.seller_cost_price,
|
|
o.total_amount,
|
|
o.actual_paid_amount,
|
|
o.payment_status,
|
|
o.payment_method,
|
|
o.created_at,
|
|
o.device_id,
|
|
COALESCE(sh.shop_name, '') AS shop_name
|
|
`).
|
|
Order("o.id DESC").
|
|
Limit(limit).
|
|
Offset(offset)
|
|
if err := query.Scan(&orders).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return orders, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) fetchOrderItems(ctx context.Context, orderIDs []uint) (map[uint][]orderExportItemRow, error) {
|
|
result := make(map[uint][]orderExportItemRow, len(orderIDs))
|
|
if len(orderIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
var items []orderExportItemRow
|
|
if err := s.db.WithContext(ctx).
|
|
Table("tb_order_item").
|
|
Select("order_id, package_name, quantity").
|
|
Where("order_id IN ?", orderIDs).
|
|
Where("deleted_at IS NULL").
|
|
Order("order_id ASC, id ASC").
|
|
Scan(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, item := range items {
|
|
result[item.OrderID] = append(result[item.OrderID], item)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) fetchBoundCards(ctx context.Context, deviceIDs []uint) (map[uint][]orderExportCardRow, error) {
|
|
result := make(map[uint][]orderExportCardRow, len(deviceIDs))
|
|
deviceIDs = normalizeUintSlice(deviceIDs)
|
|
if len(deviceIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
var cards []orderExportCardRow
|
|
if err := s.db.WithContext(ctx).
|
|
Table("tb_device_sim_binding AS b").
|
|
Select(`
|
|
b.device_id,
|
|
b.slot_position,
|
|
c.msisdn,
|
|
c.iccid
|
|
`).
|
|
Joins("JOIN tb_iot_card AS c ON c.id = b.iot_card_id AND c.deleted_at IS NULL").
|
|
Where("b.device_id IN ?", deviceIDs).
|
|
Where("b.bind_status = ?", constants.BindStatusBound).
|
|
Where("b.deleted_at IS NULL").
|
|
Order("b.device_id ASC, b.slot_position ASC, b.id ASC").
|
|
Scan(&cards).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, card := range cards {
|
|
result[card.DeviceID] = append(result[card.DeviceID], card)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) fetchPreferredPayments(ctx context.Context, orderIDs []uint) (map[uint]orderExportPaymentRow, error) {
|
|
result := make(map[uint]orderExportPaymentRow, len(orderIDs))
|
|
if len(orderIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
var payments []orderExportPaymentRow
|
|
if err := s.db.WithContext(ctx).
|
|
Table("tb_payment").
|
|
Select("order_id, third_party_trade_no, status").
|
|
Where("order_id IN ?", orderIDs).
|
|
Where("order_type = ?", model.PaymentOrderTypePackage).
|
|
Where("deleted_at IS NULL").
|
|
Order("order_id ASC, CASE WHEN status = 1 THEN 0 ELSE 1 END ASC, created_at DESC, id DESC").
|
|
Scan(&payments).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, payment := range payments {
|
|
if _, exists := result[payment.OrderID]; exists {
|
|
continue
|
|
}
|
|
result[payment.OrderID] = payment
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) resolveOrderListAssetIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, *uint, error) {
|
|
if identifier == "" {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
var registry orderExportAssetIdentifierRow
|
|
registryErr := s.db.WithContext(ctx).
|
|
Table("tb_asset_identifier").
|
|
Select("asset_type, asset_id").
|
|
Where("identifier = ?", identifier).
|
|
First(®istry).Error
|
|
if registryErr != nil && registryErr != gorm.ErrRecordNotFound {
|
|
return nil, nil, registryErr
|
|
}
|
|
if registryErr == nil {
|
|
switch registry.AssetType {
|
|
case model.AssetTypeIotCard:
|
|
if s.assetVisible(ctx, params, "tb_iot_card", registry.AssetID) {
|
|
cardID := registry.AssetID
|
|
return &cardID, nil, nil
|
|
}
|
|
case model.AssetTypeDevice:
|
|
if s.assetVisible(ctx, params, "tb_device", registry.AssetID) {
|
|
deviceID := registry.AssetID
|
|
return nil, &deviceID, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if deviceID, err := s.resolveDeviceIDByIdentifier(ctx, params, identifier); err != nil {
|
|
return nil, nil, err
|
|
} else if deviceID != nil {
|
|
return nil, deviceID, nil
|
|
}
|
|
|
|
cardID, err := s.resolveCardIDByIdentifier(ctx, params, identifier)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return cardID, nil, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) assetVisible(ctx context.Context, params ExportParams, table string, id uint) bool {
|
|
var count int64
|
|
query := s.db.WithContext(ctx).Table(table).Where("id = ? AND deleted_at IS NULL", id)
|
|
if params.UserType == constants.UserTypeAgent {
|
|
if len(params.ScopeShopIDs) == 0 {
|
|
return false
|
|
}
|
|
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
|
|
}
|
|
return query.Count(&count).Error == nil && count > 0
|
|
}
|
|
|
|
func (s *OrderDataSource) resolveDeviceIDByIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, error) {
|
|
var row struct {
|
|
ID uint `gorm:"column:id"`
|
|
}
|
|
query := s.db.WithContext(ctx).
|
|
Table("tb_device").
|
|
Select("id").
|
|
Where("deleted_at IS NULL").
|
|
Where("virtual_no = ? OR imei = ? OR sn = ?", identifier, identifier, identifier)
|
|
if params.UserType == constants.UserTypeAgent {
|
|
if len(params.ScopeShopIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
|
|
}
|
|
err := query.First(&row).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row.ID, nil
|
|
}
|
|
|
|
func (s *OrderDataSource) resolveCardIDByIdentifier(ctx context.Context, params ExportParams, identifier string) (*uint, error) {
|
|
conditions := []string{"virtual_no = ?", "msisdn = ?", "iccid = ?"}
|
|
args := []any{identifier, identifier, identifier}
|
|
if len(identifier) == 19 {
|
|
conditions = append(conditions, "iccid_19 = ?")
|
|
args = append(args, identifier)
|
|
} else if len(identifier) == 20 {
|
|
conditions = append(conditions, "iccid_20 = ?")
|
|
args = append(args, identifier)
|
|
}
|
|
|
|
var row struct {
|
|
ID uint `gorm:"column:id"`
|
|
}
|
|
query := s.db.WithContext(ctx).
|
|
Table("tb_iot_card").
|
|
Select("id").
|
|
Where("deleted_at IS NULL").
|
|
Where(strings.Join(conditions, " OR "), args...)
|
|
if params.UserType == constants.UserTypeAgent {
|
|
if len(params.ScopeShopIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
query = query.Where("shop_id IN ?", params.ScopeShopIDs)
|
|
}
|
|
err := query.First(&row).Error
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &row.ID, nil
|
|
}
|
|
|
|
type orderExportRow struct {
|
|
ID uint `gorm:"column:id"`
|
|
OrderNo string `gorm:"column:order_no"`
|
|
ShopName string `gorm:"column:shop_name"`
|
|
AssetIdentifier string `gorm:"column:asset_identifier"`
|
|
SellerCostPrice int64 `gorm:"column:seller_cost_price"`
|
|
TotalAmount int64 `gorm:"column:total_amount"`
|
|
ActualPaidAmount *int64 `gorm:"column:actual_paid_amount"`
|
|
PaymentStatus int `gorm:"column:payment_status"`
|
|
PaymentMethod string `gorm:"column:payment_method"`
|
|
CreatedAt time.Time `gorm:"column:created_at"`
|
|
DeviceID *uint `gorm:"column:device_id"`
|
|
}
|
|
|
|
type orderExportItemRow struct {
|
|
OrderID uint `gorm:"column:order_id"`
|
|
PackageName string `gorm:"column:package_name"`
|
|
Quantity int `gorm:"column:quantity"`
|
|
}
|
|
|
|
type orderExportPaymentRow struct {
|
|
OrderID uint `gorm:"column:order_id"`
|
|
ThirdPartyTradeNo string `gorm:"column:third_party_trade_no"`
|
|
Status int `gorm:"column:status"`
|
|
}
|
|
|
|
type orderExportCardRow struct {
|
|
DeviceID uint `gorm:"column:device_id"`
|
|
SlotPosition int `gorm:"column:slot_position"`
|
|
MSISDN string `gorm:"column:msisdn"`
|
|
ICCID string `gorm:"column:iccid"`
|
|
}
|
|
|
|
type orderExportAssetIdentifierRow struct {
|
|
AssetType string `gorm:"column:asset_type"`
|
|
AssetID uint `gorm:"column:asset_id"`
|
|
}
|
|
|
|
func buildOrderExportHeaders(cardGroups int) []string {
|
|
if cardGroups < orderExportMinCardGroups {
|
|
cardGroups = orderExportMinCardGroups
|
|
}
|
|
|
|
headers := []string{
|
|
"订单号",
|
|
"店铺",
|
|
"虚拟号",
|
|
"套餐名称",
|
|
"成本价",
|
|
"下单金额",
|
|
"支付状态",
|
|
"支付方式",
|
|
"数量",
|
|
"下单时间",
|
|
"第三方单号",
|
|
}
|
|
for i := 1; i <= cardGroups; i++ {
|
|
headers = append(headers,
|
|
fmt.Sprintf("接入号%d", i),
|
|
fmt.Sprintf("ICCID%d", i),
|
|
)
|
|
}
|
|
return headers
|
|
}
|
|
|
|
func buildOrderExportRow(item orderExportRow, items []orderExportItemRow, payment orderExportPaymentRow, cards []orderExportCardRow, cardGroups int) []string {
|
|
if cardGroups < orderExportMinCardGroups {
|
|
cardGroups = orderExportMinCardGroups
|
|
}
|
|
|
|
row := []string{
|
|
item.OrderNo,
|
|
item.ShopName,
|
|
item.AssetIdentifier,
|
|
joinOrderPackageNames(items),
|
|
formatMoneyYuan(item.SellerCostPrice),
|
|
formatMoneyYuan(orderActualPaidAmount(item)),
|
|
constants.GetOrderPaymentStatusName(item.PaymentStatus),
|
|
formatOrderPaymentMethod(item.PaymentMethod),
|
|
formatOrderQuantity(items),
|
|
item.CreatedAt.Format(exportTimeLayout),
|
|
payment.ThirdPartyTradeNo,
|
|
}
|
|
for i := 0; i < cardGroups; i++ {
|
|
if i >= len(cards) {
|
|
row = append(row, "", "")
|
|
continue
|
|
}
|
|
row = append(row, cards[i].MSISDN, cards[i].ICCID)
|
|
}
|
|
return row
|
|
}
|
|
|
|
func orderCardGroupCountFromHeaders(headers []string) int {
|
|
if len(headers) < orderExportBaseHeaderCount {
|
|
return 0
|
|
}
|
|
cardColumnCount := len(headers) - orderExportBaseHeaderCount
|
|
if cardColumnCount <= 0 || cardColumnCount%orderExportCardGroupSize != 0 {
|
|
return 0
|
|
}
|
|
return cardColumnCount / orderExportCardGroupSize
|
|
}
|
|
|
|
func joinOrderPackageNames(items []orderExportItemRow) string {
|
|
if len(items) == 0 {
|
|
return ""
|
|
}
|
|
names := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
if item.PackageName != "" {
|
|
names = append(names, item.PackageName)
|
|
}
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|
|
|
|
func formatOrderQuantity(items []orderExportItemRow) string {
|
|
if len(items) == 0 {
|
|
return "0"
|
|
}
|
|
total := 0
|
|
for _, item := range items {
|
|
total += item.Quantity
|
|
}
|
|
return fmt.Sprintf("%d", total)
|
|
}
|
|
|
|
func orderActualPaidAmount(item orderExportRow) int64 {
|
|
if item.ActualPaidAmount != nil {
|
|
return *item.ActualPaidAmount
|
|
}
|
|
if item.PaymentStatus == model.PaymentStatusPaid {
|
|
return item.TotalAmount
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func formatOrderPaymentMethod(method string) string {
|
|
switch method {
|
|
case model.PaymentMethodWallet:
|
|
return "钱包支付"
|
|
case model.PaymentMethodWechat:
|
|
return "微信支付"
|
|
case model.PaymentMethodAlipay:
|
|
return "支付宝支付"
|
|
case model.PaymentMethodOffline:
|
|
return "线下支付"
|
|
default:
|
|
return method
|
|
}
|
|
}
|