chore(init): 初始化解忧商行项目并完成 Phase 0 基座
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/chat"
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
listingpkg "hfb_sys/backend/internal/modules/listing"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
smsnode "hfb_sys/backend/internal/modules/sms"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequest) (*OrderDTO, error) {
|
||||
var createdID uint64
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || listing.InTransaction {
|
||||
return ErrListingUnavailable
|
||||
}
|
||||
if listing.OwnerID == renterID {
|
||||
return ErrCannotRentOwnListing
|
||||
}
|
||||
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
active, err := hasActiveOrderForAssets(tx, listing.ID, account.ID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if active {
|
||||
return ErrListingUnavailable
|
||||
}
|
||||
snapshot, err := makeAccountSnapshot(account, listing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accountSource, sourceChannel, err := orderAccountSourceSnapshot(tx, listing.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orderNo, err := newOrderNo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rentHours := estimateOrderDurationHours(snapshot)
|
||||
shelfRatioDelta, markupSiteID, err := loadShelfRatioTx(tx, req.DistributionSiteCode, listing.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pricing := buildOrderPricing(listing, account)
|
||||
basePriceCent := listing.PriceCent
|
||||
sitePriceCent, _, siteRatio := listingpkg.ApplyShelfRatio(account.HafCoinAmount, listing.PriceCent, shelfRatioDelta)
|
||||
shelfMarkupCent := sitePriceCent - basePriceCent
|
||||
if sitePriceCent != listing.PriceCent {
|
||||
pricing.RentAmountCent = sitePriceCent
|
||||
if pricing.OwnerRentAmountCent > pricing.RentAmountCent {
|
||||
pricing.OwnerRentAmountCent = pricing.RentAmountCent
|
||||
}
|
||||
pricing.PlatformFeeCent = maxCent(pricing.RentAmountCent-pricing.OwnerRentAmountCent, 0)
|
||||
if pricing.ExtraItemAmountCent > pricing.RentAmountCent {
|
||||
pricing.ExtraItemAmountCent = pricing.RentAmountCent
|
||||
}
|
||||
pricing.PureCoinAmountCent = maxCent(pricing.RentAmountCent-pricing.ExtraItemAmountCent, 0)
|
||||
if pricing.OwnerPureCoinAmountCent > pricing.OwnerRentAmountCent {
|
||||
pricing.OwnerPureCoinAmountCent = pricing.OwnerRentAmountCent
|
||||
}
|
||||
pricing.PureCoinPlatformFeeCent = maxCent(pricing.PureCoinAmountCent-pricing.OwnerPureCoinAmountCent, 0)
|
||||
}
|
||||
distributionSite, distributionRule, err := distribution.ResolveOrderSourceTx(tx, req.DistributionSiteCode, listing.ID, account.ID, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rentOriginalAmountCent := pricing.RentAmountCent
|
||||
var growthSnapshot rentergrowth.DiscountSnapshot
|
||||
if distributionSite == nil {
|
||||
snap, err := rentergrowth.SnapshotForUser(tx, renterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
growthSnapshot = snap
|
||||
} else {
|
||||
// 分销站点订单隔离主站成长等级免押与折扣,防止主站免押坏账与折扣侵蚀分销收益
|
||||
growthSnapshot = rentergrowth.DiscountSnapshot{
|
||||
LevelCode: rentergrowth.DefaultLevelCode,
|
||||
LevelName: rentergrowth.DefaultLevelName,
|
||||
DiscountBps: rentergrowth.DefaultDiscountBps,
|
||||
}
|
||||
}
|
||||
rentDiscountAmountCent := rentergrowth.CalculateDiscountCent(pricing.PureCoinAmountCent, pricing.PureCoinPlatformFeeCent, growthSnapshot.DiscountBps)
|
||||
if rentDiscountAmountCent > 0 {
|
||||
pricing.PlatformFeeCent -= rentDiscountAmountCent
|
||||
pricing.RentAmountCent -= rentDiscountAmountCent
|
||||
}
|
||||
depositOriginalAmountCent := listing.DepositAmountCent
|
||||
depositWaiver, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent, growthSnapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceTrackCode := sanitizeTrackCode(req.TrackCode)
|
||||
if sourceTrackCode == "" {
|
||||
var u model.User
|
||||
if err := tx.Select("register_track_code").First(&u, renterID).Error; err == nil && u.RegisterTrackCode != "" {
|
||||
sourceTrackCode = sanitizeTrackCode(u.RegisterTrackCode)
|
||||
}
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: orderNo,
|
||||
ListingID: listing.ID,
|
||||
AccountID: listing.AccountID,
|
||||
OwnerID: listing.OwnerID,
|
||||
RenterID: renterID,
|
||||
EstimatedDurationHours: rentHours,
|
||||
RentAmountCent: pricing.RentAmountCent,
|
||||
OwnerRentAmountCent: pricing.OwnerRentAmountCent,
|
||||
DepositAmountCent: depositWaiver.PaidCent,
|
||||
DepositOriginalAmountCent: depositOriginalAmountCent,
|
||||
DepositWaivedAmountCent: depositWaiver.WaivedCent,
|
||||
DepositFreeLevelQuotaCent: growthSnapshot.LevelDepositFreeQuotaCent,
|
||||
DepositFreeManualQuotaCent: growthSnapshot.ManualDepositFreeQuotaCent,
|
||||
DepositFreeUsedBeforeCent: depositWaiver.UsedBeforeCent,
|
||||
PlatformFeeCent: pricing.PlatformFeeCent,
|
||||
BasePriceCent: basePriceCent,
|
||||
ShelfMarkupCent: shelfMarkupCent,
|
||||
MarkupSiteID: markupSiteID,
|
||||
ShelfRatio: siteRatio,
|
||||
ShelfRatioDelta: shelfRatioDelta,
|
||||
RentOriginalAmountCent: rentOriginalAmountCent,
|
||||
RentDiscountAmountCent: rentDiscountAmountCent,
|
||||
PureCoinOriginalAmountCent: pricing.PureCoinAmountCent,
|
||||
ExtraItemOriginalAmountCent: pricing.ExtraItemAmountCent,
|
||||
RenterGrowthLevel: growthSnapshot.LevelCode,
|
||||
RenterGrowthLevelName: growthSnapshot.LevelName,
|
||||
RenterDiscountBps: growthSnapshot.DiscountBps,
|
||||
AccountSnapshot: snapshot,
|
||||
AccountSource: accountSource,
|
||||
SourceChannel: sourceChannel,
|
||||
Status: orderStatusPendingPayment,
|
||||
HandoffStatus: handoffStatusNone,
|
||||
HandoffMode: listingHandoffMode(listing),
|
||||
SettlementMode: listingSettlementMode(listing),
|
||||
ManagedAdminID: listing.ManagedAdminID,
|
||||
SettlementStatus: settlementStatusUnsettled,
|
||||
OfflineSettlementStatus: offlineSettlementStatusNone,
|
||||
SourceTrackCode: sourceTrackCode,
|
||||
}
|
||||
if distributionSite != nil && distributionRule != nil {
|
||||
order.DistributionSiteID = &distributionSite.ID
|
||||
order.DistributionMode = distributionRule.Mode
|
||||
order.DistributionRuleVersionID = &distributionRule.ID
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单已创建",
|
||||
Content: "订单已创建,请在有效时间内完成支付。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
reserveListingForOrder(&listing)
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
createdID = order.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindForUser(ctx, renterID, createdID, req.DistributionSiteCode)
|
||||
}
|
||||
|
||||
func loadShelfRatioTx(tx *gorm.DB, siteCode string, listingID uint64) (float64, uint64, error) {
|
||||
siteCode = strings.TrimSpace(siteCode)
|
||||
if siteCode == "" || listingID == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
var row struct {
|
||||
SiteID uint64 `gorm:"column:site_id"`
|
||||
Delta float64 `gorm:"column:delta"`
|
||||
}
|
||||
err := tx.Table("distribution_site_accounts AS dsa").
|
||||
Select("dsa.site_id, COALESCE(dsa.shelf_ratio_delta, 0) AS delta").
|
||||
Joins("INNER JOIN distribution_sites AS ds ON ds.id = dsa.site_id AND ds.site_code = ? AND ds.status = 'active'", siteCode).
|
||||
Where("dsa.listing_id = ? AND dsa.display_status = 'enabled'", listingID).
|
||||
Take(&row).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
return 0, 0, err
|
||||
}
|
||||
if row.Delta < 0 {
|
||||
row.Delta = 0
|
||||
}
|
||||
return row.Delta, row.SiteID, nil
|
||||
}
|
||||
|
||||
func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) {
|
||||
var upload model.ListingUpload
|
||||
err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error
|
||||
if err == nil {
|
||||
return "external", upload.SourceChannel, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var listing model.RentalListing
|
||||
if err := tx.Select("distribution_site_id").First(&listing, listingID).Error; err == nil && listing.DistributionSiteID != nil {
|
||||
var site model.DistributionSite
|
||||
if err := tx.Select("site_code, site_name").First(&site, *listing.DistributionSiteID).Error; err == nil {
|
||||
return "distribution", fmt.Sprintf("分销站 - %s (%s)", site.SiteName, site.SiteCode), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "internal", "", nil
|
||||
}
|
||||
|
||||
func listingHandoffMode(listing model.RentalListing) string {
|
||||
if listing.HandoffMode != "" {
|
||||
return listing.HandoffMode
|
||||
}
|
||||
return handoffModeOwner
|
||||
}
|
||||
|
||||
func listingSettlementMode(listing model.RentalListing) string {
|
||||
if listing.SettlementMode != "" {
|
||||
return listing.SettlementMode
|
||||
}
|
||||
return settlementModeOwnerWallet
|
||||
}
|
||||
|
||||
type depositWaiverAmounts struct {
|
||||
PaidCent int64
|
||||
WaivedCent int64
|
||||
UsedBeforeCent int64
|
||||
}
|
||||
|
||||
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64, growthSnapshot rentergrowth.DiscountSnapshot) (depositWaiverAmounts, error) {
|
||||
if originalDepositCent <= 0 {
|
||||
return depositWaiverAmounts{}, nil
|
||||
}
|
||||
used, err := activeDepositFreeUsed(tx, renterID)
|
||||
if err != nil {
|
||||
return depositWaiverAmounts{}, err
|
||||
}
|
||||
paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, growthSnapshot.EffectiveDepositFreeQuotaCent, used)
|
||||
return depositWaiverAmounts{
|
||||
PaidCent: paidDeposit,
|
||||
WaivedCent: waivedDeposit,
|
||||
UsedBeforeCent: used,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) {
|
||||
var used int64
|
||||
err := tx.Model(&model.RentalOrder{}).
|
||||
Where("renter_id = ? AND status NOT IN ?",
|
||||
renterID,
|
||||
[]string{orderStatusCompleted, orderStatusCancelled, orderStatusClosed},
|
||||
).
|
||||
Select("COALESCE(SUM(deposit_waived_amount_cent), 0)").
|
||||
Scan(&used).Error
|
||||
return used, err
|
||||
}
|
||||
|
||||
func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) {
|
||||
remaining := maxCent(quotaCent-usedCent, 0)
|
||||
waived := minCent(originalDepositCent, remaining)
|
||||
paid := maxCent(originalDepositCent-waived, 0)
|
||||
return paid, waived
|
||||
}
|
||||
|
||||
// Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。
|
||||
func (r *Repository) Pay(ctx context.Context, userID uint64, orderID uint64) error {
|
||||
return ErrChannelPaymentRequired
|
||||
}
|
||||
|
||||
// ConfirmPaidFromChannel 在乐刷确认支付后推进订单状态;租客资金不进入站内钱包。
|
||||
func (r *Repository) ConfirmPaidFromChannel(ctx context.Context, orderID uint64, providerBizNo string) error {
|
||||
var newConvID uint64
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
convID, err := r.ConfirmPaidFromChannelTx(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConvID = convID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newConvID > 0 && r.chatNotifier != nil {
|
||||
r.chatNotifier.NotifyNewConversation(newConvID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfirmPaidFromChannelTx 在外部事务内推进支付成功后的订单状态。
|
||||
// 调用方负责提交事务,并在提交成功后按返回的会话 ID 触发异步通知。
|
||||
func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error) {
|
||||
assets, err := r.lockOrderAssets(tx, orderID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
order := assets.Order
|
||||
listing := assets.Listing
|
||||
account := assets.Account
|
||||
if order.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting {
|
||||
return 0, nil
|
||||
}
|
||||
if order.Status != orderStatusPendingPayment {
|
||||
return 0, ErrOrderCannotPay
|
||||
}
|
||||
|
||||
if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction {
|
||||
return 0, ErrListingUnavailable
|
||||
}
|
||||
active, err := hasActiveSiblingOrder(tx, order)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if active {
|
||||
return 0, ErrListingUnavailable
|
||||
}
|
||||
|
||||
// 租客已通过外部渠道付款,这里不写租客钱包流水。
|
||||
orderID = order.ID
|
||||
now := time.Now()
|
||||
order.Status = orderStatusPendingHandoff
|
||||
order.HandoffStatus = handoffStatusPendingOwner
|
||||
order.HandoffStartedAt = &now
|
||||
if err := markAssetsRented(tx, listing, account); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 付款后只拉租客进账号群。主站和分销都不再另建订单群,二维码仍是发布时分到这个群的那一张。
|
||||
conversationID := uint64(0)
|
||||
if err := chat.AddRenterToListingConversation(tx, listing.ID, order.RenterID, order.OrderNo); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var listingConv model.ChatConversation
|
||||
err = tx.Where("listing_id = ? AND type = ?", listing.ID, chat.ConversationTypeListingGroup).First(&listingConv).Error
|
||||
if err == nil {
|
||||
conversationID = listingConv.ID
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if isPlatformHandoffOrder(*order) {
|
||||
if err := appendManagedAdminNotification(tx, *order, "order", "代管订单待交接", "租客已完成支付,请尽快在订单详情中提交交接说明。"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单支付成功",
|
||||
Content: "支付已完成,等待客服提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "收到新的租号订单",
|
||||
Content: "租客已完成支付,请尽快提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单支付成功",
|
||||
Content: "支付已完成,等待号主提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// 节点短信:账号被租用、需要号主辅助登录,提醒及时处理避免被强制结束订单。
|
||||
// 代管订单由客服提交交接说明,不触达号主,因此只在这个分支入队。
|
||||
if _, err := smsnode.Enqueue(tx, smsnode.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Node: smsnode.NodeOrderHandoffAssist,
|
||||
BizType: "order",
|
||||
BizID: orderID,
|
||||
DistributionSiteID: order.DistributionSiteID,
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return conversationID, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64) error {
|
||||
// 租客可能一边点取消一边在渠道完成付款。先核对渠道,已收款就让支付正常入账,
|
||||
// 不要把订单取消掉,否则钱收了订单却没了。
|
||||
if r.pendingPaymentOrderPaidAtChannel(ctx, userID, orderID) {
|
||||
return ErrOrderPaidAtChannel
|
||||
}
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ? AND renter_id = ?", orderID, userID).
|
||||
First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.Status != orderStatusPendingPayment && order.Status != orderStatusPendingHandoff {
|
||||
return ErrOrderCannotCancel
|
||||
}
|
||||
listing, account, err := r.lockListingAccountForOrder(tx, &order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
beforeStatus := order.Status
|
||||
now := time.Now()
|
||||
order.Status = orderStatusCancelled
|
||||
order.SettledAt = &now
|
||||
orderID := order.ID
|
||||
if beforeStatus == orderStatusPendingHandoff {
|
||||
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||
order.RefundStatus = refundStatusPendingReview
|
||||
order.RefundAmountCent = totalCent
|
||||
order.RefundedAt = nil
|
||||
} else {
|
||||
order.HandoffStatus = handoffStatusCancelled
|
||||
}
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "order", "代管订单已取消", "租客已取消订单,退款待客服审核。"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将由客服审核后原路退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "订单已取消",
|
||||
Content: "租客已取消订单,退款待客服审核。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将由客服审核后原路退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 已付款订单取消后需等待客服审核退款,审核通过前继续占用账号。
|
||||
if beforeStatus == orderStatusPendingPayment {
|
||||
if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(account).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closePendingOrderPayments 在订单取消时关闭仍未完成的下单支付流水,避免后台一直显示“支付中”。
|
||||
func closePendingOrderPayments(tx *gorm.DB, orderID uint64, source string) error {
|
||||
raw, err := json.Marshal(map[string]string{
|
||||
"source": source,
|
||||
"reason": "订单已取消,关闭未完成支付单",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.PaymentOrder{}).
|
||||
Where("order_id = ? AND biz_type = ? AND status IN ?", orderID, "order_pay", []string{"created", "paying"}).
|
||||
Updates(map[string]any{
|
||||
"status": "closed",
|
||||
"raw_response": datatypes.JSON(raw),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID uint64) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != orderStatusPendingHandoff || order.HandoffStatus != handoffStatusPendingRenterConfirm {
|
||||
return ErrOrderCannotReceive
|
||||
}
|
||||
before := orderState(order)
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type IN ?", order.ID, []string{"owner_handoff", "platform_handoff"}).
|
||||
Update("confirmed_by_renter_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = orderStatusRenting
|
||||
order.HandoffStatus = handoffStatusReceived
|
||||
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||
order.RentedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "handoff", "renter_received_confirmed", processlog.ActorUser, userID, processlog.ActorUser, &order.OwnerID, "租客确认已收号,订单开始租用。", "", nil, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "handoff", "租客已确认收号", "代管订单已进入使用中。"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "handoff",
|
||||
Title: "租客已确认收号",
|
||||
Content: "订单已进入使用中。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user