修复退款审核详情关联与空值展示
This commit is contained in:
@@ -78,6 +78,13 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("mock track points repaired: %d\n", count)
|
||||
case "repair-mock-refund-reviewer":
|
||||
count, err := repairMockRefundReviewer()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("mock refund reviewers repaired: %d\n", count)
|
||||
case "migrate":
|
||||
if err := migrateDatabase(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
@@ -92,7 +99,7 @@ func main() {
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data|repair-mock-gasorder-status|repair-mock-ec-order|repair-mock-track-point>")
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|gas-resource-contract|delivery-resource-contract|migrate|mock-data|repair-mock-gasorder-status|repair-mock-ec-order|repair-mock-track-point|repair-mock-refund-reviewer>")
|
||||
}
|
||||
|
||||
type route struct {
|
||||
@@ -265,6 +272,27 @@ func repairMockTrackPoint() (int64, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// repairMockRefundReviewer 使用独立幂等命令修复固定 Mock 退款的审核人归属。
|
||||
func repairMockRefundReviewer() (int64, error) {
|
||||
config.New(serviceKey)
|
||||
if config.Spec.Databases == nil {
|
||||
return 0, fmt.Errorf("database configuration is required")
|
||||
}
|
||||
databaseService, err := database.NewDatabase(
|
||||
config.Spec.Databases.Driver,
|
||||
config.Spec.Databases.Source,
|
||||
dbsql.SetOptions(nil),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("connect database: %w", err)
|
||||
}
|
||||
count, err := seed.RepairMockRefundReviewer(databaseService)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("repair mock refund reviewer: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func migrateDatabase() error {
|
||||
config.New(serviceKey)
|
||||
options := &types.SqlOptions{
|
||||
|
||||
@@ -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()
|
||||
|
||||
65
docs/操作日志_退款审核关联名称展示修复_20260817.md
Normal file
65
docs/操作日志_退款审核关联名称展示修复_20260817.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 退款审核关联名称展示修复操作日志
|
||||
|
||||
操作时间:2026-08-17
|
||||
|
||||
操作类型:修改
|
||||
|
||||
影响模块:平台总后台、财务管理、退款审核
|
||||
|
||||
## 操作前状态
|
||||
|
||||
- 退款业务对象依赖前端动态加载原订单关系,加载不到时显示“名称加载失败”。
|
||||
- 申请用户直接显示截断 UUID,财务列表无法直接识别用户。
|
||||
|
||||
## 具体操作
|
||||
|
||||
1. 退款列表按当前页批量读取气体配送订单、商城订单和用户名称,不执行逐行查询。
|
||||
2. 退款详情复用同一展示构建逻辑,接口增加 `business_display_name` 和 `user_display_name` 只读展示字段。
|
||||
3. 业务对象显示原订单号,用户显示姓名;两个字段均保留 UUID 复制和详情跳转。
|
||||
4. 订单或用户已失效时显示明确业务文案,不将失效关系误报为普通网络加载失败。
|
||||
5. 增加业务类型、订单号和失效关系的回归测试及前端展示契约断言。
|
||||
6. 新增“退款商品”:商城订单使用成交快照,气体配送订单通过退款明细关联原订单项和产品档案,多件退款显示“× 数量”。
|
||||
7. 空说明和空审核说明分别显示“无说明”“无审核说明”,不再使用无语义短横线。
|
||||
8. 审核人由后端解析平台账号展示名称;历史账号归档后仍可读取,失效时显示明确文案,并保留标识复制。
|
||||
9. 支付记录显示支付单号,钱包显示归属类型摘要,完整唯一标识继续作为辅助复制信息。
|
||||
10. 修正 `MOCK-REFUND-001` 的审核人归属:由气站账号改为平台根管理员,并在重复执行 Mock 初始化时修复已有错误数据。
|
||||
11. 增加独立命令 `repair-mock-refund-reviewer`,用于绕开无关 Mock 数据冲突并安全修复当前数据库。
|
||||
|
||||
## 行为变化
|
||||
|
||||
- 修改前:业务对象显示“名称加载失败”,用户显示 UUID。
|
||||
- 修改后:业务对象显示原订单号,用户显示姓名。
|
||||
- 当前 Mock 退款的“退款商品”显示“示例液化气钢瓶”。
|
||||
- 当前 Mock 退款的“审核人”显示平台根管理员,不再显示“审核人记录已失效”。
|
||||
- 退款金额、审核状态、入账事务、权限和接口路径均未改变。
|
||||
|
||||
## 代码变更
|
||||
|
||||
- `backend/api/internal/logic/platform/payment/refund.go`:增加退款列表和详情可读关系字段。
|
||||
- `backend/api/internal/logic/platform/payment/refund_test.go`:增加订单类型、订单号和失效关系测试。
|
||||
- `backend/api/internal/seed/mock.go`:修正 Mock 退款审核人并增加历史错误数据幂等修复。
|
||||
- `backend/api/internal/seed/mock_test.go`:增加修复范围的输入保护测试。
|
||||
- `backend/api/cmd/cli/main.go`:增加 Mock 退款审核人定向修复命令。
|
||||
- `frontend/platform_admin/src/api/resources.ts`:退款关系字段改用接口展示名称。
|
||||
- `frontend/platform_admin/src/api/resource-detail-contract.ts`:固定退款详情字段顺序并隐藏审核人辅助名称字段。
|
||||
- `frontend/platform_admin/scripts/check-resource-display-contracts.mjs`:增加退款展示契约断言。
|
||||
- `docs/项目文档_平台资源中文展示统一_v1.0.md`:记录 v1.10 至 v1.13 变更。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `go test ./internal/logic/platform/payment ./internal/routers`:通过。
|
||||
- `go test ./...`:通过。
|
||||
- `go vet ./...`:通过。
|
||||
- `go build ./cmd/main/main.go`:通过。
|
||||
- `pnpm resource-display-contracts:check`:通过。
|
||||
- `pnpm contract:check`:通过,48 个资源契约一致。
|
||||
- 修改文件定向 Biome lint:通过。
|
||||
- `pnpm build`:通过,TypeScript 类型检查及 Vite 生产构建完成,共转换 2621 个模块。
|
||||
- 新增退款商品汇总后再次执行退款模块、全量后端测试、`go vet`、主程序构建及前端全套检查:全部通过。
|
||||
- `go run ./cmd/cli repair-mock-refund-reviewer`:执行成功,当前数据库修复 1 条 Mock 退款审核人记录。
|
||||
- 增加定向修复命令后再次执行后端全量测试、`go vet`、主程序与 CLI 构建:全部通过。
|
||||
|
||||
## 风险评估
|
||||
|
||||
- 可读名称仅用于已授权财务审核页面,不改变退款事实字段或跨服务标识。
|
||||
- 订单号来自不可变业务编号;用户姓名用于退款审核识别,UUID 仍作为审计定位依据保留。
|
||||
@@ -72,3 +72,7 @@ frontend/platform_admin/
|
||||
- v1.7.1:提现列表的审核人改为直接使用审核姓名快照,并保留审核人标识复制与详情跳转,避免历史账户关系加载失败影响列表展示。
|
||||
- v1.8:财务支付记录的商城订单关系统一显示订单号,支付状态使用财务领域的 `35=已支付`,渠道按业务文档统一显示“余额支付”“支付宝”“微信支付”。
|
||||
- v1.9:财务支付详情新增“支付商品”,由后端按商城订单明细的成交快照汇总商品名称和数量,确保商品改名后仍可按交易事实核对。
|
||||
- v1.10:退款审核列表和详情由接口直接返回原订单号与申请用户姓名,前端保留关系标识复制和详情跳转,不再依赖临时关系加载。
|
||||
- v1.11:退款审核新增“退款商品”,商城退款读取成交快照,气体配送退款通过原订单项读取产品名称,并汇总退款数量。
|
||||
- v1.12:退款审核补齐空值业务文案、审核人名称、支付单号和钱包归属摘要,UUID 统一降级为复制信息。
|
||||
- v1.13:修正固定 Mock 退款误把气站账号写为平台审核人的数据错误,并在重复初始化时幂等修复历史记录;退款审核人由接口稳定返回平台账号名称。
|
||||
|
||||
@@ -101,6 +101,40 @@ assertIncludes(
|
||||
"wallet_apply_cash: {",
|
||||
'提现详情必须声明专属字段顺序与审核姓名快照隐藏规则',
|
||||
);
|
||||
|
||||
// 退款审核的原订单和用户必须使用接口返回的可读名称,不依赖列表临时加载关系。
|
||||
const refundDefinition = resources.match(
|
||||
/define\('payment_refund',[\s\S]*?\n\s*\], 'list',/,
|
||||
)?.[0];
|
||||
if (!refundDefinition) throw new Error('无法读取退款审核资源定义');
|
||||
for (const contract of [
|
||||
"listDisplayKey: 'business_display_name'",
|
||||
"detailDisplayKey: 'business_display_name'",
|
||||
"listDisplayKey: 'user_display_name'",
|
||||
"detailDisplayKey: 'user_display_name'",
|
||||
"f('refund_product_summary', { emptyText: '退款商品信息缺失' })",
|
||||
"f('description', { emptyText: '无说明' })",
|
||||
"f('review_remark', { emptyText: '无审核说明' })",
|
||||
"detailDisplayKey: 'reviewer_display_name'",
|
||||
"relation('payment_order_identity', '/payment_order'",
|
||||
"relation('wallet_basic_identity', '/wallet_basic'",
|
||||
"relationOptionDisplay: 'wallet-owner'",
|
||||
'listDisplayIdentityCopy: true',
|
||||
]) {
|
||||
if (!refundDefinition.includes(contract)) {
|
||||
throw new Error(`退款审核缺少可读关系展示契约:${contract}`);
|
||||
}
|
||||
}
|
||||
assertIncludes(
|
||||
detailContract,
|
||||
"payment_refund: {",
|
||||
'退款详情必须声明专属字段顺序和审核人展示名称隐藏规则',
|
||||
);
|
||||
assertIncludes(
|
||||
detailContract,
|
||||
"hiddenKeys: ['reviewer_display_name']",
|
||||
'退款详情不得重复展示审核人名称辅助字段',
|
||||
);
|
||||
assertIncludes(
|
||||
detailContract,
|
||||
"hiddenKeys: ['reviewer_name']",
|
||||
|
||||
@@ -146,6 +146,16 @@ const contracts: Record<string, ResourceDetailContract> = {
|
||||
// 审核姓名作为审核人关系的可读快照使用,不再重复渲染独立字段。
|
||||
hiddenKeys: ['reviewer_name'],
|
||||
},
|
||||
payment_refund: {
|
||||
leadingKeys: [
|
||||
'refund_no', 'refund_status', 'business_type', 'business_identity',
|
||||
'user_identity', 'refund_product_summary', 'amount', 'reason',
|
||||
'description', 'reviewer_identity', 'review_remark', 'reviewed_at',
|
||||
'completed_at', 'payment_order_identity', 'wallet_basic_identity', 'identity',
|
||||
],
|
||||
// 审核人可读名称仅作为标识字段主文案,不重复显示为独立详情项。
|
||||
hiddenKeys: ['reviewer_display_name'],
|
||||
},
|
||||
ec_order: {
|
||||
leadingKeys: [
|
||||
'order_no', 'order_status', 'request_no', 'user_account_identity',
|
||||
|
||||
@@ -178,6 +178,7 @@ const fieldLabels: Record<string, string> = {
|
||||
previous_order_status: '异常前订单状态',
|
||||
payment_status: '支付状态',
|
||||
refund_status: '退款状态',
|
||||
refund_product_summary: '退款商品',
|
||||
apply_status: '提现状态',
|
||||
ticket_status: '工单状态',
|
||||
reconciliation_status: '对账状态',
|
||||
@@ -774,11 +775,15 @@ export const resources: ResourceUiDefinition[] = [
|
||||
f('operator_identity'), f('ymd'), f('ym'), f('remark'),
|
||||
]),
|
||||
define('payment_refund', '退款审核', 'readonly', [
|
||||
f('refund_no'), f('business_type'), f('business_identity', { type: 'identity', label: '业务对象', listRelationNameOnly: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: {
|
||||
f('refund_no'), f('business_type'), f('business_identity', { type: 'identity', label: '业务对象', listDisplayKey: 'business_display_name', detailDisplayKey: 'business_display_name', listDisplayIdentityCopy: true, displayRelationLabel: true, showIdentityCopy: true, dynamicRelation: {
|
||||
parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' },
|
||||
} }), relation('user_identity', '/user_account'),
|
||||
f('amount'), f('reason'), f('description'), f('refund_status'),
|
||||
relation('reviewer_identity', '/platform_account'), f('review_remark'), f('reviewed_at'), f('completed_at'),
|
||||
} }), relation('user_identity', '/user_account', false, { label: '用户', listDisplayKey: 'user_display_name', detailDisplayKey: 'user_display_name', listDisplayIdentityCopy: true, displayRelationLabel: true, showIdentityCopy: true }),
|
||||
f('refund_product_summary', { emptyText: '退款商品信息缺失' }),
|
||||
f('amount'), f('reason'), f('description', { emptyText: '无说明' }), f('refund_status'),
|
||||
relation('reviewer_identity', '/platform_account', false, { label: '审核人', listDisplayKey: 'reviewer_display_name', detailDisplayKey: 'reviewer_display_name', listDisplayIdentityCopy: true, displayRelationLabel: true, showIdentityCopy: true }),
|
||||
f('review_remark', { emptyText: '无审核说明' }), f('reviewed_at'), f('completed_at'),
|
||||
relation('payment_order_identity', '/payment_order', false, { label: '支付记录', displayRelationLabel: true, showIdentityCopy: true }),
|
||||
relation('wallet_basic_identity', '/wallet_basic', false, { label: '钱包', relationOptionDisplay: 'wallet-owner', displayRelationLabel: true, showIdentityCopy: true }),
|
||||
], 'list', [
|
||||
{ name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } },
|
||||
{ name: '驳回退款', resource: '/payment_refund/:identity/reject', danger: true, fields: [f('remark', { required: true })], visibleFor: { field: 'refund_status', values: [10] } },
|
||||
|
||||
Reference in New Issue
Block a user