修复退款审核详情关联与空值展示
This commit is contained in:
@@ -1,16 +1,304 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
paylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ListRefund(ctx *gin.Context) { common.ListResource(ctx, &models.PaymentRefund{}) }
|
||||
func GetRefund(ctx *gin.Context) { common.GetResource(ctx, &models.PaymentRefund{}) }
|
||||
// paymentRefundDisplay 在退款事实之外附加财务审核所需的可读关系名称。
|
||||
type paymentRefundDisplay struct {
|
||||
models.PaymentRefund
|
||||
BusinessDisplayName string `json:"business_display_name"`
|
||||
UserDisplayName string `json:"user_display_name"`
|
||||
ReviewerDisplayName string `json:"reviewer_display_name"`
|
||||
RefundProductSummary string `json:"refund_product_summary"`
|
||||
}
|
||||
|
||||
// refundEcProductSnapshot 是商城退款读取成交商品名称所需的最小快照结构。
|
||||
type refundEcProductSnapshot struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ListRefund 返回退款列表,并批量解析原订单号和申请用户姓名。
|
||||
func ListRefund(ctx *gin.Context) {
|
||||
page, size := common.PageSize(ctx)
|
||||
var refunds []models.PaymentRefund
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(
|
||||
ctx,
|
||||
common.ActiveRecords(impl.DBService.Model(&models.PaymentRefund{})),
|
||||
&models.PaymentRefund{},
|
||||
)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&refunds).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
displays, err := buildPaymentRefundDisplays(refunds)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(displays)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
|
||||
}
|
||||
|
||||
// GetRefund 返回退款详情,并附加与列表一致的可读关系名称。
|
||||
func GetRefund(ctx *gin.Context) {
|
||||
var refund models.PaymentRefund
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ?", ctx.Param("identity")).First(&refund).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
displays, err := buildPaymentRefundDisplays([]models.PaymentRefund{refund})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(displays[0])
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
// buildPaymentRefundDisplays 批量读取当前页引用,避免退款列表逐行查询。
|
||||
func buildPaymentRefundDisplays(refunds []models.PaymentRefund) ([]paymentRefundDisplay, error) {
|
||||
gasorderIdentities := make([]string, 0, len(refunds))
|
||||
ecOrderIdentities := make([]string, 0, len(refunds))
|
||||
userIdentities := make([]string, 0, len(refunds))
|
||||
for _, refund := range refunds {
|
||||
switch refund.BusinessType {
|
||||
case "gasorder":
|
||||
gasorderIdentities = append(gasorderIdentities, refund.BusinessIdentity)
|
||||
case "ec_order":
|
||||
ecOrderIdentities = append(ecOrderIdentities, refund.BusinessIdentity)
|
||||
}
|
||||
userIdentities = append(userIdentities, refund.UserIdentity)
|
||||
}
|
||||
gasorderNames := map[string]string{}
|
||||
if len(gasorderIdentities) > 0 {
|
||||
var orders []models.GasorderBasic
|
||||
if err := common.ActiveRecords(impl.DBService).Select("identity", "order_no").
|
||||
Where("identity IN ?", gasorderIdentities).Find(&orders).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, order := range orders {
|
||||
gasorderNames[order.Identity] = order.OrderNo
|
||||
}
|
||||
}
|
||||
ecOrderNames := map[string]string{}
|
||||
if len(ecOrderIdentities) > 0 {
|
||||
var orders []models.EcOrder
|
||||
if err := common.ActiveRecords(impl.DBService).Select("identity", "order_no").
|
||||
Where("identity IN ?", ecOrderIdentities).Find(&orders).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, order := range orders {
|
||||
ecOrderNames[order.Identity] = order.OrderNo
|
||||
}
|
||||
}
|
||||
userNames := map[string]string{}
|
||||
if len(userIdentities) > 0 {
|
||||
var users []models.UserAccount
|
||||
if err := common.ActiveRecords(impl.DBService).Select("identity", "name").
|
||||
Where("identity IN ?", userIdentities).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
userNames[user.Identity] = user.Name
|
||||
}
|
||||
}
|
||||
reviewerNames := map[string]string{}
|
||||
reviewerIdentities := make([]string, 0, len(refunds))
|
||||
for _, refund := range refunds {
|
||||
if refund.ReviewerIdentity != "" {
|
||||
reviewerIdentities = append(reviewerIdentities, refund.ReviewerIdentity)
|
||||
}
|
||||
}
|
||||
if len(reviewerIdentities) > 0 {
|
||||
var reviewers []models.PlatformAccount
|
||||
// 历史审核人即使已经归档也需要保留可读名称,因此不套用活动记录过滤。
|
||||
if err := impl.DBService.Model(&models.PlatformAccount{}).
|
||||
Select("identity", "display_name", "username").
|
||||
Where("identity IN ?", reviewerIdentities).Find(&reviewers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, reviewer := range reviewers {
|
||||
reviewerNames[reviewer.Identity] = readableRefundName(
|
||||
strings.TrimSpace(reviewer.DisplayName),
|
||||
strings.TrimSpace(reviewer.Username),
|
||||
)
|
||||
}
|
||||
}
|
||||
productSummaries, err := loadPaymentRefundProductSummaries(refunds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
displays := make([]paymentRefundDisplay, 0, len(refunds))
|
||||
for _, refund := range refunds {
|
||||
displays = append(displays, paymentRefundDisplay{
|
||||
PaymentRefund: refund,
|
||||
BusinessDisplayName: refundBusinessDisplayName(refund, gasorderNames, ecOrderNames),
|
||||
UserDisplayName: readableRefundName(userNames[refund.UserIdentity], "用户记录已失效"),
|
||||
ReviewerDisplayName: refundReviewerDisplayName(refund.ReviewerIdentity, reviewerNames),
|
||||
RefundProductSummary: productSummaries[refund.ID],
|
||||
})
|
||||
}
|
||||
return displays, nil
|
||||
}
|
||||
|
||||
// refundReviewerDisplayName 区分尚未审核、历史账号失效和正常审核人三种状态。
|
||||
func refundReviewerDisplayName(identity string, names map[string]string) string {
|
||||
if strings.TrimSpace(identity) == "" {
|
||||
return "尚未审核"
|
||||
}
|
||||
return readableRefundName(names[identity], "审核人记录已失效")
|
||||
}
|
||||
|
||||
// loadPaymentRefundProductSummaries 批量读取退款明细及原订单项,生成退款商品名称与数量。
|
||||
func loadPaymentRefundProductSummaries(refunds []models.PaymentRefund) (map[uint64]string, error) {
|
||||
refundIDs := make([]uint64, 0, len(refunds))
|
||||
refundTypes := make(map[uint64]string, len(refunds))
|
||||
for _, refund := range refunds {
|
||||
refundIDs = append(refundIDs, refund.ID)
|
||||
refundTypes[refund.ID] = refund.BusinessType
|
||||
}
|
||||
var refundItems []models.PaymentRefundItem
|
||||
if len(refundIDs) > 0 {
|
||||
if err := impl.DBService.Where("payment_refund_id IN ?", refundIDs).
|
||||
Order("payment_refund_id asc, id asc").Find(&refundItems).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
gasorderItemIdentities := make([]string, 0, len(refundItems))
|
||||
ecOrderItemIdentities := make([]string, 0, len(refundItems))
|
||||
for _, item := range refundItems {
|
||||
if refundTypes[item.PaymentRefundID] == "gasorder" {
|
||||
gasorderItemIdentities = append(gasorderItemIdentities, item.OrderItemIdentity)
|
||||
} else if refundTypes[item.PaymentRefundID] == "ec_order" {
|
||||
ecOrderItemIdentities = append(ecOrderItemIdentities, item.OrderItemIdentity)
|
||||
}
|
||||
}
|
||||
gasorderItems := map[string]models.GasorderItem{}
|
||||
productInfoIDs := make([]uint64, 0, len(gasorderItemIdentities))
|
||||
if len(gasorderItemIdentities) > 0 {
|
||||
var items []models.GasorderItem
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity IN ?", gasorderItemIdentities).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range items {
|
||||
gasorderItems[item.Identity] = item
|
||||
productInfoIDs = append(productInfoIDs, item.ProductInfoID)
|
||||
}
|
||||
}
|
||||
productNames := map[uint64]string{}
|
||||
if len(productInfoIDs) > 0 {
|
||||
var products []models.ProductInfo
|
||||
if err := common.ActiveRecords(impl.DBService).Select("id", "name").
|
||||
Where("id IN ?", productInfoIDs).Find(&products).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, product := range products {
|
||||
productNames[product.ID] = product.Name
|
||||
}
|
||||
}
|
||||
ecOrderItems := map[string]models.EcOrderItem{}
|
||||
if len(ecOrderItemIdentities) > 0 {
|
||||
var items []models.EcOrderItem
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity IN ?", ecOrderItemIdentities).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range items {
|
||||
ecOrderItems[item.Identity] = item
|
||||
}
|
||||
}
|
||||
labelsByRefund := make(map[uint64][]string, len(refunds))
|
||||
for _, refundItem := range refundItems {
|
||||
name := ""
|
||||
switch refundTypes[refundItem.PaymentRefundID] {
|
||||
case "gasorder":
|
||||
item := gasorderItems[refundItem.OrderItemIdentity]
|
||||
name = strings.TrimSpace(productNames[item.ProductInfoID])
|
||||
if name == "" {
|
||||
name = readableRefundName(item.ProductTypeName, item.ProductCode)
|
||||
}
|
||||
case "ec_order":
|
||||
item := ecOrderItems[refundItem.OrderItemIdentity]
|
||||
var snapshot refundEcProductSnapshot
|
||||
if json.Unmarshal([]byte(item.ProductSnapshot), &snapshot) == nil {
|
||||
name = strings.TrimSpace(snapshot.Name)
|
||||
}
|
||||
}
|
||||
labelsByRefund[refundItem.PaymentRefundID] = append(
|
||||
labelsByRefund[refundItem.PaymentRefundID],
|
||||
refundProductLabel(name, refundItem.Quantity),
|
||||
)
|
||||
}
|
||||
summaries := make(map[uint64]string, len(refunds))
|
||||
for _, refund := range refunds {
|
||||
labels := labelsByRefund[refund.ID]
|
||||
if len(labels) == 0 {
|
||||
summaries[refund.ID] = "退款商品信息缺失"
|
||||
continue
|
||||
}
|
||||
summaries[refund.ID] = strings.Join(labels, "、")
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// refundProductLabel 统一生成退款商品名称与数量文案。
|
||||
func refundProductLabel(name string, quantity int) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = "商品信息缺失"
|
||||
}
|
||||
if quantity > 1 {
|
||||
return name + " × " + strconv.Itoa(quantity)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// refundBusinessDisplayName 按稳定业务类型选择订单号,未知或失效关系给出明确文案。
|
||||
func refundBusinessDisplayName(refund models.PaymentRefund, gasorderNames, ecOrderNames map[string]string) string {
|
||||
switch refund.BusinessType {
|
||||
case "gasorder":
|
||||
return readableRefundName(gasorderNames[refund.BusinessIdentity], "订单记录已失效")
|
||||
case "ec_order":
|
||||
return readableRefundName(ecOrderNames[refund.BusinessIdentity], "订单记录已失效")
|
||||
default:
|
||||
return "未知业务对象"
|
||||
}
|
||||
}
|
||||
|
||||
// readableRefundName 统一处理退款关联名称缺失场景。
|
||||
func readableRefundName(value, fallback string) string {
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func review(approve bool) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
var request struct {
|
||||
|
||||
50
backend/api/internal/logic/platform/payment/refund_test.go
Normal file
50
backend/api/internal/logic/platform/payment/refund_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
)
|
||||
|
||||
// TestRefundReviewerDisplayName 验证审核人展示区分未审核、正常和历史失效账号。
|
||||
func TestRefundReviewerDisplayName(t *testing.T) {
|
||||
if got := refundReviewerDisplayName("", nil); got != "尚未审核" {
|
||||
t.Fatalf("未审核文案 = %q", got)
|
||||
}
|
||||
if got := refundReviewerDisplayName("reviewer-1", map[string]string{"reviewer-1": "平台根管理员"}); got != "平台根管理员" {
|
||||
t.Fatalf("审核人名称 = %q", got)
|
||||
}
|
||||
if got := refundReviewerDisplayName("reviewer-2", map[string]string{}); got != "审核人记录已失效" {
|
||||
t.Fatalf("失效审核人文案 = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefundBusinessDisplayName 验证退款业务对象按类型显示原订单号并处理失效关系。
|
||||
func TestRefundBusinessDisplayName(t *testing.T) {
|
||||
gasRefund := models.PaymentRefund{BusinessType: "gasorder", BusinessIdentity: "gas-order"}
|
||||
ecRefund := models.PaymentRefund{BusinessType: "ec_order", BusinessIdentity: "ec-order"}
|
||||
gasNames := map[string]string{"gas-order": "GAS-ORDER-001"}
|
||||
ecNames := map[string]string{"ec-order": "EC-ORDER-001"}
|
||||
if got := refundBusinessDisplayName(gasRefund, gasNames, ecNames); got != "GAS-ORDER-001" {
|
||||
t.Fatalf("气体配送订单号展示错误:%q", got)
|
||||
}
|
||||
if got := refundBusinessDisplayName(ecRefund, gasNames, ecNames); got != "EC-ORDER-001" {
|
||||
t.Fatalf("商城订单号展示错误:%q", got)
|
||||
}
|
||||
if got := refundBusinessDisplayName(models.PaymentRefund{BusinessType: "gasorder", BusinessIdentity: "missing"}, gasNames, ecNames); got != "订单记录已失效" {
|
||||
t.Fatalf("失效订单应显示稳定文案:%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefundProductLabel 验证退款商品名称和数量使用稳定中文格式。
|
||||
func TestRefundProductLabel(t *testing.T) {
|
||||
if got := refundProductLabel("示例液化气钢瓶", 1); got != "示例液化气钢瓶" {
|
||||
t.Fatalf("单件退款商品展示错误:%q", got)
|
||||
}
|
||||
if got := refundProductLabel("15kg 液化气配送服务", 2); got != "15kg 液化气配送服务 × 2" {
|
||||
t.Fatalf("多件退款商品展示错误:%q", got)
|
||||
}
|
||||
if got := refundProductLabel("", 1); got != "商品信息缺失" {
|
||||
t.Fatalf("缺失退款商品应显示稳定文案:%q", got)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,15 @@ func MockData(database *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 财务退款属于平台审核业务,Mock 审核人必须引用平台账号而不是气站账号。
|
||||
if err := ensureMockRoot(tx, string(passwordHash)); err != nil {
|
||||
return err
|
||||
}
|
||||
var rootAccount models.PlatformAccount
|
||||
if err := tx.Where("username = ?", "root").First(&rootAccount).Error; err != nil {
|
||||
return fmt.Errorf("find root platform account for mock refund: %w", err)
|
||||
}
|
||||
|
||||
delivery := models.DeliveryBasic{
|
||||
Entity: entity(3, common.StatusEnable), DeliveryCode: "MOCK-DELIVERY-001",
|
||||
GasBasicID: gas.ID, Name: "和气示例配送点", Principal: "李主管",
|
||||
@@ -394,11 +403,14 @@ func MockData(database *gorm.DB) error {
|
||||
Entity: entity(37, common.StatusEnable), RefundStatus: 20, WalletBasicID: wallet.ID,
|
||||
PaymentOrderID: walletPayment.ID, RefundNo: "MOCK-REFUND-001", RequestNo: "MOCK-REQ-REFUND-001",
|
||||
BusinessType: "gasorder", BusinessIdentity: gasOrder.Identity, UserIdentity: user.Identity,
|
||||
Amount: 1000, Reason: "模拟部分退款", ReviewerIdentity: gasAccount.Identity, ReviewedAt: &refundCompletedAt, CompletedAt: &refundCompletedAt,
|
||||
Amount: 1000, Reason: "模拟部分退款", ReviewerIdentity: rootAccount.Identity, ReviewedAt: &refundCompletedAt, CompletedAt: &refundCompletedAt,
|
||||
}
|
||||
if err := put(tx, &refund); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repairMockRefundReviewer(tx, refund.Identity, rootAccount.Identity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyCash := models.WalletApplyCash{
|
||||
Entity: entity(38, common.StatusEnable), ApplyStatus: common.StatusApproved, WalletBasicID: wallet.ID, WalletBankID: bank.ID,
|
||||
@@ -539,9 +551,6 @@ func MockData(database *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureMockRoot(tx, string(passwordHash)); err != nil {
|
||||
return err
|
||||
}
|
||||
var rootRole models.PlatformRole
|
||||
if err := tx.Where("role_code = ?", "root").First(&rootRole).Error; err != nil {
|
||||
return fmt.Errorf("find root role for mock menu: %w", err)
|
||||
@@ -1005,6 +1014,35 @@ func ensureMockRoot(database *gorm.DB, passwordHash string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// repairMockRefundReviewer 幂等修复历史 Mock 退款误关联气站账号的问题。
|
||||
func repairMockRefundReviewer(database *gorm.DB, refundIdentity string, reviewerIdentity string) error {
|
||||
if refundIdentity == "" || reviewerIdentity == "" {
|
||||
return errors.New("mock refund reviewer identity is missing")
|
||||
}
|
||||
result := database.Model(&models.PaymentRefund{}).
|
||||
Where("identity = ?", refundIdentity).
|
||||
Update("reviewer_identity", reviewerIdentity)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("repair mock refund reviewer: %w", result.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepairMockRefundReviewer 仅修复固定 Mock 退款,不触发其他模拟资源写入。
|
||||
func RepairMockRefundReviewer(database *gorm.DB) (int64, error) {
|
||||
var rootAccount models.PlatformAccount
|
||||
if err := database.Where("username = ?", "root").First(&rootAccount).Error; err != nil {
|
||||
return 0, fmt.Errorf("find root platform account for mock refund: %w", err)
|
||||
}
|
||||
result := database.Model(&models.PaymentRefund{}).
|
||||
Where("identity = ? AND refund_no = ?", entity(37, common.StatusEnable).Identity, "MOCK-REFUND-001").
|
||||
Update("reviewer_identity", rootAccount.Identity)
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("repair mock refund reviewer: %w", result.Error)
|
||||
}
|
||||
return result.RowsAffected, nil
|
||||
}
|
||||
|
||||
func entity(sequence int, status int) models.Entity {
|
||||
return models.Entity{
|
||||
Identity: fmt.Sprintf("%s%012d", mockIdentityPrefix, sequence),
|
||||
|
||||
@@ -107,6 +107,16 @@ func TestMockEcOrderStatusesMatchWorkflow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepairMockRefundReviewerRejectsMissingIdentity 验证修复逻辑不会扩大到未明确指定的记录。
|
||||
func TestRepairMockRefundReviewerRejectsMissingIdentity(t *testing.T) {
|
||||
if err := repairMockRefundReviewer(nil, "", "platform-reviewer"); err == nil {
|
||||
t.Fatal("缺少退款标识时必须拒绝修复")
|
||||
}
|
||||
if err := repairMockRefundReviewer(nil, "mock-refund", ""); err == nil {
|
||||
t.Fatal("缺少审核人标识时必须拒绝修复")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMockEcOrderRepairSpecs 验证修复命令只覆盖固定十条商城 Mock 订单。
|
||||
func TestMockEcOrderRepairSpecs(t *testing.T) {
|
||||
specs := mockEcOrderRepairSpecs()
|
||||
|
||||
Reference in New Issue
Block a user