快照订单号以及退款订单号
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s

This commit is contained in:
2026-04-23 17:14:10 +08:00
parent 4bde09837a
commit f177c26016
9 changed files with 94 additions and 4 deletions

View File

@@ -98,6 +98,10 @@ type AssetPackageResponse struct {
PackageID uint `json:"package_id" description:"套餐ID"` PackageID uint `json:"package_id" description:"套餐ID"`
PackageName string `json:"package_name" description:"套餐名称"` PackageName string `json:"package_name" description:"套餐名称"`
PackageType string `json:"package_type" description:"套餐类型formal/addon"` 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"` UsageType string `json:"usage_type" description:"使用类型single_card/device"`
Status int `json:"status" description:"状态0待生效 1生效中 2已用完 3已过期 4已失效"` Status int `json:"status" description:"状态0待生效 1生效中 2已用完 3已过期 4已失效"`
StatusName string `json:"status_name" description:"状态名称"` StatusName string `json:"status_name" description:"状态名称"`

View File

@@ -61,6 +61,9 @@ type PackageUsage struct {
gorm.Model gorm.Model
BaseModel `gorm:"embedded"` BaseModel `gorm:"embedded"`
OrderID uint `gorm:"column:order_id;index;not null;comment:订单ID" json:"order_id"` 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"` 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"` 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"` IotCardID uint `gorm:"column:iot_card_id;index;comment:IoT卡ID(单卡套餐时有值)" json:"iot_card_id"`

View File

@@ -714,6 +714,10 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa
PackageID: u.PackageID, PackageID: u.PackageID,
PackageName: pkgName, PackageName: pkgName,
PackageType: pkgType, PackageType: pkgType,
OrderID: u.OrderID,
OrderNo: u.OrderNo,
RefundID: u.RefundID,
RefundNo: u.RefundNo,
UsageType: u.UsageType, UsageType: u.UsageType,
Status: u.Status, Status: u.Status,
StatusName: packageStatusName(u.Status), StatusName: packageStatusName(u.Status),
@@ -791,6 +795,10 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui
PackageID: usage.PackageID, PackageID: usage.PackageID,
PackageName: pkgName, PackageName: pkgName,
PackageType: pkgType, PackageType: pkgType,
OrderID: usage.OrderID,
OrderNo: usage.OrderNo,
RefundID: usage.RefundID,
RefundNo: usage.RefundNo,
UsageType: usage.UsageType, UsageType: usage.UsageType,
Status: usage.Status, Status: usage.Status,
StatusName: packageStatusName(usage.Status), StatusName: packageStatusName(usage.Status),

View File

@@ -2011,6 +2011,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
Updater: order.Creator, Updater: order.Creator,
}, },
OrderID: order.ID, OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID, PackageID: pkg.ID,
PackageName: pkg.PackageName, PackageName: pkg.PackageName,
UsageType: order.OrderType, UsageType: order.OrderType,
@@ -2095,6 +2096,7 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
Updater: order.Creator, Updater: order.Creator,
}, },
OrderID: order.ID, OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID, PackageID: pkg.ID,
PackageName: pkg.PackageName, PackageName: pkg.PackageName,
UsageType: order.OrderType, UsageType: order.OrderType,

View File

@@ -491,12 +491,12 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *
} }
} }
// InvalidatePackagesForRefund 退款精准失效套餐 // InvalidatePackagesForRefund 退款精准失效套餐并写入退款快照
// 规则: // 规则:
// 1. 优先按 packageUsageID 精准失效单条套餐(且必须属于当前退款订单) // 1. 优先按 packageUsageID 精准失效单条套餐(且必须属于当前退款订单)
// 2. 未传 packageUsageID 时,按 orderID 失效该订单生成的套餐 // 2. 未传 packageUsageID 时,按 orderID 失效该订单生成的套餐
// 3. 若命中主套餐,级联失效其加油包,避免残留挂靠关系 // 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 { if orderID == 0 {
return errors.New(errors.CodeInvalidParam, "无效的订单ID") return errors.New(errors.CodeInvalidParam, "无效的订单ID")
} }
@@ -584,10 +584,20 @@ func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, ass
return nil 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). result := s.db.WithContext(ctx).
Model(&model.PackageUsage{}). Model(&model.PackageUsage{}).
Where("id IN ?", targetIDs). Where("id IN ?", targetIDs).
Update("status", constants.PackageUsageStatusInvalidated) Updates(updates)
if result.Error != nil { if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败") 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.String("asset_type", assetType),
zap.Uint("asset_id", assetID), zap.Uint("asset_id", assetID),
zap.Uint("order_id", orderID), zap.Uint("order_id", orderID),
zap.Uint("refund_id", refundID),
zap.String("refund_no", refundNo),
zap.Int("target_count", len(targetIDs)), zap.Int("target_count", len(targetIDs)),
zap.Int64("affected", result.RowsAffected), zap.Int64("affected", result.RowsAffected),
) )

View File

@@ -516,11 +516,13 @@ func (s *Service) handleAssetReset(ctx context.Context, refundID uint) {
// 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐) // 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐)
if s.packageActivationService != nil { 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{ fields := []zap.Field{
zap.String("asset_type", assetType), zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID), zap.Uint("asset_id", assetID),
zap.Uint("order_id", order.ID), zap.Uint("order_id", order.ID),
zap.Uint("refund_id", refund.ID),
zap.String("refund_no", refund.RefundNo),
zap.Error(err), zap.Error(err),
} }
if refund.PackageUsageID != nil { if refund.PackageUsageID != nil {

View File

@@ -510,6 +510,7 @@ func (h *AutoPurchaseHandler) activateMainPackage(
Updater: order.Creator, Updater: order.Creator,
}, },
OrderID: order.ID, OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID, PackageID: pkg.ID,
PackageName: pkg.PackageName, PackageName: pkg.PackageName,
UsageType: order.OrderType, UsageType: order.OrderType,
@@ -582,6 +583,7 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
Updater: order.Creator, Updater: order.Creator,
}, },
OrderID: order.ID, OrderID: order.ID,
OrderNo: order.OrderNo,
PackageID: pkg.ID, PackageID: pkg.ID,
PackageName: pkg.PackageName, PackageName: pkg.PackageName,
UsageType: order.OrderType, UsageType: order.OrderType,

View File

@@ -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;

View File

@@ -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
);