优化线下结算筛选

This commit is contained in:
yml2213
2026-08-16 20:00:09 +08:00
parent c06e5aaafd
commit 58f11b2043
11 changed files with 212 additions and 59 deletions
+8 -7
View File
@@ -150,13 +150,14 @@ type OfflineSettlementRequest struct {
} }
type AdminOrderQuery struct { type AdminOrderQuery struct {
Page int Page int
PageSize int PageSize int
Keyword string Keyword string
Status string Status string
HandoffStatus string HandoffStatus string
HandoffMode string HandoffMode string
SettlementStatus string SettlementStatus string
OfflineSettlementStatus string
} }
type AuditMeta = auditlog.Meta type AuditMeta = auditlog.Meta
@@ -55,12 +55,13 @@ func parsePagination(c *gin.Context) (int, int) {
func parseAdminOrderQuery(c *gin.Context) AdminOrderQuery { func parseAdminOrderQuery(c *gin.Context) AdminOrderQuery {
page, pageSize := parsePagination(c) page, pageSize := parsePagination(c)
return AdminOrderQuery{ return AdminOrderQuery{
Page: page, Page: page,
PageSize: pageSize, PageSize: pageSize,
Keyword: strings.TrimSpace(c.Query("keyword")), Keyword: strings.TrimSpace(c.Query("keyword")),
Status: strings.TrimSpace(c.Query("status")), Status: strings.TrimSpace(c.Query("status")),
HandoffStatus: strings.TrimSpace(c.Query("handoff_status")), HandoffStatus: strings.TrimSpace(c.Query("handoff_status")),
HandoffMode: strings.TrimSpace(c.Query("handoff_mode")), HandoffMode: strings.TrimSpace(c.Query("handoff_mode")),
SettlementStatus: strings.TrimSpace(c.Query("settlement_status")), SettlementStatus: strings.TrimSpace(c.Query("settlement_status")),
OfflineSettlementStatus: strings.TrimSpace(c.Query("offline_settlement_status")),
} }
} }
@@ -93,6 +93,9 @@ func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
if query.SettlementStatus != "" { if query.SettlementStatus != "" {
db = db.Where("o.settlement_status = ?", query.SettlementStatus) db = db.Where("o.settlement_status = ?", query.SettlementStatus)
} }
if query.OfflineSettlementStatus != "" {
db = db.Where("COALESCE(NULLIF(o.offline_settlement_status, ''), 'none') = ?", query.OfflineSettlementStatus)
}
if keyword := strings.TrimSpace(query.Keyword); keyword != "" { if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%" like := "%" + keyword + "%"
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil { if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
+7 -6
View File
@@ -136,12 +136,13 @@ type CancelRequest struct {
} }
type AdminPickupQuery struct { type AdminPickupQuery struct {
Page int Page int
PageSize int PageSize int
Keyword string Keyword string
Status string Status string
ShopName string ShopName string
AccountSource string AccountSource string
OfflineSettlementStatus string
} }
type SellerPickupQuery struct { type SellerPickupQuery struct {
@@ -54,12 +54,13 @@ func parsePagination(c *gin.Context) (int, int) {
func parseAdminQuery(c *gin.Context) AdminPickupQuery { func parseAdminQuery(c *gin.Context) AdminPickupQuery {
page, pageSize := parsePagination(c) page, pageSize := parsePagination(c)
return AdminPickupQuery{ return AdminPickupQuery{
Page: page, Page: page,
PageSize: pageSize, PageSize: pageSize,
Keyword: c.Query("keyword"), Keyword: c.Query("keyword"),
Status: c.Query("status"), Status: c.Query("status"),
ShopName: c.Query("shop_name"), ShopName: c.Query("shop_name"),
AccountSource: c.Query("account_source"), AccountSource: c.Query("account_source"),
OfflineSettlementStatus: c.Query("offline_settlement_status"),
} }
} }
@@ -687,6 +687,9 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminPickupQuery) (*Pa
if accountSource := strings.TrimSpace(query.AccountSource); accountSource != "" { if accountSource := strings.TrimSpace(query.AccountSource); accountSource != "" {
db = db.Where("p.account_source = ?", accountSource) db = db.Where("p.account_source = ?", accountSource)
} }
if offlineSettlementStatus := strings.TrimSpace(query.OfflineSettlementStatus); offlineSettlementStatus != "" {
db = db.Where("p.offline_settlement_status = ?", offlineSettlementStatus)
}
if kw := strings.TrimSpace(query.Keyword); kw != "" { if kw := strings.TrimSpace(query.Keyword); kw != "" {
like := "%" + kw + "%" like := "%" + kw + "%"
db = db.Where( db = db.Where(
@@ -175,6 +175,32 @@ func TestRepositoryListAdminFiltersOwnerPhone(t *testing.T) {
} }
} }
func TestRepositoryListAdminFiltersOfflineSettlementStatus(t *testing.T) {
repo, db := newPickupTestRepo(t)
first := seedPickup(t, db, StatusCompleted)
if err := db.Model(&first).Updates(map[string]any{
"settlement_mode": SettlementModePlatformManaged,
"offline_settlement_status": OfflineSettlementStatusPending,
}).Error; err != nil {
t.Fatalf("设置待线下结算提号失败: %v", err)
}
second := first
second.ID = 0
second.PickupNo = "PK-OFFLINE-SETTLED-001"
second.OfflineSettlementStatus = OfflineSettlementStatusSettled
if err := db.Create(&second).Error; err != nil {
t.Fatalf("创建已线下结算提号失败: %v", err)
}
result, err := repo.ListAdmin(t.Context(), AdminPickupQuery{OfflineSettlementStatus: OfflineSettlementStatusPending})
if err != nil {
t.Fatalf("ListAdmin() error = %v", err)
}
if result.Total != 1 || len(result.Items) != 1 || result.Items[0].OfflineSettlementStatus != OfflineSettlementStatusPending {
t.Fatalf("待线下结算筛选结果 = %+v", result)
}
}
func TestRepositoryListShopNamesReturnsDistinctPlatformOptions(t *testing.T) { func TestRepositoryListShopNamesReturnsDistinctPlatformOptions(t *testing.T) {
repo, db := newPickupTestRepo(t) repo, db := newPickupTestRepo(t)
first := seedPickup(t, db, StatusPickingUp) first := seedPickup(t, db, StatusPickingUp)
@@ -127,6 +127,7 @@ export interface AdminPickupListQuery {
status?: string status?: string
shop_name?: string shop_name?: string
account_source?: PickupAccountSource | '' account_source?: PickupAccountSource | ''
offline_settlement_status?: PickupOfflineSettlementStatus | ''
} }
function cleanParams(query: object) { function cleanParams(query: object) {
@@ -21,6 +21,7 @@ const filters = reactive<AdminOrderQuery>({
handoff_status: '', handoff_status: '',
handoff_mode: '', handoff_mode: '',
settlement_status: '', settlement_status: '',
offline_settlement_status: '',
}) })
const loading = ref(false) const loading = ref(false)
const orders = ref<Order[]>([]) const orders = ref<Order[]>([])
@@ -92,6 +93,7 @@ async function loadOrders() {
handoff_status: filters.handoff_status, handoff_status: filters.handoff_status,
handoff_mode: filters.handoff_mode, handoff_mode: filters.handoff_mode,
settlement_status: filters.settlement_status, settlement_status: filters.settlement_status,
offline_settlement_status: filters.offline_settlement_status,
}) })
orders.value = result.items orders.value = result.items
total.value = result.total total.value = result.total
@@ -115,6 +117,7 @@ function resetFilters() {
filters.handoff_status = '' filters.handoff_status = ''
filters.handoff_mode = '' filters.handoff_mode = ''
filters.settlement_status = '' filters.settlement_status = ''
filters.offline_settlement_status = ''
void queryOrders() void queryOrders()
} }
@@ -225,6 +228,18 @@ function isPlatformManaged(row: Order) {
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="线下结算">
<el-select
v-model="filters.offline_settlement_status"
clearable
placeholder="全部"
style="width: 150px"
@change="queryOrders"
>
<el-option label="待确认线下结算" value="pending" />
<el-option label="已确认线下结算" value="settled" />
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :icon="Search" :loading="loading" @click="queryOrders" <el-button type="primary" :icon="Search" :loading="loading" @click="queryOrders"
>查询</el-button >查询</el-button
@@ -16,6 +16,7 @@ import {
type AvailableListing, type AvailableListing,
type AvailableListingSourceType, type AvailableListingSourceType,
type PickupAccountSource, type PickupAccountSource,
type PickupOfflineSettlementStatus,
} from '@/features/admin/api/adminPickup' } from '@/features/admin/api/adminPickup'
import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money' import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time' import { formatDateTime } from '@/shared/utils/time'
@@ -32,6 +33,7 @@ const filters = reactive({
status: '', status: '',
shop_name: '', shop_name: '',
account_source: '' as PickupAccountSource | '', account_source: '' as PickupAccountSource | '',
offline_settlement_status: '' as PickupOfflineSettlementStatus | '',
}) })
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
@@ -100,6 +102,16 @@ function handleSearch() {
filters.page = 1 filters.page = 1
loadList() loadList()
} }
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.shop_name = ''
filters.account_source = ''
filters.offline_settlement_status = ''
filters.page = 1
loadList()
}
function handlePageChange(p: number) { function handlePageChange(p: number) {
filters.page = p filters.page = p
loadList() loadList()
@@ -441,39 +453,63 @@ function listingOptionLabel(item: AvailableListing) {
</div> </div>
</div> </div>
<div class="filter-bar"> <div class="pickup-filter-bar">
<el-input <el-form inline @submit.prevent>
v-model="filters.keyword" <el-form-item label="关键词" class="pickup-keyword-filter">
placeholder="提号编号 / 上架编号 / 账号标题 / 手机号" <el-input
clearable v-model="filters.keyword"
style="width: 300px" placeholder="提号编号 / 上架编号 / 账号标题 / 手机号"
@keyup.enter="handleSearch" clearable
/> @keyup.enter="handleSearch"
<el-select v-model="filters.status" placeholder="状态" clearable style="width: 130px"> @clear="handleSearch"
<el-option label="提号中" value="picking_up" /> />
<el-option label="已完成" value="completed" /> </el-form-item>
<el-option label="已取消" value="cancelled" /> <el-form-item label="提号状态">
</el-select> <el-select v-model="filters.status" clearable placeholder="全部" @change="handleSearch">
<el-select <el-option label="提号中" value="picking_up" />
v-model="filters.shop_name" <el-option label="已完成" value="completed" />
placeholder="店铺名称" <el-option label="已取消" value="cancelled" />
filterable </el-select>
clearable </el-form-item>
style="width: 160px" <el-form-item label="店铺">
> <el-select
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" /> v-model="filters.shop_name"
</el-select> filterable
<el-select clearable
v-model="filters.account_source" placeholder="全部"
placeholder="账号来源" @change="handleSearch"
clearable >
style="width: 190px" <el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
> </el-select>
<el-option label="外部上传 · 平台代管" value="external_platform_managed" /> </el-form-item>
<el-option label="站内上传" value="internal" /> <el-form-item label="账号来源">
</el-select> <el-select
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button> v-model="filters.account_source"
<el-button :icon="Refresh" @click="loadList">刷新</el-button> clearable
placeholder="全部"
@change="handleSearch"
>
<el-option label="外部上传 · 平台代管" value="external_platform_managed" />
<el-option label="站内上传" value="internal" />
</el-select>
</el-form-item>
<el-form-item label="线下结算">
<el-select
v-model="filters.offline_settlement_status"
clearable
placeholder="全部"
@change="handleSearch"
>
<el-option label="待确认" value="pending" />
<el-option label="已确认" value="settled" />
</el-select>
</el-form-item>
<el-form-item class="pickup-filter-actions">
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</el-form-item>
</el-form>
</div> </div>
<el-table v-loading="loading" :data="items" class="table-panel"> <el-table v-loading="loading" :data="items" class="table-panel">
@@ -872,6 +908,70 @@ function listingOptionLabel(item: AvailableListing) {
font-size: 12px; font-size: 12px;
color: #64748b; color: #64748b;
} }
.pickup-filter-bar {
margin-bottom: 16px;
}
.pickup-filter-bar :deep(.el-form) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
}
.pickup-filter-bar :deep(.el-form-item) {
margin: 0;
}
.pickup-filter-bar :deep(.el-form-item__label) {
padding-right: 7px;
color: #52616f;
font-size: 13px;
}
.pickup-filter-bar :deep(.el-select) {
width: 128px;
}
.pickup-filter-bar :deep(.el-form-item:nth-child(3) .el-select) {
width: 150px;
}
.pickup-filter-bar :deep(.el-form-item:nth-child(4) .el-select) {
width: 175px;
}
.pickup-keyword-filter :deep(.el-input) {
width: 280px;
}
.pickup-filter-actions :deep(.el-form-item__content) {
gap: 8px;
}
@media (max-width: 900px) {
.pickup-filter-bar :deep(.el-form) {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pickup-filter-bar :deep(.el-form-item),
.pickup-filter-bar :deep(.el-select),
.pickup-keyword-filter :deep(.el-input) {
width: 100%;
}
.pickup-filter-bar :deep(.el-form-item__content) {
min-width: 0;
flex: 1;
}
}
@media (max-width: 600px) {
.pickup-filter-bar :deep(.el-form) {
grid-template-columns: minmax(0, 1fr);
}
}
.text-muted { .text-muted {
color: #94a3b8; color: #94a3b8;
} }
@@ -196,6 +196,7 @@ export interface AdminOrderQuery {
handoff_status?: string handoff_status?: string
handoff_mode?: string handoff_mode?: string
settlement_status?: string settlement_status?: string
offline_settlement_status?: 'pending' | 'settled' | ''
} }
interface SubmitCheckoutRequest { interface SubmitCheckoutRequest {