补全分销分账结算闭环并修复站点越权
- 权限落库:新增 000066 迁移,注册 11 个 distribution:* 权限并授权 super_admin/ops/finance - 越权修复:siteAccessAllowed 校验贯穿站点/规则/订单/展示/支付通道/结算全部按 ID 操作 - 分账闭环:SplitSettlement 按比例拆分分成基数,销售加价归卖出方,尾差记入 rounding_adjustment_cent 归平台;支付快照与退款重算均按拆分写入参与方金额 - 结算批次:创建时聚合周期内待结算订单生成明细并汇总金额,结算时校验明细与总额一致后联动置位财务快照 - 手工补数接口禁止覆盖已有快照并写入审计日志 - 杂项:二维码池校验站点启用状态且不吞真实 DB 错误;绑定支付通道校验商户配置存在;表探测改为 sync.Once 缓存
This commit is contained in:
@@ -6,9 +6,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,6 +23,10 @@ var (
|
||||
ErrInvalidPeriod = errors.New("invalid distribution settlement period")
|
||||
ErrSettlementBatchNotFound = errors.New("distribution settlement batch not found")
|
||||
ErrBatchAlreadySettled = errors.New("distribution settlement batch already settled")
|
||||
ErrSettlementBatchEmpty = errors.New("distribution settlement batch empty")
|
||||
ErrSettlementTotalMismatch = errors.New("distribution settlement total mismatch")
|
||||
ErrPaymentConfigNotFound = errors.New("distribution payment config not found")
|
||||
ErrOrderSnapshotExists = errors.New("distribution order snapshot exists")
|
||||
ErrSiteAccessDenied = errors.New("distribution site access denied")
|
||||
)
|
||||
|
||||
@@ -53,6 +59,34 @@ func (r *Repository) ScopeSiteID(ctx context.Context, adminID, requested uint64)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// ensureSiteAccess 校验管理员可访问指定站点:
|
||||
// 无成员记录(超级管理员语义)放行,有成员记录则必须命中。
|
||||
func (r *Repository) ensureSiteAccess(ctx context.Context, adminID, siteID uint64) error {
|
||||
if adminID == 0 {
|
||||
return nil
|
||||
}
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionSiteMember{}).
|
||||
Where("admin_user_id = ? AND status = 'active'", adminID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
if siteID == 0 {
|
||||
return ErrSiteAccessDenied
|
||||
}
|
||||
var allowed int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionSiteMember{}).
|
||||
Where("admin_user_id = ? AND site_id = ? AND status = 'active'", adminID, siteID).Count(&allowed).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if allowed == 0 {
|
||||
return ErrSiteAccessDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListSites(ctx context.Context) ([]SiteDTO, error) {
|
||||
var rows []model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).Order("id ASC").Find(&rows).Error; err != nil {
|
||||
@@ -65,7 +99,7 @@ func (r *Repository) ListSites(ctx context.Context) ([]SiteDTO, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindSite(ctx context.Context, id uint64) (*SiteDTO, error) {
|
||||
func (r *Repository) FindSite(ctx context.Context, adminID, id uint64) (*SiteDTO, error) {
|
||||
var row model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -73,6 +107,9 @@ func (r *Repository) FindSite(ctx context.Context, id uint64) (*SiteDTO, error)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := siteDTO(row)
|
||||
return &item, nil
|
||||
}
|
||||
@@ -101,7 +138,7 @@ func (r *Repository) CreateSite(ctx context.Context, req CreateSiteRequest) (*Si
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateSite(ctx context.Context, id uint64, req UpdateSiteRequest) (*SiteDTO, error) {
|
||||
func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req UpdateSiteRequest) (*SiteDTO, error) {
|
||||
var row model.DistributionSite
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -109,6 +146,9 @@ func (r *Repository) UpdateSite(ctx context.Context, id uint64, req UpdateSiteRe
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.SiteName != nil {
|
||||
row.SiteName = strings.TrimSpace(*req.SiteName)
|
||||
}
|
||||
@@ -137,11 +177,14 @@ func (r *Repository) UpdateSite(ctx context.Context, id uint64, req UpdateSiteRe
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListRules(ctx context.Context, siteID *uint64, mode string) ([]RuleDTO, error) {
|
||||
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")
|
||||
if siteID == nil {
|
||||
db = db.Where("site_id IS NULL")
|
||||
} else {
|
||||
if err := r.ensureSiteAccess(ctx, adminID, *siteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db = db.Where("site_id = ?", *siteID)
|
||||
}
|
||||
if mode = strings.TrimSpace(mode); mode != "" {
|
||||
@@ -159,6 +202,11 @@ func (r *Repository) ListRules(ctx context.Context, siteID *uint64, mode string)
|
||||
}
|
||||
|
||||
func (r *Repository) CreateRule(ctx context.Context, adminID uint64, req CreateRuleRequest) (*RuleDTO, error) {
|
||||
if req.SiteID != nil {
|
||||
if err := r.ensureSiteAccess(ctx, adminID, *req.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
version := 1
|
||||
var maxVersion int
|
||||
db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}).Where("mode = ?", req.Mode)
|
||||
@@ -197,7 +245,7 @@ func (r *Repository) CreateRule(ctx context.Context, adminID uint64, req CreateR
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateRuleStatus(ctx context.Context, id uint64, status string) (*RuleDTO, error) {
|
||||
func (r *Repository) UpdateRuleStatus(ctx context.Context, adminID, id uint64, status string) (*RuleDTO, error) {
|
||||
var row model.DistributionRuleVersion
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -205,6 +253,11 @@ func (r *Repository) UpdateRuleStatus(ctx context.Context, id uint64, status str
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if row.SiteID != nil {
|
||||
if err := r.ensureSiteAccess(ctx, adminID, *row.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
row.Status = strings.TrimSpace(status)
|
||||
if err := r.db.WithContext(ctx).Save(&row).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -253,22 +306,25 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
|
||||
return nil, err
|
||||
}
|
||||
items := make([]DistributionOrderDTO, 0)
|
||||
selectSQL := "ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, ro.created_at"
|
||||
selectSQL := "ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at"
|
||||
if err := base.Select(selectSQL).Order("ro.id DESC").Offset((page - 1) * size).Limit(size).Scan(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DistributionOrderPage{Items: items, Total: total, Page: page, PageSize: size}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindOrder(ctx context.Context, id uint64) (*DistributionOrderDTO, error) {
|
||||
func (r *Repository) FindOrder(ctx context.Context, adminID, id uint64) (*DistributionOrderDTO, error) {
|
||||
var item DistributionOrderDTO
|
||||
err := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").Select("ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, ro.created_at").Where("ro.id = ?", id).Scan(&item).Error
|
||||
err := r.db.WithContext(ctx).Table("rental_orders ro").Joins("JOIN distribution_order_financials f ON f.order_id = ro.id").Select("ro.id AS order_id, ro.order_no, ro.distribution_site_id AS site_id, ro.listing_id, ro.account_id, ro.owner_id, ro.renter_id, ro.status, ro.handoff_status, ro.refund_status, ro.settlement_status, ro.distribution_mode, f.gross_paid_amount_cent, f.refund_amount_cent, f.net_paid_amount_cent, f.payment_fee_cent, f.service_fee_cent, f.markup_amount_cent, f.tax_amount_cent, f.split_base_amount_cent, f.rounding_adjustment_cent, ro.created_at").Where("ro.id = ?", id).Scan(&item).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.OrderID == 0 {
|
||||
return nil, ErrOrderNotFound
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, item.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
@@ -288,7 +344,7 @@ func (r *Repository) ListAccountDisplays(ctx context.Context, siteID uint64, sta
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateAccountDisplay(ctx context.Context, id uint64, status string) (*AccountDisplayDTO, error) {
|
||||
func (r *Repository) UpdateAccountDisplay(ctx context.Context, adminID, id uint64, status string) (*AccountDisplayDTO, error) {
|
||||
var row model.DistributionSiteAccount
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -296,6 +352,9 @@ func (r *Repository) UpdateAccountDisplay(ctx context.Context, id uint64, status
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.DisplayStatus = strings.TrimSpace(status)
|
||||
if err := r.db.WithContext(ctx).Save(&row).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -317,6 +376,14 @@ func (r *Repository) ListPaymentChannels(ctx context.Context, siteID uint64) ([]
|
||||
}
|
||||
|
||||
func (r *Repository) BindPaymentChannel(ctx context.Context, siteID uint64, req BindPaymentChannelRequest) (*PaymentChannelDTO, error) {
|
||||
var configCount int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.PaymentMerchantConfig{}).
|
||||
Where("id = ?", req.ConfigID).Count(&configCount).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configCount == 0 {
|
||||
return nil, ErrPaymentConfigNotFound
|
||||
}
|
||||
row := model.DistributionSitePaymentChannel{SiteID: siteID, ConfigID: req.ConfigID, Priority: req.Priority, Status: "active"}
|
||||
if err := r.db.WithContext(ctx).Where("site_id = ? AND config_id = ?", siteID, req.ConfigID).FirstOrCreate(&row).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -324,15 +391,18 @@ func (r *Repository) BindPaymentChannel(ctx context.Context, siteID uint64, req
|
||||
return &PaymentChannelDTO{ID: row.ID, SiteID: row.SiteID, ConfigID: row.ConfigID, Priority: row.Priority, Status: row.Status}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UnbindPaymentChannel(ctx context.Context, id uint64) error {
|
||||
result := r.db.WithContext(ctx).Delete(&model.DistributionSitePaymentChannel{}, id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
func (r *Repository) UnbindPaymentChannel(ctx context.Context, adminID, id uint64) error {
|
||||
var row model.DistributionSitePaymentChannel
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrPaymentChannelNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrPaymentChannelNotFound
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.SiteID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return r.db.WithContext(ctx).Delete(&row).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ListSettlementBatches(ctx context.Context, siteID uint64) ([]SettlementBatchDTO, error) {
|
||||
@@ -351,34 +421,107 @@ func (r *Repository) CreateSettlementBatch(ctx context.Context, adminID uint64,
|
||||
if req.PeriodEnd.Before(req.PeriodStart) {
|
||||
return nil, ErrInvalidPeriod
|
||||
}
|
||||
row := model.DistributionSettlementBatch{SiteID: req.SiteID, BatchNo: "DS" + time.Now().Format("20060102150405.000000000"), PeriodStart: req.PeriodStart, PeriodEnd: req.PeriodEnd, Status: "draft", CreatedBy: &adminID}
|
||||
if err := r.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||||
if err := r.ensureSiteAccess(ctx, adminID, req.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SettlementBatchDTO{ID: row.ID, SiteID: row.SiteID, BatchNo: row.BatchNo, PeriodStart: row.PeriodStart, PeriodEnd: row.PeriodEnd, TotalAmountCent: row.TotalAmountCent, Status: row.Status}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) MarkSettlementBatch(ctx context.Context, id uint64) (*SettlementBatchDTO, error) {
|
||||
var row model.DistributionSettlementBatch
|
||||
if err := r.db.WithContext(ctx).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrSettlementBatchNotFound
|
||||
if req.SiteID == 0 {
|
||||
return nil, ErrInvalidPeriod
|
||||
}
|
||||
var batch model.DistributionSettlementBatch
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
batch = model.DistributionSettlementBatch{SiteID: req.SiteID, BatchNo: "DS" + time.Now().Format("20060102150405.000000000"), PeriodStart: req.PeriodStart, PeriodEnd: req.PeriodEnd, Status: "draft", CreatedBy: &adminID}
|
||||
if err := tx.Create(&batch).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 聚合周期内尚未进入任何批次的待结算订单财务快照。
|
||||
var financials []model.DistributionOrderFinancial
|
||||
if err := tx.Where("site_id = ? AND settlement_status = 'pending'", req.SiteID).
|
||||
Where("created_at >= ? AND created_at < ?", req.PeriodStart, req.PeriodEnd).
|
||||
Where("NOT EXISTS (SELECT 1 FROM distribution_settlement_items i WHERE i.order_id = distribution_order_financials.order_id)").
|
||||
Order("id ASC").Find(&financials).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
total := int64(0)
|
||||
for _, financial := range financials {
|
||||
var parties []model.DistributionOrderParty
|
||||
if err := tx.Where("order_id = ?", financial.OrderID).Find(&parties).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, party := range parties {
|
||||
item := model.DistributionSettlementItem{BatchID: batch.ID, OrderID: financial.OrderID, PartyID: party.ID, AmountCent: party.IncomeAmountCent, Status: "pending"}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
total += party.IncomeAmountCent
|
||||
}
|
||||
}
|
||||
batch.TotalAmountCent = total
|
||||
return tx.Model(&model.DistributionSettlementBatch{}).Where("id = ?", batch.ID).
|
||||
Update("total_amount_cent", total).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.Status == "settled" {
|
||||
return nil, ErrBatchAlreadySettled
|
||||
}
|
||||
now := time.Now()
|
||||
row.Status = "settled"
|
||||
row.SettledAt = &now
|
||||
if err := r.db.WithContext(ctx).Save(&row).Error; err != nil {
|
||||
return &SettlementBatchDTO{ID: batch.ID, SiteID: batch.SiteID, BatchNo: batch.BatchNo, PeriodStart: batch.PeriodStart, PeriodEnd: batch.PeriodEnd, TotalAmountCent: batch.TotalAmountCent, Status: batch.Status}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) MarkSettlementBatch(ctx context.Context, adminID, id uint64) (*SettlementBatchDTO, error) {
|
||||
var row model.DistributionSettlementBatch
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrSettlementBatchNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, row.SiteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if row.Status == "settled" {
|
||||
return ErrBatchAlreadySettled
|
||||
}
|
||||
var items []model.DistributionSettlementItem
|
||||
if err := tx.Where("batch_id = ?", row.ID).Find(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return ErrSettlementBatchEmpty
|
||||
}
|
||||
var total int64
|
||||
for _, item := range items {
|
||||
total += item.AmountCent
|
||||
}
|
||||
if total != row.TotalAmountCent {
|
||||
return ErrSettlementTotalMismatch
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.DistributionSettlementItem{}).Where("batch_id = ?", row.ID).
|
||||
Update("status", "settled").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.DistributionOrderFinancial{}).
|
||||
Where("order_id IN (?)", tx.Model(&model.DistributionSettlementItem{}).
|
||||
Select("order_id").Where("batch_id = ?", row.ID)).
|
||||
Updates(map[string]any{"settlement_status": "settled", "settled_at": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
row.Status = "settled"
|
||||
row.SettledAt = &now
|
||||
return tx.Save(&row).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SettlementBatchDTO{ID: row.ID, SiteID: row.SiteID, BatchNo: row.BatchNo, PeriodStart: row.PeriodStart, PeriodEnd: row.PeriodEnd, TotalAmountCent: row.TotalAmountCent, Status: row.Status}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateFinancialSnapshot(ctx context.Context, orderID uint64, req CreateFinancialSnapshotRequest) (*DistributionOrderDTO, error) {
|
||||
func (r *Repository) CreateFinancialSnapshot(ctx context.Context, adminID, orderID uint64, req CreateFinancialSnapshotRequest) (*DistributionOrderDTO, error) {
|
||||
if req.SiteID == 0 || orderID == 0 {
|
||||
return nil, ErrInvalidAmount
|
||||
}
|
||||
if err := r.ensureSiteAccess(ctx, adminID, req.SiteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rule model.DistributionRuleVersion
|
||||
if err := r.db.WithContext(ctx).First(&rule, req.RuleVersionID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -386,23 +529,33 @@ func (r *Repository) CreateFinancialSnapshot(ctx context.Context, orderID uint64
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if req.SiteID == 0 || orderID == 0 {
|
||||
return nil, ErrInvalidAmount
|
||||
var orderExists int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.RentalOrder{}).Where("id = ?", orderID).Count(&orderExists).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderExists == 0 {
|
||||
return nil, ErrOrderNotFound
|
||||
}
|
||||
// 手工补数只允许创建,不允许覆盖支付事务写入的真实快照。
|
||||
var existing int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.DistributionOrderFinancial{}).
|
||||
Where("order_id = ?", orderID).Count(&existing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing > 0 {
|
||||
return nil, ErrOrderSnapshotExists
|
||||
}
|
||||
partyInputs := make([]PartyProfitInput, 0, len(req.Parties))
|
||||
for _, party := range req.Parties {
|
||||
partyInputs = append(partyInputs, PartyProfitInput{PartyRole: party.PartyRole, IncomeAmountCent: party.IncomeAmountCent, CostAmountCent: party.CostAmountCent})
|
||||
}
|
||||
result, err := CalculateSettlement(Rule{ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, PaymentFeeBPS: rule.PaymentFeeBPS, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS}, SettlementInput{GrossPaidAmountCent: req.GrossPaidAmountCent, RefundAmountCent: req.RefundAmountCent, MarkupAmountCent: req.MarkupAmountCent, GrossProfitByParty: partyInputs})
|
||||
result, err := CalculateSettlement(Rule{Mode: rule.Mode, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, PaymentFeeBPS: rule.PaymentFeeBPS, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS}, SettlementInput{GrossPaidAmountCent: req.GrossPaidAmountCent, RefundAmountCent: req.RefundAmountCent, MarkupAmountCent: req.MarkupAmountCent, GrossProfitByParty: partyInputs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, SettlementStatus: "pending"}
|
||||
if err := tx.Where("order_id = ?", orderID).Assign(financial).FirstOrCreate(&financial).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("order_id = ?", orderID).Delete(&model.DistributionOrderParty{}).Error; err != nil {
|
||||
financial := model.DistributionOrderFinancial{OrderID: orderID, SiteID: req.SiteID, RuleVersionID: req.RuleVersionID, GrossPaidAmountCent: result.GrossPaidAmountCent, RefundAmountCent: result.RefundAmountCent, NetPaidAmountCent: result.NetPaidAmountCent, PaymentFeeBPS: rule.PaymentFeeBPS, PaymentFeeCent: result.PaymentFeeCent, ServiceFeeValue: rule.ServiceFeeValue, ServiceFeeUnit: rule.ServiceFeeUnit, ServiceFeeCent: result.ServiceFeeCent, MarkupAmountCent: result.MarkupAmountCent, TaxEnabled: rule.TaxEnabled, TaxRateBPS: rule.TaxRateBPS, TaxAmountCent: result.TaxAmountCent, SplitBaseAmountCent: result.SplitBaseAmountCent, RoundingAdjustmentCent: SplitRemainderCent(result.SplitBaseAmountCent, SplitSettlement(Rule{Mode: rule.Mode}, result.SplitBaseAmountCent, result.MarkupAmountCent)), SettlementStatus: "pending"}
|
||||
if err := tx.Create(&financial).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, party := range req.Parties {
|
||||
@@ -416,10 +569,16 @@ func (r *Repository) CreateFinancialSnapshot(ctx context.Context, orderID uint64
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{"distribution_site_id": req.SiteID, "distribution_rule_version_id": req.RuleVersionID}).Error
|
||||
if err := tx.Model(&model.RentalOrder{}).Where("id = ?", orderID).Updates(map[string]any{"distribution_site_id": req.SiteID, "distribution_rule_version_id": req.RuleVersionID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin", ActorID: adminID, Action: "distribution_financial_snapshot_create", BizType: "distribution_order", BizID: &orderID,
|
||||
Detail: map[string]any{"site_id": req.SiteID, "rule_version_id": req.RuleVersionID, "gross_paid_amount_cent": req.GrossPaidAmountCent, "refund_amount_cent": req.RefundAmountCent, "parties": len(req.Parties)},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindOrder(ctx, orderID)
|
||||
return r.FindOrder(ctx, adminID, orderID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user