From f177c26016a339fe25a532c777bb7aee053f6390 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 23 Apr 2026 17:14:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=AB=E7=85=A7=E8=AE=A2=E5=8D=95=E5=8F=B7?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E9=80=80=E6=AC=BE=E8=AE=A2=E5=8D=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/model/dto/asset_dto.go | 4 ++ internal/model/package.go | 3 ++ internal/service/asset/service.go | 8 +++ internal/service/order/service.go | 2 + .../service/package/activation_service.go | 18 +++++-- internal/service/refund/service.go | 4 +- internal/task/auto_purchase.go | 2 + ..._refund_snapshot_to_package_usage.down.sql | 6 +++ ...er_refund_snapshot_to_package_usage.up.sql | 51 +++++++++++++++++++ 9 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 migrations/000134_add_order_refund_snapshot_to_package_usage.down.sql create mode 100644 migrations/000134_add_order_refund_snapshot_to_package_usage.up.sql diff --git a/internal/model/dto/asset_dto.go b/internal/model/dto/asset_dto.go index 291413e..c66b38c 100644 --- a/internal/model/dto/asset_dto.go +++ b/internal/model/dto/asset_dto.go @@ -98,6 +98,10 @@ type AssetPackageResponse struct { PackageID uint `json:"package_id" description:"套餐ID"` PackageName string `json:"package_name" description:"套餐名称"` PackageType string `json:"package_type" description:"套餐类型:formal/addon"` + OrderID uint `json:"order_id" description:"关联订单ID(无订单分配时为0)"` + OrderNo string `json:"order_no,omitempty" description:"订单号快照(无订单分配时为空)"` + RefundID *uint `json:"refund_id,omitempty" description:"退款主键ID快照(未退款时为空)"` + RefundNo string `json:"refund_no,omitempty" description:"退款单号快照(未退款时为空)"` UsageType string `json:"usage_type" description:"使用类型:single_card/device"` Status int `json:"status" description:"状态:0待生效 1生效中 2已用完 3已过期 4已失效"` StatusName string `json:"status_name" description:"状态名称"` diff --git a/internal/model/package.go b/internal/model/package.go index 45b5de1..2bec3ad 100644 --- a/internal/model/package.go +++ b/internal/model/package.go @@ -61,6 +61,9 @@ type PackageUsage struct { gorm.Model BaseModel `gorm:"embedded"` OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"` + OrderNo string `gorm:"column:order_no;type:varchar(30);not null;default:'';comment:订单号快照(从订单复制)" json:"order_no"` + RefundID *uint `gorm:"column:refund_id;index;comment:退款单ID快照(退款后写入)" json:"refund_id,omitempty"` + RefundNo string `gorm:"column:refund_no;type:varchar(50);not null;default:'';comment:退款单号快照(退款后写入)" json:"refund_no"` PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"` UsageType string `gorm:"column:usage_type;type:varchar(20);not null;comment:使用类型 single_card-单卡套餐 device-设备级套餐" json:"usage_type"` IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"` diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index 924c61a..7800a8f 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -714,6 +714,10 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa PackageID: u.PackageID, PackageName: pkgName, PackageType: pkgType, + OrderID: u.OrderID, + OrderNo: u.OrderNo, + RefundID: u.RefundID, + RefundNo: u.RefundNo, UsageType: u.UsageType, Status: u.Status, StatusName: packageStatusName(u.Status), @@ -791,6 +795,10 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui PackageID: usage.PackageID, PackageName: pkgName, PackageType: pkgType, + OrderID: usage.OrderID, + OrderNo: usage.OrderNo, + RefundID: usage.RefundID, + RefundNo: usage.RefundNo, UsageType: usage.UsageType, Status: usage.Status, StatusName: packageStatusName(usage.Status), diff --git a/internal/service/order/service.go b/internal/service/order/service.go index bfdf1d9..6624370 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -2011,6 +2011,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m Updater: order.Creator, }, OrderID: order.ID, + OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, @@ -2095,6 +2096,7 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order * Updater: order.Creator, }, OrderID: order.ID, + OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, diff --git a/internal/service/package/activation_service.go b/internal/service/package/activation_service.go index f1e60b5..dd9b479 100644 --- a/internal/service/package/activation_service.go +++ b/internal/service/package/activation_service.go @@ -491,12 +491,12 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx * } } -// InvalidatePackagesForRefund 退款精准失效套餐 +// InvalidatePackagesForRefund 退款精准失效套餐并写入退款快照 // 规则: // 1. 优先按 packageUsageID 精准失效单条套餐(且必须属于当前退款订单) // 2. 未传 packageUsageID 时,按 orderID 失效该订单生成的套餐 // 3. 若命中主套餐,级联失效其加油包,避免残留挂靠关系 -func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, assetType string, assetID uint, orderID uint, packageUsageID *uint) error { +func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, assetType string, assetID uint, orderID uint, refundID uint, refundNo string, packageUsageID *uint) error { if orderID == 0 { return errors.New(errors.CodeInvalidParam, "无效的订单ID") } @@ -584,10 +584,20 @@ func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, ass return nil } + updates := map[string]any{ + "status": constants.PackageUsageStatusInvalidated, + } + if refundID > 0 { + updates["refund_id"] = refundID + } + if refundNo != "" { + updates["refund_no"] = refundNo + } + result := s.db.WithContext(ctx). Model(&model.PackageUsage{}). Where("id IN ?", targetIDs). - Update("status", constants.PackageUsageStatusInvalidated) + Updates(updates) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败") } @@ -596,6 +606,8 @@ func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, ass zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Uint("order_id", orderID), + zap.Uint("refund_id", refundID), + zap.String("refund_no", refundNo), zap.Int("target_count", len(targetIDs)), zap.Int64("affected", result.RowsAffected), ) diff --git a/internal/service/refund/service.go b/internal/service/refund/service.go index 4c5aab6..458daef 100644 --- a/internal/service/refund/service.go +++ b/internal/service/refund/service.go @@ -516,11 +516,13 @@ func (s *Service) handleAssetReset(ctx context.Context, refundID uint) { // 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐) if s.packageActivationService != nil { - if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.PackageUsageID); err != nil { + if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.ID, refund.RefundNo, refund.PackageUsageID); err != nil { fields := []zap.Field{ zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Uint("order_id", order.ID), + zap.Uint("refund_id", refund.ID), + zap.String("refund_no", refund.RefundNo), zap.Error(err), } if refund.PackageUsageID != nil { diff --git a/internal/task/auto_purchase.go b/internal/task/auto_purchase.go index b027433..5753544 100644 --- a/internal/task/auto_purchase.go +++ b/internal/task/auto_purchase.go @@ -510,6 +510,7 @@ func (h *AutoPurchaseHandler) activateMainPackage( Updater: order.Creator, }, OrderID: order.ID, + OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, @@ -582,6 +583,7 @@ func (h *AutoPurchaseHandler) activateAddonPackage( Updater: order.Creator, }, OrderID: order.ID, + OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, diff --git a/migrations/000134_add_order_refund_snapshot_to_package_usage.down.sql b/migrations/000134_add_order_refund_snapshot_to_package_usage.down.sql new file mode 100644 index 0000000..fbe9e28 --- /dev/null +++ b/migrations/000134_add_order_refund_snapshot_to_package_usage.down.sql @@ -0,0 +1,6 @@ +DROP INDEX IF EXISTS idx_package_usage_refund_id; + +ALTER TABLE tb_package_usage + DROP COLUMN IF EXISTS refund_no, + DROP COLUMN IF EXISTS refund_id, + DROP COLUMN IF EXISTS order_no; diff --git a/migrations/000134_add_order_refund_snapshot_to_package_usage.up.sql b/migrations/000134_add_order_refund_snapshot_to_package_usage.up.sql new file mode 100644 index 0000000..e63f66e --- /dev/null +++ b/migrations/000134_add_order_refund_snapshot_to_package_usage.up.sql @@ -0,0 +1,51 @@ +-- 为 tb_package_usage 新增订单号与退款快照字段 +-- 目标:资产套餐接口直接读快照,不联表 tb_order/tb_refund_request +ALTER TABLE tb_package_usage + ADD COLUMN IF NOT EXISTS order_no VARCHAR(30) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS refund_id BIGINT, + ADD COLUMN IF NOT EXISTS refund_no VARCHAR(50) NOT NULL DEFAULT ''; + +COMMENT ON COLUMN tb_package_usage.order_no IS '订单号快照(从订单复制)'; +COMMENT ON COLUMN tb_package_usage.refund_id IS '退款单ID快照(退款后写入)'; +COMMENT ON COLUMN tb_package_usage.refund_no IS '退款单号快照(退款后写入)'; + +CREATE INDEX IF NOT EXISTS idx_package_usage_refund_id ON tb_package_usage(refund_id); + +-- 存量数据回填订单号:通过 order_id 关联订单表 +UPDATE tb_package_usage pu +SET order_no = o.order_no +FROM tb_order o +WHERE pu.order_id = o.id + AND pu.order_id != 0 + AND (pu.order_no = '' OR pu.order_no IS NULL); + +-- 存量数据回填退款快照(优先 package_usage_id 精准映射) +UPDATE tb_package_usage pu +SET refund_id = r.id, + refund_no = r.refund_no +FROM tb_refund_request r +WHERE r.package_usage_id IS NOT NULL + AND pu.id = r.package_usage_id + AND ( + pu.refund_id IS NULL + OR pu.refund_no = '' + OR pu.refund_no IS NULL + ); + +-- 兜底回填:按 order_id 取最新退款单(兼容历史未填 package_usage_id 的场景) +WITH latest_refund AS ( + SELECT DISTINCT ON (order_id) id, order_id, refund_no + FROM tb_refund_request + ORDER BY order_id, id DESC +) +UPDATE tb_package_usage pu +SET refund_id = lr.id, + refund_no = lr.refund_no +FROM latest_refund lr +WHERE pu.order_id = lr.order_id + AND pu.order_id != 0 + AND ( + pu.refund_id IS NULL + OR pu.refund_no = '' + OR pu.refund_no IS NULL + );