完善订单交接结账流程留痕
This commit is contained in:
@@ -150,6 +150,19 @@ func (h *Handler) ListFinancialAdjustments(c *gin.Context) {
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
func (h *Handler) ProcessEvents(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.ProcessEvents(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writePickupError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// SettleFinancialAdjustment 确认代管提号的补款或追回调整已线下处理。
|
||||
func (h *Handler) SettleFinancialAdjustment(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package pickup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const processBusinessPickup = "pickup"
|
||||
|
||||
type ProcessEventDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Stage string `json:"stage"`
|
||||
Action string `json:"action"`
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID uint64 `json:"actor_id"`
|
||||
ActorName string `json:"actor_name"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID *uint64 `json:"target_id,omitempty"`
|
||||
TargetName string `json:"target_name"`
|
||||
Content string `json:"content"`
|
||||
Reason string `json:"reason"`
|
||||
Payload datatypes.JSON `json:"payload"`
|
||||
AttachmentURLs []string `json:"attachment_urls"`
|
||||
StateBefore datatypes.JSON `json:"state_before"`
|
||||
StateAfter datatypes.JSON `json:"state_after"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func pickupState(row model.AdminPickup) map[string]any {
|
||||
return map[string]any{
|
||||
"pickup_status": row.Status, "offline_settlement_status": row.OfflineSettlementStatus,
|
||||
"settlement_mode": row.SettlementMode,
|
||||
}
|
||||
}
|
||||
|
||||
func appendPickupEvent(tx *gorm.DB, pickup model.AdminPickup, action string, adminID uint64, content, reason string, payload, before map[string]any) error {
|
||||
return processlog.Append(tx, processlog.Entry{
|
||||
BusinessType: processBusinessPickup, BusinessID: pickup.ID, Stage: "pickup", Action: action,
|
||||
ActorType: processlog.ActorAdmin, ActorID: adminID, TargetType: processlog.ActorUser,
|
||||
TargetID: &pickup.OwnerID, Content: content, Reason: reason, Payload: payload,
|
||||
StateBefore: before, StateAfter: pickupState(pickup),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ProcessEvents(ctx context.Context, pickupID uint64) ([]ProcessEventDTO, error) {
|
||||
var pickup model.AdminPickup
|
||||
if err := r.db.WithContext(ctx).First(&pickup, pickupID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := processlog.List(r.db.WithContext(ctx), processBusinessPickup, pickupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ProcessEventDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, ProcessEventDTO{
|
||||
ID: row.ID, Stage: row.Stage, Action: row.Action, ActorType: row.ActorType, ActorID: row.ActorID,
|
||||
ActorName: row.ActorName, TargetType: row.TargetType, TargetID: row.TargetID, TargetName: row.TargetName,
|
||||
Content: row.Content, Reason: row.Reason, Payload: row.Payload, AttachmentURLs: decodeStringList(row.AttachmentURLs),
|
||||
StateBefore: row.StateBefore, StateAfter: row.StateAfter, CreatedAt: row.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func decodeStringList(raw datatypes.JSON) []string {
|
||||
items := make([]string, 0)
|
||||
_ = json.Unmarshal(raw, &items)
|
||||
return items
|
||||
}
|
||||
@@ -105,6 +105,13 @@ func (r *Repository) Create(ctx context.Context, req CreateRequest, adminID uint
|
||||
if err := tx.Create(&pickup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_created", adminID, "管理员创建线下提号。", pickup.Remark, map[string]any{
|
||||
"listing_price_cent": pickup.ListingPriceCent, "profit_amount_cent": pickup.ProfitAmountCent,
|
||||
"account_source": pickup.AccountSource, "source_channel": pickup.SourceChannel,
|
||||
"settlement_mode": pickup.SettlementMode,
|
||||
}, map[string]any{}); err != nil {
|
||||
return err
|
||||
}
|
||||
createdID = pickup.ID
|
||||
|
||||
// 锁定商品:status=rented,从「已上架」列表移除,进入「已锁定」
|
||||
@@ -188,6 +195,7 @@ func (r *Repository) Complete(ctx context.Context, pickupID uint64, req Complete
|
||||
if pickup.Status != StatusPickingUp {
|
||||
return ErrPickupNotPickingUp
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil {
|
||||
@@ -273,6 +281,12 @@ func (r *Repository) Complete(ctx context.Context, pickupID uint64, req Complete
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_completed", adminID, "管理员完成提号。", pickup.CompleteRemark, map[string]any{
|
||||
"settle_amount_cent": pickup.SettleAmountCent, "profit_amount_cent": pickup.ProfitAmountCent,
|
||||
"settlement_mode": pickup.SettlementMode, "offline_settlement_status": pickup.OfflineSettlementStatus,
|
||||
}, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
@@ -316,6 +330,7 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
||||
pickup.OfflineSettlementStatus != OfflineSettlementStatusPending || pickup.SettleAmountCent <= 0 {
|
||||
return ErrOfflineSettlementCannotMark
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
pendingTotals, err := pendingPickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -345,6 +360,11 @@ func (r *Repository) MarkOfflineSettlement(ctx context.Context, pickupID uint64,
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_offline_settlement_confirmed", adminID, "确认已完成提号线下结算。", pickup.OfflineSettlementRemark, map[string]any{
|
||||
"settle_amount_cent": effectiveSettleAmountCent, "settled_adjustment_count": pendingTotals.Count,
|
||||
}, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bid := pickup.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
@@ -386,12 +406,18 @@ func (r *Repository) UpdateProfit(ctx context.Context, pickupID uint64, req Upda
|
||||
if pickup.Status != StatusPickingUp {
|
||||
return ErrPickupNotPickingUp
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
|
||||
oldProfitAmountCent := pickup.ProfitAmountCent
|
||||
pickup.ProfitAmountCent = req.ProfitAmountCent
|
||||
if err := tx.Model(&pickup).Update("profit_amount_cent", pickup.ProfitAmountCent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_profit_updated", adminID, "修改提号利润。", strings.TrimSpace(req.Reason), map[string]any{
|
||||
"old_profit_amount_cent": oldProfitAmountCent, "profit_amount_cent": pickup.ProfitAmountCent,
|
||||
}, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bid := pickup.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
@@ -431,6 +457,7 @@ func (r *Repository) CreateFinancialAdjustment(ctx context.Context, pickupID uin
|
||||
if pickup.Status != StatusCompleted {
|
||||
return ErrFinancialAdjustmentInvalid
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
|
||||
totals, err := pickupFinancialAdjustmentTotals(tx, pickup.ID)
|
||||
if err != nil {
|
||||
@@ -471,6 +498,13 @@ func (r *Repository) CreateFinancialAdjustment(ctx context.Context, pickupID uin
|
||||
if err := tx.Create(&adjustment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_financial_adjusted", adminID, "创建提号财务调整。", adjustment.Reason, map[string]any{
|
||||
"adjustment_id": adjustment.ID, "profit_delta_cent": adjustment.ProfitDeltaCent,
|
||||
"settle_delta_cent": adjustment.SettleDeltaCent, "settlement_mode": adjustment.SettlementMode,
|
||||
"adjustment_status": adjustment.Status,
|
||||
}, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 站内钱包的补款在创建调整时立即到账;扣款统一转待追回,避免余额被扣成负数。
|
||||
if pickup.SettlementMode == SettlementModeOwnerWallet && settleDeltaCent > 0 {
|
||||
@@ -559,6 +593,17 @@ func (r *Repository) SettleFinancialAdjustment(ctx context.Context, adjustmentID
|
||||
if err := tx.Save(&adjustment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var pickup model.AdminPickup
|
||||
if err := tx.First(&pickup, adjustment.PickupID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_adjustment_settled", adminID, "确认提号财务调整已处理。", adjustment.SettlementRemark, map[string]any{
|
||||
"adjustment_id": adjustment.ID, "profit_delta_cent": adjustment.ProfitDeltaCent,
|
||||
"settle_delta_cent": adjustment.SettleDeltaCent, "before_status": beforeStatus,
|
||||
"adjustment_status": adjustment.Status,
|
||||
}, pickupState(pickup)); err != nil {
|
||||
return err
|
||||
}
|
||||
bid := adjustment.ID
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
@@ -595,6 +640,7 @@ func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string,
|
||||
if pickup.Status != StatusPickingUp {
|
||||
return ErrPickupNotPickingUp
|
||||
}
|
||||
before := pickupState(pickup)
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, pickup.ListingID).Error; err != nil {
|
||||
return err
|
||||
@@ -609,6 +655,9 @@ func (r *Repository) Cancel(ctx context.Context, pickupID uint64, reason string,
|
||||
if err := tx.Save(&pickup).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPickupEvent(tx, pickup, "pickup_cancelled", adminID, "管理员取消提号。", reason, nil, before); err != nil {
|
||||
return err
|
||||
}
|
||||
// 取消提号:恢复上架,重新出现在商品管理「已上架」
|
||||
fromStatus := listing.Status
|
||||
listing.InTransaction = false
|
||||
|
||||
@@ -607,6 +607,7 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) {
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.AdminUser{},
|
||||
&model.GameAccount{},
|
||||
&model.RentalListing{},
|
||||
&model.ListingUpload{},
|
||||
@@ -615,6 +616,7 @@ func newPickupTestRepo(t *testing.T) (*Repository, *gorm.DB) {
|
||||
&model.WalletAccount{},
|
||||
&model.WalletLedger{},
|
||||
&model.AuditLog{},
|
||||
&model.ProcessEvent{},
|
||||
&model.Notification{},
|
||||
&model.ListingStatusEvent{},
|
||||
); err != nil {
|
||||
|
||||
@@ -87,6 +87,13 @@ func (s *Service) ListFinancialAdjustments(ctx context.Context, pickupID uint64)
|
||||
return s.repo.ListFinancialAdjustments(ctx, pickupID)
|
||||
}
|
||||
|
||||
func (s *Service) ProcessEvents(ctx context.Context, pickupID uint64) ([]ProcessEventDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ProcessEvents(ctx, pickupID)
|
||||
}
|
||||
|
||||
func (s *Service) SettleFinancialAdjustment(ctx context.Context, adjustmentID uint64, req FinancialAdjustmentSettlementRequest, adminID uint64, meta auditlog.Meta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
|
||||
Reference in New Issue
Block a user