修复退款审核详情关联与空值展示
This commit is contained in:
@@ -78,6 +78,13 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Printf("mock track points repaired: %d\n", count)
|
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":
|
case "migrate":
|
||||||
if err := migrateDatabase(); err != nil {
|
if err := migrateDatabase(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
@@ -92,7 +99,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
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 {
|
type route struct {
|
||||||
@@ -265,6 +272,27 @@ func repairMockTrackPoint() (int64, error) {
|
|||||||
return count, nil
|
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 {
|
func migrateDatabase() error {
|
||||||
config.New(serviceKey)
|
config.New(serviceKey)
|
||||||
options := &types.SqlOptions{
|
options := &types.SqlOptions{
|
||||||
|
|||||||
@@ -1,16 +1,304 @@
|
|||||||
package payment
|
package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.apinb.com/bsm-sdk/core/errcode"
|
"git.apinb.com/bsm-sdk/core/errcode"
|
||||||
"git.apinb.com/bsm-sdk/core/infra"
|
"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"
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||||
paylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
paylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/payment"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListRefund(ctx *gin.Context) { common.ListResource(ctx, &models.PaymentRefund{}) }
|
// paymentRefundDisplay 在退款事实之外附加财务审核所需的可读关系名称。
|
||||||
func GetRefund(ctx *gin.Context) { common.GetResource(ctx, &models.PaymentRefund{}) }
|
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 {
|
func review(approve bool) gin.HandlerFunc {
|
||||||
return func(ctx *gin.Context) {
|
return func(ctx *gin.Context) {
|
||||||
var request struct {
|
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
|
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{
|
delivery := models.DeliveryBasic{
|
||||||
Entity: entity(3, common.StatusEnable), DeliveryCode: "MOCK-DELIVERY-001",
|
Entity: entity(3, common.StatusEnable), DeliveryCode: "MOCK-DELIVERY-001",
|
||||||
GasBasicID: gas.ID, Name: "和气示例配送点", Principal: "李主管",
|
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,
|
Entity: entity(37, common.StatusEnable), RefundStatus: 20, WalletBasicID: wallet.ID,
|
||||||
PaymentOrderID: walletPayment.ID, RefundNo: "MOCK-REFUND-001", RequestNo: "MOCK-REQ-REFUND-001",
|
PaymentOrderID: walletPayment.ID, RefundNo: "MOCK-REFUND-001", RequestNo: "MOCK-REQ-REFUND-001",
|
||||||
BusinessType: "gasorder", BusinessIdentity: gasOrder.Identity, UserIdentity: user.Identity,
|
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 {
|
if err := put(tx, &refund); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := repairMockRefundReviewer(tx, refund.Identity, rootAccount.Identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
applyCash := models.WalletApplyCash{
|
applyCash := models.WalletApplyCash{
|
||||||
Entity: entity(38, common.StatusEnable), ApplyStatus: common.StatusApproved, WalletBasicID: wallet.ID, WalletBankID: bank.ID,
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureMockRoot(tx, string(passwordHash)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var rootRole models.PlatformRole
|
var rootRole models.PlatformRole
|
||||||
if err := tx.Where("role_code = ?", "root").First(&rootRole).Error; err != nil {
|
if err := tx.Where("role_code = ?", "root").First(&rootRole).Error; err != nil {
|
||||||
return fmt.Errorf("find root role for mock menu: %w", err)
|
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
|
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 {
|
func entity(sequence int, status int) models.Entity {
|
||||||
return models.Entity{
|
return models.Entity{
|
||||||
Identity: fmt.Sprintf("%s%012d", mockIdentityPrefix, sequence),
|
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 订单。
|
// TestMockEcOrderRepairSpecs 验证修复命令只覆盖固定十条商城 Mock 订单。
|
||||||
func TestMockEcOrderRepairSpecs(t *testing.T) {
|
func TestMockEcOrderRepairSpecs(t *testing.T) {
|
||||||
specs := mockEcOrderRepairSpecs()
|
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.7.1:提现列表的审核人改为直接使用审核姓名快照,并保留审核人标识复制与详情跳转,避免历史账户关系加载失败影响列表展示。
|
||||||
- v1.8:财务支付记录的商城订单关系统一显示订单号,支付状态使用财务领域的 `35=已支付`,渠道按业务文档统一显示“余额支付”“支付宝”“微信支付”。
|
- v1.8:财务支付记录的商城订单关系统一显示订单号,支付状态使用财务领域的 `35=已支付`,渠道按业务文档统一显示“余额支付”“支付宝”“微信支付”。
|
||||||
- v1.9:财务支付详情新增“支付商品”,由后端按商城订单明细的成交快照汇总商品名称和数量,确保商品改名后仍可按交易事实核对。
|
- v1.9:财务支付详情新增“支付商品”,由后端按商城订单明细的成交快照汇总商品名称和数量,确保商品改名后仍可按交易事实核对。
|
||||||
|
- v1.10:退款审核列表和详情由接口直接返回原订单号与申请用户姓名,前端保留关系标识复制和详情跳转,不再依赖临时关系加载。
|
||||||
|
- v1.11:退款审核新增“退款商品”,商城退款读取成交快照,气体配送退款通过原订单项读取产品名称,并汇总退款数量。
|
||||||
|
- v1.12:退款审核补齐空值业务文案、审核人名称、支付单号和钱包归属摘要,UUID 统一降级为复制信息。
|
||||||
|
- v1.13:修正固定 Mock 退款误把气站账号写为平台审核人的数据错误,并在重复初始化时幂等修复历史记录;退款审核人由接口稳定返回平台账号名称。
|
||||||
|
|||||||
@@ -101,6 +101,40 @@ assertIncludes(
|
|||||||
"wallet_apply_cash: {",
|
"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(
|
assertIncludes(
|
||||||
detailContract,
|
detailContract,
|
||||||
"hiddenKeys: ['reviewer_name']",
|
"hiddenKeys: ['reviewer_name']",
|
||||||
|
|||||||
@@ -146,6 +146,16 @@ const contracts: Record<string, ResourceDetailContract> = {
|
|||||||
// 审核姓名作为审核人关系的可读快照使用,不再重复渲染独立字段。
|
// 审核姓名作为审核人关系的可读快照使用,不再重复渲染独立字段。
|
||||||
hiddenKeys: ['reviewer_name'],
|
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: {
|
ec_order: {
|
||||||
leadingKeys: [
|
leadingKeys: [
|
||||||
'order_no', 'order_status', 'request_no', 'user_account_identity',
|
'order_no', 'order_status', 'request_no', 'user_account_identity',
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ const fieldLabels: Record<string, string> = {
|
|||||||
previous_order_status: '异常前订单状态',
|
previous_order_status: '异常前订单状态',
|
||||||
payment_status: '支付状态',
|
payment_status: '支付状态',
|
||||||
refund_status: '退款状态',
|
refund_status: '退款状态',
|
||||||
|
refund_product_summary: '退款商品',
|
||||||
apply_status: '提现状态',
|
apply_status: '提现状态',
|
||||||
ticket_status: '工单状态',
|
ticket_status: '工单状态',
|
||||||
reconciliation_status: '对账状态',
|
reconciliation_status: '对账状态',
|
||||||
@@ -774,11 +775,15 @@ export const resources: ResourceUiDefinition[] = [
|
|||||||
f('operator_identity'), f('ymd'), f('ym'), f('remark'),
|
f('operator_identity'), f('ymd'), f('ym'), f('remark'),
|
||||||
]),
|
]),
|
||||||
define('payment_refund', '退款审核', 'readonly', [
|
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' },
|
parentKey: 'business_type', parentLabel: '业务类型', resources: { gasorder: '/gasorder_basic', ec_order: '/ec_order' },
|
||||||
} }), relation('user_identity', '/user_account'),
|
} }), relation('user_identity', '/user_account', false, { label: '用户', listDisplayKey: 'user_display_name', detailDisplayKey: 'user_display_name', listDisplayIdentityCopy: true, displayRelationLabel: true, showIdentityCopy: true }),
|
||||||
f('amount'), f('reason'), f('description'), f('refund_status'),
|
f('refund_product_summary', { emptyText: '退款商品信息缺失' }),
|
||||||
relation('reviewer_identity', '/platform_account'), f('review_remark'), f('reviewed_at'), f('completed_at'),
|
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', [
|
], 'list', [
|
||||||
{ name: '审核通过并退入钱包', resource: '/payment_refund/:identity/approve', fields: [f('remark')], visibleFor: { field: 'refund_status', values: [10] } },
|
{ 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] } },
|
{ 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