From 21aabd77f14866e566ee8b60e44c666d391cd193 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 27 Jun 2026 23:01:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E5=B7=B2=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/listing/mutation.go | 13 ++++-- .../internal/modules/listing/service_test.go | 41 +++++++++++++++++++ backend/internal/modules/order/assets.go | 7 ++++ .../modules/order/checkout_finalize.go | 2 +- backend/internal/modules/order/constants.go | 1 + .../internal/modules/order/repository_test.go | 25 +++++++++++ .../000018_completed_order_listing_status.sql | 25 +++++++++++ .../admin/views/AdminListingsView.vue | 1 + .../seller/composables/usePublishForm.ts | 5 +++ .../seller/views/SellerListingsView.vue | 28 ++++++++++--- frontend/src/shared/types/status.ts | 9 +++- frontend/src/shared/utils/statusLabels.ts | 1 + 12 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/000018_completed_order_listing_status.sql diff --git a/backend/internal/modules/listing/mutation.go b/backend/internal/modules/listing/mutation.go index 5df0ac6..d3b01eb 100644 --- a/backend/internal/modules/listing/mutation.go +++ b/backend/internal/modules/listing/mutation.go @@ -208,7 +208,7 @@ func (r *Repository) Update(ctx context.Context, ownerID uint64, listingID uint6 if err != nil { return err } - if listing.Status == "rented" || listing.InTransaction { + if listingLockedForOwnerMutation(listing) { return ErrListingLocked } @@ -256,7 +256,7 @@ func (r *Repository) SubmitReview(ctx context.Context, ownerID uint64, listingID if err != nil { return err } - if listing.Status == "rented" || listing.InTransaction { + if listingLockedForOwnerMutation(listing) { return ErrListingLocked } listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) @@ -346,7 +346,7 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint if err != nil { return err } - if listing.Status == "rented" || listing.InTransaction { + if listingLockedForOwnerMutation(listing) { return ErrListingLocked } listing.Status = "offline" @@ -365,3 +365,10 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint }) return dto, err } + +func listingLockedForOwnerMutation(listing *model.RentalListing) bool { + if listing == nil { + return true + } + return listing.Status == "rented" || listing.Status == "completed" || listing.InTransaction +} diff --git a/backend/internal/modules/listing/service_test.go b/backend/internal/modules/listing/service_test.go index 0160371..3593c49 100644 --- a/backend/internal/modules/listing/service_test.go +++ b/backend/internal/modules/listing/service_test.go @@ -213,6 +213,47 @@ func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) { } } +func TestRepositoryRejectsCompletedListingOwnerMutations(t *testing.T) { + db := database.NewTestDB() + if err := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); err != nil { + t.Fatalf("failed to migrate test db: %v", err) + } + repo := NewRepository(db, nil) + account := model.GameAccount{ + OwnerID: 1, + GameName: "delta_force", + ServerRegion: "QQ", + LoginPlatform: "QQ账号密码", + Title: "已完成账号", + Status: "offline", + } + if err := db.Create(&account).Error; err != nil { + t.Fatalf("failed to create account: %v", err) + } + listing := model.RentalListing{ + ListingNo: "202606270099", + AccountID: account.ID, + OwnerID: 1, + Status: "completed", + ReviewStatus: "approved", + PriceCent: 10000, + DepositAmountCent: 50000, + } + if err := db.Create(&listing).Error; err != nil { + t.Fatalf("failed to create listing: %v", err) + } + + if _, err := repo.Update(t.Context(), 1, listing.ID, validCreateRequest(), false); err != ErrListingLocked { + t.Fatalf("Update expected ErrListingLocked, got %v", err) + } + if _, err := repo.SubmitReview(t.Context(), 1, listing.ID, false); err != ErrListingLocked { + t.Fatalf("SubmitReview expected ErrListingLocked, got %v", err) + } + if _, err := repo.Offline(t.Context(), 1, listing.ID); err != ErrListingLocked { + t.Fatalf("Offline expected ErrListingLocked, got %v", err) + } +} + func validCreateRequest() CreateRequest { return CreateRequest{ Title: "测试账号", diff --git a/backend/internal/modules/order/assets.go b/backend/internal/modules/order/assets.go index 5768df0..bb2dd05 100644 --- a/backend/internal/modules/order/assets.go +++ b/backend/internal/modules/order/assets.go @@ -63,6 +63,13 @@ func archiveAssets(listing *model.RentalListing, account *model.GameAccount) { account.Status = accountStatusOffline } +func completeAssets(listing *model.RentalListing, account *model.GameAccount) { + listing.Status = listingStatusCompleted + listing.InTransaction = false + listing.PublishedAt = nil + account.Status = accountStatusOffline +} + func markAssetsAbnormal(listing *model.RentalListing, account *model.GameAccount) { listing.Status = listingStatusAbnormal listing.InTransaction = false diff --git a/backend/internal/modules/order/checkout_finalize.go b/backend/internal/modules/order/checkout_finalize.go index fc5f707..c665714 100644 --- a/backend/internal/modules/order/checkout_finalize.go +++ b/backend/internal/modules/order/checkout_finalize.go @@ -21,7 +21,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che order.SettlementStatus = settlementStatusSettled order.SettledAt = &now order.OwnerSettledAt = &now - archiveAssets(listing, account) + completeAssets(listing, account) settlement := buildCheckoutSettlement(*order, checkout) if err := appendCheckoutOwnerIncome(tx, order, settlement); err != nil { diff --git a/backend/internal/modules/order/constants.go b/backend/internal/modules/order/constants.go index a6bb424..1c2f94a 100644 --- a/backend/internal/modules/order/constants.go +++ b/backend/internal/modules/order/constants.go @@ -47,6 +47,7 @@ const ( listingStatusPublished = "published" listingStatusRented = "rented" listingStatusOffline = "offline" + listingStatusCompleted = "completed" listingStatusAbnormal = "abnormal" accountStatusPublished = "published" diff --git a/backend/internal/modules/order/repository_test.go b/backend/internal/modules/order/repository_test.go index 53ebca4..c7e4322 100644 --- a/backend/internal/modules/order/repository_test.go +++ b/backend/internal/modules/order/repository_test.go @@ -165,6 +165,31 @@ func TestArchiveAssetsMovesListingOffline(t *testing.T) { } } +func TestCompleteAssetsMovesListingCompleted(t *testing.T) { + now := time.Now() + listing := model.RentalListing{ + Status: listingStatusRented, + InTransaction: true, + PublishedAt: &now, + } + account := model.GameAccount{Status: accountStatusRented} + + completeAssets(&listing, &account) + + if listing.Status != listingStatusCompleted { + t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusCompleted) + } + if listing.InTransaction { + t.Fatal("listing.InTransaction = true, want false") + } + if listing.PublishedAt != nil { + t.Fatal("listing.PublishedAt is not nil") + } + if account.Status != accountStatusOffline { + t.Fatalf("account.Status = %q, want %q", account.Status, accountStatusOffline) + } +} + func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) { oldLocal := time.Local time.Local = time.UTC diff --git a/backend/migrations/000018_completed_order_listing_status.sql b/backend/migrations/000018_completed_order_listing_status.sql new file mode 100644 index 0000000..d1ffb2d --- /dev/null +++ b/backend/migrations/000018_completed_order_listing_status.sql @@ -0,0 +1,25 @@ +-- +goose Up +-- +goose StatementBegin + +UPDATE rental_listings l +JOIN rental_orders o ON o.listing_id = l.id +SET l.status = 'completed', + l.in_transaction = 0, + l.published_at = NULL +WHERE o.status = 'completed' + AND l.status = 'offline'; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +UPDATE rental_listings l +JOIN rental_orders o ON o.listing_id = l.id +SET l.status = 'offline', + l.in_transaction = 0, + l.published_at = NULL +WHERE o.status = 'completed' + AND l.status = 'completed'; + +-- +goose StatementEnd diff --git a/frontend/src/features/admin/views/AdminListingsView.vue b/frontend/src/features/admin/views/AdminListingsView.vue index 789a614..6f7dea2 100644 --- a/frontend/src/features/admin/views/AdminListingsView.vue +++ b/frontend/src/features/admin/views/AdminListingsView.vue @@ -473,6 +473,7 @@ function formatQuantity(value: number) { + diff --git a/frontend/src/features/seller/composables/usePublishForm.ts b/frontend/src/features/seller/composables/usePublishForm.ts index 5feb365..0ddd56d 100644 --- a/frontend/src/features/seller/composables/usePublishForm.ts +++ b/frontend/src/features/seller/composables/usePublishForm.ts @@ -190,6 +190,11 @@ export function usePublishForm(options: UsePublishFormOptions) { loading.value = true try { const listing = await fetchSellerListing(editListingID.value) + if (listing.status === 'completed') { + options.notifyError('该商品订单已完成,不能再次编辑') + await router.push(options.submitSuccessPath) + return + } applyListingToForm(listing) } catch (error) { options.notifyError(readError(error, '商品信息加载失败')) diff --git a/frontend/src/features/seller/views/SellerListingsView.vue b/frontend/src/features/seller/views/SellerListingsView.vue index dc67155..ab91219 100644 --- a/frontend/src/features/seller/views/SellerListingsView.vue +++ b/frontend/src/features/seller/views/SellerListingsView.vue @@ -44,6 +44,11 @@ const stats = computed(() => [ label: '已下架', value: listings.value.filter(item => item.status === 'offline').length, }, + { + key: 'completed', + label: '已完成', + value: listings.value.filter(item => item.status === 'completed').length, + }, ]) onMounted(loadListings) @@ -124,15 +129,15 @@ function coinText(row: Listing) { } function canSubmit(row: Listing) { - return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published' + return !isTerminalListing(row) && !isPendingReview(row) && row.status !== 'published' } function canEdit(row: Listing) { - return !isPendingReview(row) && row.status !== 'rented' && row.status !== 'published' + return !isTerminalListing(row) && !isPendingReview(row) && row.status !== 'published' } function canOffline(row: Listing) { - return row.status !== 'rented' && row.status !== 'offline' + return !isTerminalListing(row) && row.status !== 'offline' } function editPath(row: Listing) { @@ -146,6 +151,7 @@ function statusTone(status: string) { published: 'success', draft: 'info', offline: 'muted', + completed: 'success', rented: 'warning', abnormal: 'danger', } @@ -166,8 +172,16 @@ function effectiveReviewStatus(row: Listing) { return row.status === 'offline' ? 'none' : row.review_status } +function showReviewStatus(row: Listing) { + return row.status !== 'completed' +} + +function isTerminalListing(row: Listing) { + return row.status === 'rented' || row.status === 'completed' +} + function isPendingReview(row: Listing) { - return row.status !== 'offline' && row.review_status === 'pending' + return !isTerminalListing(row) && row.status !== 'offline' && row.review_status === 'pending' } @@ -218,7 +232,11 @@ function isPendingReview(row: Listing) { {{ listingStatusLabel(item.status) }} - + {{ listingReviewStatusLabel(effectiveReviewStatus(item)) }} diff --git a/frontend/src/shared/types/status.ts b/frontend/src/shared/types/status.ts index 37f2519..38fb7c7 100644 --- a/frontend/src/shared/types/status.ts +++ b/frontend/src/shared/types/status.ts @@ -1,4 +1,11 @@ -export const listingStatuses = ['draft', 'published', 'rented', 'offline', 'abnormal'] as const +export const listingStatuses = [ + 'draft', + 'published', + 'rented', + 'offline', + 'completed', + 'abnormal', +] as const export type ListingStatus = (typeof listingStatuses)[number] export const listingReviewStatuses = ['none', 'pending', 'approved', 'rejected'] as const diff --git a/frontend/src/shared/utils/statusLabels.ts b/frontend/src/shared/utils/statusLabels.ts index 06c3a42..23f8474 100644 --- a/frontend/src/shared/utils/statusLabels.ts +++ b/frontend/src/shared/utils/statusLabels.ts @@ -18,6 +18,7 @@ const listingStatusMap: Record = { published: '已上架', rented: '租用中', offline: '已下架', + completed: '已完成', abnormal: '异常', }