chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestClonedProdDataOperations(t *testing.T) {
|
||||
dsn := "hfb:secret@tcp(127.0.0.1:13306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("无法连接本地 hfb_sys (可能在CI环境或容器未开启): %v", err)
|
||||
}
|
||||
|
||||
var pickupCount int64
|
||||
if err := db.Model(&model.AdminPickup{}).Count(&pickupCount).Error; err != nil || pickupCount < 1000 {
|
||||
t.Skipf("本地 hfb_sys 未包含生产克隆数据 (当前 count=%d, err=%v),跳过克隆数据回归测试", pickupCount, err)
|
||||
}
|
||||
|
||||
repo := NewRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Logf("=== 1. 验证克隆生产环境数据基础特征 (单量: %d) ===", pickupCount)
|
||||
if pickupCount != 4498 {
|
||||
t.Logf("注意: 当前提号记录数为 %d (线上基准 4498)", pickupCount)
|
||||
}
|
||||
|
||||
// 为保证测试幂等性,先将测试用的几个变体恢复至原始未映射状态
|
||||
db.Exec("DELETE FROM pickup_shop_mappings WHERE source_name IN ('王大锤游戏专营', '淘宝王大锤商行', '王大锤商行')")
|
||||
db.Exec("UPDATE admin_pickups SET shop_name = raw_shop_name WHERE raw_shop_name IN ('王大锤游戏专营', '淘宝王大锤商行', '王大锤商行')")
|
||||
|
||||
// 1. 获取初始历史聚合统计
|
||||
stats, err := repo.GetHistoricalShopStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetHistoricalShopStats() 失败: %v", err)
|
||||
}
|
||||
t.Logf("统计概览: 总店铺数=%d, 涉及历史提号总单量=%d, 标准店=%d, 已映射=%d, 待映射=%d",
|
||||
stats.TotalShops, stats.TotalPickups, stats.StandardCount, stats.MappedCount, stats.UnmappedCount)
|
||||
|
||||
if stats.TotalPickups != pickupCount {
|
||||
t.Fatalf("统计单量与数据库记录数不一致: 统计=%d, 表总数=%d", stats.TotalPickups, pickupCount)
|
||||
}
|
||||
if stats.TotalShops < 60 {
|
||||
t.Fatalf("店铺种类数与线上统计不符: %d (预期 >= 60)", stats.TotalShops)
|
||||
}
|
||||
|
||||
// 打印 Top 5 店铺
|
||||
for i := 0; i < 5 && i < len(stats.Items); i++ {
|
||||
t.Logf(" Top %d: 「%s」 单量=%d, 状态=%s", i+1, stats.Items[i].SourceName, stats.Items[i].Count, stats.Items[i].MappingStatus)
|
||||
}
|
||||
|
||||
// 2. 测试一键将「糖浆」设为标准店铺
|
||||
t.Log("=== 2. 测试一键收录标准店铺 「糖浆」 ===")
|
||||
// 先检查是否已有
|
||||
var existingTangjiang model.PickupShop
|
||||
if err := db.Where("name = ?", "糖浆").First(&existingTangjiang).Error; err == gorm.ErrRecordNotFound {
|
||||
_, err := repo.CreateShop(ctx, CreatePickupShopRequest{
|
||||
Name: "糖浆",
|
||||
Platform: "拼多多",
|
||||
Status: PickupShopStatusActive,
|
||||
SortOrder: 100,
|
||||
Remark: "测试收录",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("收录「糖浆」失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新统计,验证「糖浆」状态变为 standard
|
||||
statsAfterTangjiang, err := repo.GetHistoricalShopStats(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("重新获取统计失败: %v", err)
|
||||
}
|
||||
var tangjiangItem *PickupShopHistoricalItemDTO
|
||||
for _, it := range statsAfterTangjiang.Items {
|
||||
if it.SourceName == "糖浆" {
|
||||
tangjiangItem = &it
|
||||
break
|
||||
}
|
||||
}
|
||||
if tangjiangItem == nil || tangjiangItem.MappingStatus != "standard" {
|
||||
t.Fatalf("「糖浆」状态未变为 standard: %+v", tangjiangItem)
|
||||
}
|
||||
t.Logf("✓ 「糖浆」成功识别为标准店铺 (历史单量: %d)", tangjiangItem.Count)
|
||||
|
||||
// 3. 测试批量映射与数据清洗
|
||||
t.Log("=== 3. 测试批量映射变体至标准店铺 「王大锤游戏」 ===")
|
||||
// 先确保目标标准店铺「王大锤游戏」存在
|
||||
var stdDachui model.PickupShop
|
||||
if err := db.Where("name = ?", "王大锤游戏").First(&stdDachui).Error; err == gorm.ErrRecordNotFound {
|
||||
createdDachui, err := repo.CreateShop(ctx, CreatePickupShopRequest{
|
||||
Name: "王大锤游戏",
|
||||
Platform: "淘宝",
|
||||
Status: PickupShopStatusActive,
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("创建「王大锤游戏」失败: %v", err)
|
||||
}
|
||||
stdDachui = model.PickupShop{ID: createdDachui.ID, Name: createdDachui.Name}
|
||||
}
|
||||
|
||||
// 待合并清洗变体:王大锤游戏专营 (36单), 淘宝王大锤商行 (3单), 王大锤商行 (1单)
|
||||
variants := []string{"王大锤游戏专营", "淘宝王大锤商行", "王大锤商行"}
|
||||
mapRes, err := repo.MapHistoricalShops(ctx, MapHistoricalShopsRequest{
|
||||
FromShopNames: variants,
|
||||
TargetShopName: "王大锤游戏",
|
||||
Remark: "自动化回归批量合并测试",
|
||||
}, 1, auditlog.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("MapHistoricalShops() 失败: %v", err)
|
||||
}
|
||||
t.Logf("✓ 批量映射成功: 清洗更新提号单数 = %d, 目标店铺 = %s (ID=%d)",
|
||||
mapRes.UpdatedPickupsCount, mapRes.TargetShopName, mapRes.TargetShopID)
|
||||
|
||||
if mapRes.UpdatedPickupsCount != 40 {
|
||||
t.Fatalf("预期清洗 40 单 (36+3+1), 实际清洗 = %d", mapRes.UpdatedPickupsCount)
|
||||
}
|
||||
|
||||
// 4. 验证数据库中这 40 单的 shop_name 和 raw_shop_name
|
||||
t.Log("=== 4. 验证提号单持久化字段状态 ===")
|
||||
var checkRows []struct {
|
||||
ID uint64
|
||||
ShopName string
|
||||
RawShopName string
|
||||
}
|
||||
if err := db.Model(&model.AdminPickup{}).
|
||||
Where("raw_shop_name IN ?", variants).
|
||||
Scan(&checkRows).Error; err != nil {
|
||||
t.Fatalf("查询已清洗记录失败: %v", err)
|
||||
}
|
||||
if len(checkRows) != 40 {
|
||||
t.Fatalf("查询到已清洗记录数 = %d, want 40", len(checkRows))
|
||||
}
|
||||
for _, r := range checkRows {
|
||||
if r.ShopName != "王大锤游戏" {
|
||||
t.Fatalf("ID=%d shop_name 未更新为目标: %q", r.ID, r.ShopName)
|
||||
}
|
||||
if r.RawShopName == "" || r.RawShopName == "王大锤游戏" {
|
||||
t.Fatalf("ID=%d raw_shop_name 丢失了原始字样: %q", r.ID, r.RawShopName)
|
||||
}
|
||||
}
|
||||
t.Log("✓ 全部 40 单提号 shop_name 已清洗为「王大锤游戏」,且 raw_shop_name 完美保留原始变体字样!")
|
||||
|
||||
// 5. 验证后台列表查询与店铺聚合过滤
|
||||
t.Log("=== 5. 验证后台提号列表筛选 ===")
|
||||
listRes, err := repo.ListAdmin(ctx, AdminPickupQuery{
|
||||
ShopName: "王大锤游戏",
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("按「王大锤游戏」筛选列表失败: %v", err)
|
||||
}
|
||||
t.Logf("✓ 按「王大锤游戏」筛选总单量: %d", listRes.Total)
|
||||
if listRes.Total < 880 {
|
||||
t.Fatalf("合并后「王大锤游戏」总单量预期 >= 880, 实际: %d", listRes.Total)
|
||||
}
|
||||
|
||||
// 验证时间范围筛选
|
||||
startTime, _ := time.Parse("2006-01-02", "2026-08-01")
|
||||
endTime, _ := time.Parse("2006-01-02", "2026-08-31")
|
||||
timeRes, err := repo.ListAdmin(ctx, AdminPickupQuery{
|
||||
StartTime: &startTime,
|
||||
EndTime: &endTime,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("时间范围筛选失败: %v", err)
|
||||
}
|
||||
t.Logf("✓ 2026年8月提号单量: %d", timeRes.Total)
|
||||
if timeRes.Total == 0 {
|
||||
t.Fatal("2026年8月提号单量不应为 0")
|
||||
}
|
||||
|
||||
// 6. 测试撤销映射并还原数据
|
||||
t.Log("=== 6. 测试撤销映射规则并还原提号单 ===")
|
||||
if err := repo.DeleteShopMapping(ctx, "王大锤商行", 1, auditlog.Meta{}); err != nil {
|
||||
t.Fatalf("撤销「王大锤商行」映射失败: %v", err)
|
||||
}
|
||||
|
||||
var restoredRow model.AdminPickup
|
||||
if err := db.Where("raw_shop_name = ?", "王大锤商行").First(&restoredRow).Error; err != nil {
|
||||
t.Fatalf("查询还原记录失败: %v", err)
|
||||
}
|
||||
if restoredRow.ShopName != "王大锤商行" {
|
||||
t.Fatalf("撤销后 shop_name 未还原为原始值: 实际 %q, 期望 %q", restoredRow.ShopName, "王大锤商行")
|
||||
}
|
||||
t.Logf("✓ 「王大锤商行」映射已成功撤销,原提号单 shop_name 已安全还原为「王大锤商行」!")
|
||||
|
||||
t.Log("=== 全部克隆数据回归测试用例验证通过! ===")
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// 提号状态
|
||||
const (
|
||||
StatusPickingUp = "picking_up"
|
||||
StatusCompleted = "completed"
|
||||
StatusCancelled = "cancelled"
|
||||
|
||||
AccountSourceInternal = "internal"
|
||||
AccountSourceExternalPlatformManaged = "external_platform_managed"
|
||||
ListingSourceExternal = "external"
|
||||
ListingSourceInternal = "internal"
|
||||
SettlementModeOwnerWallet = "owner_wallet"
|
||||
SettlementModePlatformManaged = "platform_managed"
|
||||
OfflineSettlementStatusNone = "none"
|
||||
OfflineSettlementStatusPending = "pending"
|
||||
OfflineSettlementStatusSettled = "settled"
|
||||
FinancialAdjustmentStatusSettled = "settled"
|
||||
FinancialAdjustmentStatusPending = "pending"
|
||||
FinancialAdjustmentStatusRecovery = "recovery_pending"
|
||||
|
||||
PickupShopStatusActive = "active"
|
||||
PickupShopStatusDisabled = "disabled"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrListingUnavailable = errors.New("listing unavailable")
|
||||
ErrPickupNotFound = errors.New("pickup not found")
|
||||
ErrPickupNotPickingUp = errors.New("pickup not in picking_up status")
|
||||
ErrInvalidAmount = errors.New("invalid amount")
|
||||
ErrInvalidProfit = errors.New("invalid profit amount")
|
||||
ErrDuplicatePickup = errors.New("duplicate pickup in progress")
|
||||
ErrOfflineSettlementCannotMark = errors.New("pickup offline settlement cannot be marked")
|
||||
ErrFinancialAdjustmentInvalid = errors.New("financial adjustment invalid")
|
||||
ErrFinancialAdjustmentNotFound = errors.New("financial adjustment not found")
|
||||
ErrFinancialAdjustmentCannotSettle = errors.New("financial adjustment cannot be settled")
|
||||
ErrPickupShopNotFound = errors.New("pickup shop not found")
|
||||
ErrPickupShopNameExists = errors.New("pickup shop name already exists")
|
||||
ErrPickupCancelled = errors.New("pickup is cancelled")
|
||||
ErrInvalidThirdPartyOrderNo = errors.New("invalid third party order no")
|
||||
)
|
||||
|
||||
type PickupDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
PickupNo string `json:"pickup_no"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
AccountTitle string `json:"account_title"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerPhone string `json:"owner_phone"`
|
||||
AdminID uint64 `json:"admin_id"`
|
||||
Operator string `json:"operator"`
|
||||
Platform string `json:"platform"`
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no"`
|
||||
ShopName string `json:"shop_name"`
|
||||
RawShopName string `json:"raw_shop_name"`
|
||||
AccountSource string `json:"account_source"`
|
||||
SourceChannel string `json:"source_channel"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
ListingPriceCent int64 `json:"listing_price_cent"`
|
||||
OrderAmountCent int64 `json:"order_amount_cent"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
OwnerPriceCent int64 `json:"owner_price_cent"`
|
||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
||||
ProfitAdjustmentCent int64 `json:"profit_adjustment_cent"`
|
||||
EffectiveProfitAmountCent int64 `json:"effective_profit_amount_cent"`
|
||||
SellerRatio float64 `json:"seller_ratio"`
|
||||
BuyerRatio float64 `json:"buyer_ratio"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot,omitempty"`
|
||||
SettleAmountCent int64 `json:"settle_amount_cent"`
|
||||
SettleAdjustmentCent int64 `json:"settle_adjustment_cent"`
|
||||
EffectiveSettleAmountCent int64 `json:"effective_settle_amount_cent"`
|
||||
Status string `json:"status"`
|
||||
OfflineSettlementStatus string `json:"offline_settlement_status"`
|
||||
OfflineSettlementRemark string `json:"offline_settlement_remark"`
|
||||
OfflineSettledBy *uint64 `json:"offline_settled_by,omitempty"`
|
||||
OfflineSettledAt *time.Time `json:"offline_settled_at,omitempty"`
|
||||
Remark string `json:"remark"`
|
||||
CompleteRemark string `json:"complete_remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CancelledAt *time.Time `json:"cancelled_at,omitempty"`
|
||||
}
|
||||
|
||||
type FinancialAdjustmentDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
PickupID uint64 `json:"pickup_id"`
|
||||
ProfitDeltaCent int64 `json:"profit_delta_cent"`
|
||||
SettleDeltaCent int64 `json:"settle_delta_cent"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
CreatedBy uint64 `json:"created_by"`
|
||||
SettledBy *uint64 `json:"settled_by,omitempty"`
|
||||
SettledAt *time.Time `json:"settled_at,omitempty"`
|
||||
SettlementRemark string `json:"settlement_remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
ListingID uint64 `json:"listing_id" binding:"required"`
|
||||
Operator string `json:"operator" binding:"max=64"`
|
||||
Platform string `json:"platform"`
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no" binding:"max=128"`
|
||||
ShopName string `json:"shop_name" binding:"max=100"`
|
||||
OrderAmountCent int64 `json:"order_amount_cent" binding:"min=0"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent" binding:"min=0"`
|
||||
ProfitAmountCent int64 `json:"profit_amount_cent"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type CompleteRequest struct {
|
||||
SettleAmountCent int64 `json:"settle_amount_cent" binding:"required,min=1"`
|
||||
ProfitAmountCent *int64 `json:"profit_amount_cent"`
|
||||
CompleteRemark string `json:"complete_remark"`
|
||||
}
|
||||
|
||||
type OfflineSettlementRequest struct {
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type UpdateProfitRequest struct {
|
||||
ProfitAmountCent int64 `json:"profit_amount_cent" binding:"min=0"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UpdateThirdPartyOrderNoRequest struct {
|
||||
ThirdPartyOrderNo string `json:"third_party_order_no" binding:"required,max=128"`
|
||||
}
|
||||
|
||||
// FinancialAdjustmentRequest 按目标金额创建完成后的差额流水,至少提供一个金额。
|
||||
type FinancialAdjustmentRequest struct {
|
||||
ProfitAmountCent *int64 `json:"profit_amount_cent"`
|
||||
SettleAmountCent *int64 `json:"settle_amount_cent"`
|
||||
Reason string `json:"reason" binding:"required,max=255"`
|
||||
}
|
||||
|
||||
type FinancialAdjustmentSettlementRequest struct {
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type CancelRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
type AdminPickupQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
Status string
|
||||
ShopName string
|
||||
AccountSource string
|
||||
OfflineSettlementStatus string
|
||||
Operator string
|
||||
ThirdPartyOrderNo string
|
||||
StartTime *time.Time
|
||||
EndTime *time.Time
|
||||
}
|
||||
|
||||
type PickupShopDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreatePickupShopRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Platform string `json:"platform" binding:"max=32"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active disabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type UpdatePickupShopRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Platform string `json:"platform" binding:"max=32"`
|
||||
Status string `json:"status" binding:"required,oneof=active disabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type PickupShopQuery struct {
|
||||
Keyword string
|
||||
Platform string
|
||||
Status string
|
||||
}
|
||||
|
||||
type PickupShopHistoricalItemDTO struct {
|
||||
SourceName string `json:"source_name"`
|
||||
Count int64 `json:"count"`
|
||||
MappingStatus string `json:"mapping_status"` // standard, mapped, unmapped
|
||||
TargetShopID uint64 `json:"target_shop_id,omitempty"`
|
||||
TargetShopName string `json:"target_shop_name,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
}
|
||||
|
||||
type PickupShopHistoricalStatsDTO struct {
|
||||
Items []PickupShopHistoricalItemDTO `json:"items"`
|
||||
TotalPickups int64 `json:"total_pickups"`
|
||||
TotalShops int `json:"total_shops"`
|
||||
StandardCount int `json:"standard_count"`
|
||||
MappedCount int `json:"mapped_count"`
|
||||
UnmappedCount int `json:"unmapped_count"`
|
||||
}
|
||||
|
||||
type MapHistoricalShopsRequest struct {
|
||||
FromShopNames []string `json:"from_shop_names" binding:"required,min=1"`
|
||||
TargetShopName string `json:"target_shop_name" binding:"required,max=100"`
|
||||
CreateTargetMissing bool `json:"create_target_missing"`
|
||||
TargetPlatform string `json:"target_platform" binding:"max=32"`
|
||||
Remark string `json:"remark" binding:"max=255"`
|
||||
}
|
||||
|
||||
type MapHistoricalShopsResultDTO struct {
|
||||
UpdatedPickupsCount int64 `json:"updated_pickups_count"`
|
||||
TargetShopName string `json:"target_shop_name"`
|
||||
TargetShopID uint64 `json:"target_shop_id"`
|
||||
CreatedStandardShop bool `json:"created_standard_shop"`
|
||||
}
|
||||
|
||||
type SellerPickupQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Status string
|
||||
}
|
||||
|
||||
type AvailableListingDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
AccountTitle string `json:"account_title"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
OwnerPhone string `json:"owner_phone"`
|
||||
ListingPriceCent int64 `json:"listing_price_cent"`
|
||||
OwnerPriceCent int64 `json:"owner_price_cent"`
|
||||
OwnerPurePriceCent int64 `json:"owner_pure_price_cent"`
|
||||
OwnerExtraItemPriceCent int64 `json:"owner_extra_item_price_cent"`
|
||||
OwnerTotalPriceCent int64 `json:"owner_total_price_cent"`
|
||||
WebsiteProfitCent int64 `json:"website_profit_cent"`
|
||||
SellerRatio float64 `json:"seller_ratio"`
|
||||
BuyerRatio float64 `json:"buyer_ratio"`
|
||||
AccountSource string `json:"account_source"`
|
||||
IsExternalUpload bool `json:"is_external_upload"`
|
||||
SourceChannel string `json:"source_channel"`
|
||||
HandoffMode string `json:"handoff_mode"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
}
|
||||
|
||||
type AvailableListingQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
SourceType string
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items []PickupDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type AvailableListingsResult struct {
|
||||
Items []AvailableListingDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
// Create 创建提号订单(管理员)
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
var req CreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数格式错误")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Create(c.Request.Context(), req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// Complete 完成提号(管理员)
|
||||
func (h *Handler) Complete(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CompleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "结算金额不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Complete(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// MarkOfflineSettlement 确认平台代管提号已完成实际线下打款(管理员)。
|
||||
func (h *Handler) MarkOfflineSettlement(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req OfflineSettlementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "线下结算备注格式不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.MarkOfflineSettlement(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateProfit 修改提号中的线下利润(管理员)
|
||||
func (h *Handler) UpdateProfit(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdateProfitRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "利润金额不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateProfit(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateThirdPartyOrderNo 补录或修改第三方单号(管理员)
|
||||
func (h *Handler) UpdateThirdPartyOrderNo(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdateThirdPartyOrderNoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "第三方单号不能为空且长度不能超过128位")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateThirdPartyOrderNo(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// CreateFinancialAdjustment 为已完成提号创建利润或打款调整。
|
||||
func (h *Handler) CreateFinancialAdjustment(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req FinancialAdjustmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "调整金额或原因不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateFinancialAdjustment(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// ListFinancialAdjustments 返回提号完成后的财务调整历史。
|
||||
func (h *Handler) ListFinancialAdjustments(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListFinancialAdjustments(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
func (h *Handler) ProcessEvents(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.ProcessEvents(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// SettleFinancialAdjustment 确认代管提号的补款或追回调整已线下处理。
|
||||
func (h *Handler) SettleFinancialAdjustment(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req FinancialAdjustmentSettlementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "确认备注格式不正确")
|
||||
return
|
||||
}
|
||||
if err := h.service.SettleFinancialAdjustment(c.Request.Context(), id, req, adminID, auditMeta(c)); err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"settled": true})
|
||||
}
|
||||
|
||||
// Cancel 取消提号(管理员)
|
||||
func (h *Handler) Cancel(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CancelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "取消原因不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.Cancel(c.Request.Context(), id, req.Reason, adminID, auditMeta(c)); err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"cancelled": true})
|
||||
}
|
||||
|
||||
// List 提号订单列表(管理员)
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
query := parseAdminQuery(c)
|
||||
result, err := h.service.ListAdmin(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// ShopOptions 返回后台提号已使用的店铺名称。
|
||||
func (h *Handler) ShopOptions(c *gin.Context) {
|
||||
items, err := h.service.ListShopNames(c.Request.Context(), c.Query("platform"))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
// Detail 提号订单详情(管理员)
|
||||
func (h *Handler) Detail(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := h.service.FindByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// AvailableListings 可提号的 listing 搜索(管理员)
|
||||
func (h *Handler) AvailableListings(c *gin.Context) {
|
||||
page, pageSize := parsePagination(c)
|
||||
result, err := h.service.ListAvailableListings(c.Request.Context(), AvailableListingQuery{
|
||||
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||
SourceType: strings.TrimSpace(c.Query("source_type")),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// ListForSeller 卖家自己的提号记录
|
||||
func (h *Handler) ListForSeller(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
query := parseSellerQuery(c)
|
||||
result, err := h.service.ListForSeller(c.Request.Context(), userID, query)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func writePickupError(c *gin.Context, err error) {
|
||||
response.RecordError(c, err)
|
||||
switch {
|
||||
case errors.Is(err, ErrDependencyUnavailable):
|
||||
response.ServiceUnavailable(c, "数据库未连接")
|
||||
case errors.Is(err, ErrListingUnavailable):
|
||||
response.BadRequest(c, "账号不可用或已在交易中")
|
||||
case errors.Is(err, ErrDuplicatePickup):
|
||||
response.BadRequest(c, "该账号已有进行中的提号订单")
|
||||
case errors.Is(err, ErrPickupNotFound):
|
||||
response.Error(c, http.StatusNotFound, "pickup_not_found", "提号订单不存在")
|
||||
case errors.Is(err, ErrPickupNotPickingUp):
|
||||
response.BadRequest(c, "提号订单状态不允许该操作")
|
||||
case errors.Is(err, ErrInvalidAmount):
|
||||
response.BadRequest(c, "结算金额不正确")
|
||||
case errors.Is(err, ErrInvalidProfit):
|
||||
response.BadRequest(c, "利润金额不正确")
|
||||
case errors.Is(err, ErrOfflineSettlementCannotMark):
|
||||
response.BadRequest(c, "当前提号不可确认线下结算")
|
||||
case errors.Is(err, ErrFinancialAdjustmentInvalid):
|
||||
response.BadRequest(c, "调整金额、状态或原因不正确")
|
||||
case errors.Is(err, ErrFinancialAdjustmentNotFound):
|
||||
response.Error(c, http.StatusNotFound, "pickup_financial_adjustment_not_found", "财务调整记录不存在")
|
||||
case errors.Is(err, ErrFinancialAdjustmentCannotSettle):
|
||||
response.BadRequest(c, "当前调整不可确认处理")
|
||||
case errors.Is(err, ErrPickupShopNotFound):
|
||||
response.Error(c, http.StatusNotFound, "pickup_shop_not_found", "店铺不存在")
|
||||
case errors.Is(err, ErrPickupShopNameExists):
|
||||
response.Error(c, http.StatusConflict, "pickup_shop_name_exists", "店铺名称已存在")
|
||||
case errors.Is(err, ErrPickupCancelled):
|
||||
response.BadRequest(c, "提号订单已取消,不能补录单号")
|
||||
case errors.Is(err, ErrInvalidThirdPartyOrderNo):
|
||||
response.BadRequest(c, "第三方单号不能为空且长度不能超过128位")
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ListShops 获取店铺配置列表(管理员)
|
||||
func (h *Handler) ListShops(c *gin.Context) {
|
||||
query := parsePickupShopQuery(c)
|
||||
items, err := h.service.ListShops(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
// CreateShop 创建店铺配置(管理员)
|
||||
func (h *Handler) CreateShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
var req CreatePickupShopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "店铺参数格式不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateShop(c.Request.Context(), req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// UpdateShop 更新店铺配置(管理员)
|
||||
func (h *Handler) UpdateShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpdatePickupShopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "店铺参数格式不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateShop(c.Request.Context(), id, req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
// DeleteShop 删除店铺配置(管理员)
|
||||
func (h *Handler) DeleteShop(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteShop(c.Request.Context(), id, adminID, auditMeta(c)); err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
// GetHistoricalShopStats 获取历史店铺聚合统计及映射情况(管理员)
|
||||
func (h *Handler) GetHistoricalShopStats(c *gin.Context) {
|
||||
stats, err := h.service.GetHistoricalShopStats(c.Request.Context())
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, stats)
|
||||
}
|
||||
|
||||
// MapHistoricalShops 批量映射历史店铺并清洗提号单(管理员)
|
||||
func (h *Handler) MapHistoricalShops(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
var req MapHistoricalShopsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "映射参数格式不正确")
|
||||
return
|
||||
}
|
||||
res, err := h.service.MapHistoricalShops(c.Request.Context(), req, adminID, auditMeta(c))
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, res)
|
||||
}
|
||||
|
||||
// DeleteShopMapping 删除历史店铺映射规则并还原提号单(管理员)
|
||||
func (h *Handler) DeleteShopMapping(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
sourceName := c.Query("source_name")
|
||||
if sourceName == "" {
|
||||
response.BadRequest(c, "缺少 source_name 参数")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteShopMapping(c.Request.Context(), sourceName, adminID, auditMeta(c)); err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func auditMeta(c *gin.Context) auditlog.Meta {
|
||||
return auditlog.Meta{
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
RequestID: middleware.GetRequestID(c),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func currentAdminID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextAdminID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
adminID, ok := value.(uint64)
|
||||
return adminID, ok
|
||||
}
|
||||
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextUserID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
userID, ok := value.(uint64)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
response.BadRequest(c, "ID 不正确")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func parsePagination(c *gin.Context) (int, int) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func parseFlexibleTime(val string) *time.Time {
|
||||
val = strings.TrimSpace(val)
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02",
|
||||
}
|
||||
loc := time.Local
|
||||
for _, layout := range layouts {
|
||||
if t, err := time.ParseInLocation(layout, val, loc); err == nil {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAdminQuery(c *gin.Context) AdminPickupQuery {
|
||||
page, pageSize := parsePagination(c)
|
||||
return AdminPickupQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Keyword: c.Query("keyword"),
|
||||
Status: c.Query("status"),
|
||||
ShopName: c.Query("shop_name"),
|
||||
AccountSource: c.Query("account_source"),
|
||||
OfflineSettlementStatus: c.Query("offline_settlement_status"),
|
||||
Operator: c.Query("operator"),
|
||||
ThirdPartyOrderNo: c.Query("third_party_order_no"),
|
||||
StartTime: parseFlexibleTime(c.Query("start_time")),
|
||||
EndTime: parseFlexibleTime(c.Query("end_time")),
|
||||
}
|
||||
}
|
||||
|
||||
func parsePickupShopQuery(c *gin.Context) PickupShopQuery {
|
||||
return PickupShopQuery{
|
||||
Keyword: c.Query("keyword"),
|
||||
Platform: c.Query("platform"),
|
||||
Status: c.Query("status"),
|
||||
}
|
||||
}
|
||||
|
||||
func parseSellerQuery(c *gin.Context) SellerPickupQuery {
|
||||
page, pageSize := parsePagination(c)
|
||||
return SellerPickupQuery{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Status: c.Query("status"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const processBusinessPickup = "pickup"
|
||||
|
||||
type ProcessEventDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Stage string `json:"stage"`
|
||||
Action string `json:"action"`
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID uint64 `json:"actor_id"`
|
||||
ActorName string `json:"actor_name"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID *uint64 `json:"target_id,omitempty"`
|
||||
TargetName string `json:"target_name"`
|
||||
Content string `json:"content"`
|
||||
Reason string `json:"reason"`
|
||||
Payload datatypes.JSON `json:"payload"`
|
||||
AttachmentURLs []string `json:"attachment_urls"`
|
||||
StateBefore datatypes.JSON `json:"state_before"`
|
||||
StateAfter datatypes.JSON `json:"state_after"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func pickupState(row model.AdminPickup) map[string]any {
|
||||
return map[string]any{
|
||||
"pickup_status": row.Status, "offline_settlement_status": row.OfflineSettlementStatus,
|
||||
"settlement_mode": row.SettlementMode,
|
||||
}
|
||||
}
|
||||
|
||||
func appendPickupEvent(tx *gorm.DB, pickup model.AdminPickup, action string, adminID uint64, content, reason string, payload, before map[string]any) error {
|
||||
return processlog.Append(tx, processlog.Entry{
|
||||
BusinessType: processBusinessPickup, BusinessID: pickup.ID, Stage: "pickup", Action: action,
|
||||
ActorType: processlog.ActorAdmin, ActorID: adminID, TargetType: processlog.ActorUser,
|
||||
TargetID: &pickup.OwnerID, Content: content, Reason: reason, Payload: payload,
|
||||
StateBefore: before, StateAfter: pickupState(pickup),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ProcessEvents(ctx context.Context, pickupID uint64) ([]ProcessEventDTO, error) {
|
||||
var pickup model.AdminPickup
|
||||
if err := r.db.WithContext(ctx).First(&pickup, pickupID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := processlog.List(r.db.WithContext(ctx), processBusinessPickup, pickupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ProcessEventDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, ProcessEventDTO{
|
||||
ID: row.ID, Stage: row.Stage, Action: row.Action, ActorType: row.ActorType, ActorID: row.ActorID,
|
||||
ActorName: row.ActorName, TargetType: row.TargetType, TargetID: row.TargetID, TargetName: row.TargetName,
|
||||
Content: row.Content, Reason: row.Reason, Payload: row.Payload, AttachmentURLs: decodeStringList(row.AttachmentURLs),
|
||||
StateBefore: row.StateBefore, StateAfter: row.StateAfter, CreatedAt: row.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func decodeStringList(raw datatypes.JSON) []string {
|
||||
items := make([]string, 0)
|
||||
_ = json.Unmarshal(raw, &items)
|
||||
return items
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req CreateRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if req.ListingID == 0 {
|
||||
return nil, ErrListingUnavailable
|
||||
}
|
||||
if req.ProfitAmountCent < 0 {
|
||||
return nil, ErrInvalidProfit
|
||||
}
|
||||
return s.repo.Create(ctx, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) Complete(ctx context.Context, pickupID uint64, req CompleteRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 || req.SettleAmountCent <= 0 {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
if req.ProfitAmountCent != nil && *req.ProfitAmountCent < 0 {
|
||||
return nil, ErrInvalidProfit
|
||||
}
|
||||
return s.repo.Complete(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) MarkOfflineSettlement(ctx context.Context, pickupID uint64, req OfflineSettlementRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
return s.repo.MarkOfflineSettlement(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateProfit(ctx context.Context, pickupID uint64, req UpdateProfitRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
if req.ProfitAmountCent < 0 {
|
||||
return nil, ErrInvalidProfit
|
||||
}
|
||||
return s.repo.UpdateProfit(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateThirdPartyOrderNo(ctx context.Context, pickupID uint64, req UpdateThirdPartyOrderNoRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
if strings.TrimSpace(req.ThirdPartyOrderNo) == "" {
|
||||
return nil, ErrInvalidThirdPartyOrderNo
|
||||
}
|
||||
return s.repo.UpdateThirdPartyOrderNo(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) CreateFinancialAdjustment(ctx context.Context, pickupID uint64, req FinancialAdjustmentRequest, adminID uint64, meta auditlog.Meta) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 || (req.ProfitAmountCent == nil && req.SettleAmountCent == nil) || strings.TrimSpace(req.Reason) == "" {
|
||||
return nil, ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
if (req.ProfitAmountCent != nil && *req.ProfitAmountCent < 0) || (req.SettleAmountCent != nil && *req.SettleAmountCent < 0) {
|
||||
return nil, ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
return s.repo.CreateFinancialAdjustment(ctx, pickupID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) ListFinancialAdjustments(ctx context.Context, pickupID uint64) ([]FinancialAdjustmentDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 {
|
||||
return nil, ErrPickupNotFound
|
||||
}
|
||||
return s.repo.ListFinancialAdjustments(ctx, pickupID)
|
||||
}
|
||||
|
||||
func (s *Service) ProcessEvents(ctx context.Context, pickupID uint64) ([]ProcessEventDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ProcessEvents(ctx, pickupID)
|
||||
}
|
||||
|
||||
func (s *Service) SettleFinancialAdjustment(ctx context.Context, adjustmentID uint64, req FinancialAdjustmentSettlementRequest, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if adjustmentID == 0 {
|
||||
return ErrFinancialAdjustmentNotFound
|
||||
}
|
||||
return s.repo.SettleFinancialAdjustment(ctx, adjustmentID, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(ctx context.Context, pickupID uint64, reason string, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if pickupID == 0 || reason == "" {
|
||||
return ErrPickupNotPickingUp
|
||||
}
|
||||
return s.repo.Cancel(ctx, pickupID, reason, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) FindByID(ctx context.Context, id uint64) (*PickupDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) ListAdmin(ctx context.Context, query AdminPickupQuery) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListAdmin(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListShopNames(ctx context.Context, platform string) ([]string, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListShopNames(ctx, platform)
|
||||
}
|
||||
|
||||
func (s *Service) ListForSeller(ctx context.Context, ownerID uint64, query SellerPickupQuery) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListForSeller(ctx, ownerID, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListAvailableListings(ctx context.Context, query AvailableListingQuery) (*AvailableListingsResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListAvailableListings(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListShops(ctx context.Context, query PickupShopQuery) ([]PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListShops(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) CreateShop(ctx context.Context, req CreatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.CreateShop(ctx, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateShop(ctx context.Context, id uint64, req UpdatePickupShopRequest, adminID uint64, meta auditlog.Meta) (*PickupShopDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.UpdateShop(ctx, id, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteShop(ctx context.Context, id uint64, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.DeleteShop(ctx, id, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) GetHistoricalShopStats(ctx context.Context) (*PickupShopHistoricalStatsDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.GetHistoricalShopStats(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) MapHistoricalShops(ctx context.Context, req MapHistoricalShopsRequest, adminID uint64, meta auditlog.Meta) (*MapHistoricalShopsResultDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.MapHistoricalShops(ctx, req, adminID, meta)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteShopMapping(ctx context.Context, sourceName string, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.DeleteShopMapping(ctx, sourceName, adminID, meta)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user