商品已完成优化
This commit is contained in:
@@ -208,7 +208,7 @@ func (r *Repository) Update(ctx context.Context, ownerID uint64, listingID uint6
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if listing.Status == "rented" || listing.InTransaction {
|
if listingLockedForOwnerMutation(listing) {
|
||||||
return ErrListingLocked
|
return ErrListingLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ func (r *Repository) SubmitReview(ctx context.Context, ownerID uint64, listingID
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if listing.Status == "rented" || listing.InTransaction {
|
if listingLockedForOwnerMutation(listing) {
|
||||||
return ErrListingLocked
|
return ErrListingLocked
|
||||||
}
|
}
|
||||||
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
|
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
|
||||||
@@ -346,7 +346,7 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if listing.Status == "rented" || listing.InTransaction {
|
if listingLockedForOwnerMutation(listing) {
|
||||||
return ErrListingLocked
|
return ErrListingLocked
|
||||||
}
|
}
|
||||||
listing.Status = "offline"
|
listing.Status = "offline"
|
||||||
@@ -365,3 +365,10 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint
|
|||||||
})
|
})
|
||||||
return dto, err
|
return dto, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listingLockedForOwnerMutation(listing *model.RentalListing) bool {
|
||||||
|
if listing == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return listing.Status == "rented" || listing.Status == "completed" || listing.InTransaction
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
func validCreateRequest() CreateRequest {
|
||||||
return CreateRequest{
|
return CreateRequest{
|
||||||
Title: "测试账号",
|
Title: "测试账号",
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ func archiveAssets(listing *model.RentalListing, account *model.GameAccount) {
|
|||||||
account.Status = accountStatusOffline
|
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) {
|
func markAssetsAbnormal(listing *model.RentalListing, account *model.GameAccount) {
|
||||||
listing.Status = listingStatusAbnormal
|
listing.Status = listingStatusAbnormal
|
||||||
listing.InTransaction = false
|
listing.InTransaction = false
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
|||||||
order.SettlementStatus = settlementStatusSettled
|
order.SettlementStatus = settlementStatusSettled
|
||||||
order.SettledAt = &now
|
order.SettledAt = &now
|
||||||
order.OwnerSettledAt = &now
|
order.OwnerSettledAt = &now
|
||||||
archiveAssets(listing, account)
|
completeAssets(listing, account)
|
||||||
|
|
||||||
settlement := buildCheckoutSettlement(*order, checkout)
|
settlement := buildCheckoutSettlement(*order, checkout)
|
||||||
if err := appendCheckoutOwnerIncome(tx, order, settlement); err != nil {
|
if err := appendCheckoutOwnerIncome(tx, order, settlement); err != nil {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const (
|
|||||||
listingStatusPublished = "published"
|
listingStatusPublished = "published"
|
||||||
listingStatusRented = "rented"
|
listingStatusRented = "rented"
|
||||||
listingStatusOffline = "offline"
|
listingStatusOffline = "offline"
|
||||||
|
listingStatusCompleted = "completed"
|
||||||
listingStatusAbnormal = "abnormal"
|
listingStatusAbnormal = "abnormal"
|
||||||
|
|
||||||
accountStatusPublished = "published"
|
accountStatusPublished = "published"
|
||||||
|
|||||||
@@ -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) {
|
func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {
|
||||||
oldLocal := time.Local
|
oldLocal := time.Local
|
||||||
time.Local = time.UTC
|
time.Local = time.UTC
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -473,6 +473,7 @@ function formatQuantity(value: number) {
|
|||||||
<el-option label="已上架" value="published" />
|
<el-option label="已上架" value="published" />
|
||||||
<el-option label="已锁定" value="rented" />
|
<el-option label="已锁定" value="rented" />
|
||||||
<el-option label="已下架" value="offline" />
|
<el-option label="已下架" value="offline" />
|
||||||
|
<el-option label="已完成" value="completed" />
|
||||||
<el-option label="异常" value="abnormal" />
|
<el-option label="异常" value="abnormal" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const listing = await fetchSellerListing(editListingID.value)
|
const listing = await fetchSellerListing(editListingID.value)
|
||||||
|
if (listing.status === 'completed') {
|
||||||
|
options.notifyError('该商品订单已完成,不能再次编辑')
|
||||||
|
await router.push(options.submitSuccessPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
applyListingToForm(listing)
|
applyListingToForm(listing)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
options.notifyError(readError(error, '商品信息加载失败'))
|
options.notifyError(readError(error, '商品信息加载失败'))
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ const stats = computed(() => [
|
|||||||
label: '已下架',
|
label: '已下架',
|
||||||
value: listings.value.filter(item => item.status === 'offline').length,
|
value: listings.value.filter(item => item.status === 'offline').length,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'completed',
|
||||||
|
label: '已完成',
|
||||||
|
value: listings.value.filter(item => item.status === 'completed').length,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
onMounted(loadListings)
|
onMounted(loadListings)
|
||||||
@@ -124,15 +129,15 @@ function coinText(row: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canSubmit(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) {
|
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) {
|
function canOffline(row: Listing) {
|
||||||
return row.status !== 'rented' && row.status !== 'offline'
|
return !isTerminalListing(row) && row.status !== 'offline'
|
||||||
}
|
}
|
||||||
|
|
||||||
function editPath(row: Listing) {
|
function editPath(row: Listing) {
|
||||||
@@ -146,6 +151,7 @@ function statusTone(status: string) {
|
|||||||
published: 'success',
|
published: 'success',
|
||||||
draft: 'info',
|
draft: 'info',
|
||||||
offline: 'muted',
|
offline: 'muted',
|
||||||
|
completed: 'success',
|
||||||
rented: 'warning',
|
rented: 'warning',
|
||||||
abnormal: 'danger',
|
abnormal: 'danger',
|
||||||
}
|
}
|
||||||
@@ -166,8 +172,16 @@ function effectiveReviewStatus(row: Listing) {
|
|||||||
return row.status === 'offline' ? 'none' : row.review_status
|
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) {
|
function isPendingReview(row: Listing) {
|
||||||
return row.status !== 'offline' && row.review_status === 'pending'
|
return !isTerminalListing(row) && row.status !== 'offline' && row.review_status === 'pending'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -218,7 +232,11 @@ function isPendingReview(row: Listing) {
|
|||||||
<span class="status-pill" :class="statusTone(item.status)">{{
|
<span class="status-pill" :class="statusTone(item.status)">{{
|
||||||
listingStatusLabel(item.status)
|
listingStatusLabel(item.status)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="status-pill" :class="reviewTone(effectiveReviewStatus(item))">
|
<span
|
||||||
|
v-if="showReviewStatus(item)"
|
||||||
|
class="status-pill"
|
||||||
|
:class="reviewTone(effectiveReviewStatus(item))"
|
||||||
|
>
|
||||||
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
{{ listingReviewStatusLabel(effectiveReviewStatus(item)) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 type ListingStatus = (typeof listingStatuses)[number]
|
||||||
|
|
||||||
export const listingReviewStatuses = ['none', 'pending', 'approved', 'rejected'] as const
|
export const listingReviewStatuses = ['none', 'pending', 'approved', 'rejected'] as const
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const listingStatusMap: Record<ListingStatus, string> = {
|
|||||||
published: '已上架',
|
published: '已上架',
|
||||||
rented: '租用中',
|
rented: '租用中',
|
||||||
offline: '已下架',
|
offline: '已下架',
|
||||||
|
completed: '已完成',
|
||||||
abnormal: '异常',
|
abnormal: '异常',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user