diff --git a/backend/internal/modules/distribution/dto.go b/backend/internal/modules/distribution/dto.go index 765b177..b6c25b2 100644 --- a/backend/internal/modules/distribution/dto.go +++ b/backend/internal/modules/distribution/dto.go @@ -145,13 +145,15 @@ type DistributionOrderDTO struct { } type DistributionOrderListQuery struct { - SiteID uint64 - Kind string - Role string - Status string - Keyword string - Page int - Size int + SiteID uint64 + Kind string + Role string + Status string + Mode string + FinancialStatus string + Keyword string + Page int + Size int } type DistributionOrderPage struct { diff --git a/backend/internal/modules/distribution/handler.go b/backend/internal/modules/distribution/handler.go index 3a4e1df..1d841ae 100644 --- a/backend/internal/modules/distribution/handler.go +++ b/backend/internal/modules/distribution/handler.go @@ -144,13 +144,15 @@ func (h *Handler) ListOrders(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{ - SiteID: scoped, - Kind: c.Query("kind"), - Role: c.Query("role"), - Status: c.Query("status"), - Keyword: c.Query("keyword"), - Page: page, - Size: size, + SiteID: scoped, + Kind: c.Query("kind"), + Role: c.Query("role"), + Status: c.Query("status"), + Mode: c.Query("mode"), + FinancialStatus: c.Query("financial_status"), + Keyword: c.Query("keyword"), + Page: page, + Size: size, }) if err != nil { writeError(c, err) diff --git a/backend/internal/modules/distribution/repository.go b/backend/internal/modules/distribution/repository.go index 9a3c402..5dd72ff 100644 --- a/backend/internal/modules/distribution/repository.go +++ b/backend/internal/modules/distribution/repository.go @@ -331,9 +331,16 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd } func (r *Repository) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) { - db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}).Order("effective_from DESC, id DESC") + db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}). + Order("CASE WHEN site_id IS NULL THEN 0 ELSE 1 END ASC, site_id ASC, mode ASC, version DESC, id DESC") if siteID == nil { - db = db.Where("site_id IS NULL") + global, err := r.hasGlobalDistributionAccess(ctx, adminID) + if err != nil { + return nil, err + } + if !global { + db = db.Where("site_id IS NULL") + } } else { if err := r.ensureSiteAccess(ctx, adminID, *siteID); err != nil { return nil, err @@ -475,6 +482,12 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList if query.Status != "" { base = base.Where("ro.status = ?", query.Status) } + if mode := strings.TrimSpace(query.Mode); mode != "" { + base = base.Where("ro.distribution_mode = ?", mode) + } + if financialStatus := strings.TrimSpace(query.FinancialStatus); financialStatus != "" { + base = base.Where("f.settlement_status = ?", financialStatus) + } if strings.TrimSpace(query.Keyword) != "" { kw := "%" + strings.TrimSpace(query.Keyword) + "%" base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw) diff --git a/frontend/src/features/admin/api/adminDistribution.ts b/frontend/src/features/admin/api/adminDistribution.ts index f6f017f..f1bee2f 100644 --- a/frontend/src/features/admin/api/adminDistribution.ts +++ b/frontend/src/features/admin/api/adminDistribution.ts @@ -42,7 +42,11 @@ export interface DistributionOrder { status: string refund_status: string settlement_status: string + distributor_settlement_status?: string + release_at?: string distribution_mode: string + gross_paid_amount_cent: number + refund_amount_cent: number net_paid_amount_cent: number payment_fee_cent: number service_fee_cent: number @@ -194,6 +198,8 @@ export async function updateDistributionRuleStatus(id: number, status: string) { export async function fetchDistributionOrders(params: { kind?: string status?: string + mode?: string + financialStatus?: string siteID?: number page?: number pageSize?: number @@ -204,6 +210,8 @@ export async function fetchDistributionOrders(params: { params: { kind: params.kind || undefined, status: params.status || undefined, + mode: params.mode || undefined, + financial_status: params.financialStatus || undefined, site_id: params.siteID || undefined, page: params.page || 1, page_size: params.pageSize || 20, diff --git a/frontend/src/features/admin/utils/distributionLabels.ts b/frontend/src/features/admin/utils/distributionLabels.ts new file mode 100644 index 0000000..598c45f --- /dev/null +++ b/frontend/src/features/admin/utils/distributionLabels.ts @@ -0,0 +1,112 @@ +import type { DistributionRule } from '@/features/admin/api/adminDistribution' + +export const DISTRIBUTION_MODES = [ + { + value: 'two_party_distributor_receive', + label: '两方 · 分站收号', + hint: '主站下单,货在分站', + }, + { + value: 'two_party_platform_receive', + label: '两方 · 分站卖出', + hint: '分站卖主站货', + }, + { + value: 'three_party', + label: '三方分账', + hint: '分站卖另一分站的货', + }, +] as const + +const MODE_LABELS: Record = Object.fromEntries( + DISTRIBUTION_MODES.map(item => [item.value, item.label]) +) + +const PARTY_ROLE_LABELS: Record = { + distributor_receive: '分站收号', + platform_sell: '主站卖出', + platform_receive: '主站收号', + distributor_sell: '分站卖出', + distributor_a: '收号站', + distributor_b: '卖出站', +} + +export function distributionModeLabel(mode?: string) { + if (!mode) return '-' + return MODE_LABELS[mode] || mode +} + +export function distributionPartyRoleLabel(role?: string) { + if (!role) return '-' + return PARTY_ROLE_LABELS[role] || role +} + +export function formatRatioPercent(value?: number) { + const n = Number(value || 0) + return `${n.toFixed(n % 1 === 0 ? 0 : 1)}%` +} + +export function formatPaymentFeeBps(bps?: number) { + return `${Number(bps || 0) / 100}%` +} + +export function ruleSplitParts(rule: DistributionRule) { + switch (rule.mode) { + case 'two_party_distributor_receive': + return [ + { label: '分站收号', value: formatRatioPercent(rule.distributor_receive_ratio) }, + { label: '主站卖出', value: formatRatioPercent(rule.platform_sell_ratio) }, + ] + case 'two_party_platform_receive': + return [ + { label: '主站收号', value: formatRatioPercent(rule.platform_receive_ratio) }, + { label: '分站卖出', value: formatRatioPercent(rule.distributor_sell_ratio) }, + ] + case 'three_party': + return [ + { label: '收号站', value: formatRatioPercent(rule.distributor_a_ratio) }, + { label: '卖出站', value: formatRatioPercent(rule.distributor_b_ratio) }, + ] + default: + return [] + } +} + +export function formatServiceFee( + rule: Pick +) { + if (rule.service_fee_unit === 'fixed_amount') { + return `${Number(rule.service_fee_value || 0).toFixed(2)} 元/单` + } + return formatRatioPercent(rule.service_fee_value) +} + +export function settlementBatchKind(batchNo?: string) { + if (batchNo?.startsWith('DA')) { + return { label: '自动释放', type: 'success' as const } + } + if (batchNo?.startsWith('DS')) { + return { label: '人工批次', type: 'warning' as const } + } + return { label: '结算批次', type: 'info' as const } +} + +export function distributorReleaseLabel(row: { + distributor_settlement_status?: string + release_at?: string +}) { + if (row.distributor_settlement_status === 'settled') { + return { label: '已释放入账', type: 'success' as const } + } + if (row.distributor_settlement_status === 'pending' && row.release_at) { + const date = new Date(row.release_at) + const text = Number.isNaN(date.getTime()) + ? '待释放' + : `待释放 ${date.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })}` + return { label: text, type: 'warning' as const } + } + if (row.distributor_settlement_status === 'pending') { + return { label: '待释放', type: 'info' as const } + } + return { label: '未进入释放', type: 'info' as const } +} diff --git a/frontend/src/features/admin/views/AdminDistributionOrdersView.vue b/frontend/src/features/admin/views/AdminDistributionOrdersView.vue index 187099b..daaff0d 100644 --- a/frontend/src/features/admin/views/AdminDistributionOrdersView.vue +++ b/frontend/src/features/admin/views/AdminDistributionOrdersView.vue @@ -7,8 +7,14 @@ import { type DistributionOrder, type DistributionSite, } from '@/features/admin/api/adminDistribution' +import { + DISTRIBUTION_MODES, + distributionModeLabel, + distributorReleaseLabel, +} from '@/features/admin/utils/distributionLabels' import { formatCent } from '@/shared/utils/money' import { formatDateTime } from '@/shared/utils/time' +import { orderStatusLabel, refundStatusLabel } from '@/shared/utils/statusLabels' import { readError } from '@/shared/utils/error' const loading = ref(false) @@ -18,18 +24,13 @@ const total = ref(0) const query = reactive({ siteID: undefined as number | undefined, - kind: '' as string, - status: '' as string, + mode: '', + status: '', + financialStatus: '', page: 1, pageSize: 20, }) -const modeMap: Record = { - two_party_distributor_receive: '收号', - two_party_platform_receive: '售出', - three_party: '三方', -} - async function loadSites() { try { sites.value = await fetchDistributionSites() @@ -43,8 +44,9 @@ async function loadOrders() { try { const res = await fetchDistributionOrders({ siteID: query.siteID, - kind: query.kind || undefined, + mode: query.mode || undefined, status: query.status || undefined, + financialStatus: query.financialStatus || undefined, page: query.page, pageSize: query.pageSize, }) @@ -64,15 +66,16 @@ function handleSearch() { function handleReset() { query.siteID = undefined - query.kind = '' + query.mode = '' query.status = '' + query.financialStatus = '' query.page = 1 loadOrders() } function getSiteName(siteId: number) { const site = sites.value.find(s => s.id === siteId) - return site ? `${site.site_name}` : `站点 #${siteId}` + return site ? site.site_name : `站点 #${siteId}` } onMounted(async () => { @@ -85,9 +88,9 @@ onMounted(async () => {
@@ -104,17 +107,33 @@ onMounted(async () => { /> - - - - - + + + - - - - + + + + + + + + + + + + @@ -127,53 +146,62 @@ onMounted(async () => { - + - + - + - + - + + + + + + + - - - - + - + + + + - + @@ -215,6 +243,7 @@ onMounted(async () => { font-size: 13px; color: #6b7280; margin: 0; + max-width: 720px; } .filter-card { @@ -226,4 +255,9 @@ onMounted(async () => { justify-content: flex-end; margin-top: 16px; } + +.muted { + color: #9ca3af; + font-size: 12px; +} diff --git a/frontend/src/features/admin/views/AdminDistributionRulesView.vue b/frontend/src/features/admin/views/AdminDistributionRulesView.vue index aab861a..111aeb2 100644 --- a/frontend/src/features/admin/views/AdminDistributionRulesView.vue +++ b/frontend/src/features/admin/views/AdminDistributionRulesView.vue @@ -1,26 +1,49 @@