package fin import ( "bytes" "encoding/json" "errors" "io" "strconv" "strings" "time" "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" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" ) // finPaymentProductSnapshot 是商城下单时固化的商品成交快照最小结构。 type finPaymentProductSnapshot struct { Name string `json:"name"` } // ListFinPayment 返回财务支付记录列表,并批量附加成交商品摘要。 func ListFinPayment(ctx *gin.Context) { page, size := common.PageSize(ctx) var payments []models.FinPayment var total int64 query := common.ApplyKeywordFilter( ctx, common.ActiveRecords(impl.DBService.Model(&models.FinPayment{})), &models.FinPayment{}, ) 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(&payments).Error; err != nil { infra.Response.Error(ctx, err) return } summaries, err := loadFinPaymentProductSummaries(payments) if err != nil { infra.Response.Error(ctx, err) return } response, err := common.PublicResourceResponse(payments) if err != nil { infra.Response.Error(ctx, err) return } rows, ok := response.([]any) if !ok || len(rows) != len(payments) { infra.Response.Error(ctx, errors.New("invalid finance payment list response")) return } for index, payment := range payments { row, valid := rows[index].(map[string]any) if !valid { infra.Response.Error(ctx, errors.New("invalid finance payment row response")) return } row["product_summary"] = summaries[payment.EcOrderID] } infra.Response.Success(ctx, gin.H{ "total": total, "list": common.ProtectPreciseLocation(ctx, &models.FinPayment{}, response), }) } // loadFinPaymentProductSummaries 一次查询当前页全部订单明细,避免列表逐行查询。 func loadFinPaymentProductSummaries(payments []models.FinPayment) (map[uint64]string, error) { orderIDs := make([]uint64, 0, len(payments)) seen := make(map[uint64]struct{}, len(payments)) for _, payment := range payments { if _, exists := seen[payment.EcOrderID]; exists { continue } seen[payment.EcOrderID] = struct{}{} orderIDs = append(orderIDs, payment.EcOrderID) } itemsByOrder := make(map[uint64][]models.EcOrderItem, len(orderIDs)) if len(orderIDs) > 0 { var items []models.EcOrderItem if err := common.ActiveRecords(impl.DBService). Where("ec_order_id IN ?", orderIDs). Order("ec_order_id asc, id asc").Find(&items).Error; err != nil { return nil, err } for _, item := range items { itemsByOrder[item.EcOrderID] = append(itemsByOrder[item.EcOrderID], item) } } summaries := make(map[uint64]string, len(orderIDs)) for _, orderID := range orderIDs { summaries[orderID] = finPaymentProductSummary(itemsByOrder[orderID]) } return summaries, nil } // GetFinPayment 返回支付详情,并从订单商品快照生成财务可读的商品摘要。 func GetFinPayment(ctx *gin.Context) { var payment models.FinPayment if err := common.ActiveRecords(impl.DBService). Where("identity = ?", ctx.Param("identity")).First(&payment).Error; err != nil { common.RespondRecordError(ctx, err) return } var items []models.EcOrderItem if err := common.ActiveRecords(impl.DBService). Where("ec_order_id = ?", payment.EcOrderID). Order("id asc").Find(&items).Error; err != nil { infra.Response.Error(ctx, err) return } response, err := common.PublicResourceResponse(payment) if err != nil { infra.Response.Error(ctx, err) return } data, ok := response.(map[string]any) if !ok { infra.Response.Error(ctx, errors.New("invalid finance payment response")) return } data["product_summary"] = finPaymentProductSummary(items) infra.Response.Success(ctx, data) } // finPaymentProductSummary 优先使用不可变成交快照,避免商品改名后财务详情发生漂移。 func finPaymentProductSummary(items []models.EcOrderItem) string { summaries := make([]string, 0, len(items)) for _, item := range items { var snapshot finPaymentProductSnapshot if err := json.Unmarshal([]byte(item.ProductSnapshot), &snapshot); err != nil { continue } name := strings.TrimSpace(snapshot.Name) if name == "" { continue } if item.Quantity > 1 { name += " × " + strconv.Itoa(item.Quantity) } summaries = append(summaries, name) } if len(summaries) == 0 { return "商品信息缺失" } return strings.Join(summaries, "、") } func FinSettlementHandlers() (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) { fields := []string{"settlement_no", "subject_type", "subject_id", "period_start", "period_end"} return func(ctx *gin.Context) { common.ListResource(ctx, &models.FinSettlement{}) }, func(ctx *gin.Context) { if err := rewriteSettlementSubject(ctx); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } _, create, _, _ := common.ResourceHandlers(&models.FinSettlement{}, fields, fields) create(ctx) }, func(ctx *gin.Context) { common.GetResource(ctx, &models.FinSettlement{}) }, func(ctx *gin.Context) { if err := rewriteSettlementSubject(ctx); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } _, _, _, update := common.ResourceHandlers(&models.FinSettlement{}, fields, fields) update(ctx) } } func rewriteSettlementSubject(ctx *gin.Context) error { var input map[string]any if err := ctx.ShouldBindJSON(&input); err != nil { return err } if !validSettlementPeriod(input["period_start"], input["period_end"]) { return errors.New("invalid settlement period") } subjectType, _ := input["subject_type"].(string) identity, _ := input["subject_identity"].(string) var model any switch subjectType { case "gas", "gas_basic": model = &models.GasBasic{} case "delivery", "delivery_basic": model = &models.DeliveryBasic{} case "staff", "staff_account": model = &models.StaffAccount{} default: return errors.New("invalid settlement subject") } id, err := common.ResolveIdentityID(model, identity, true) if err != nil { return err } delete(input, "subject_identity") input["subject_id"] = id encoded, err := json.Marshal(input) if err != nil { return err } ctx.Request.Body = io.NopCloser(bytes.NewReader(encoded)) return nil } func validSettlementPeriod(startValue, endValue any) bool { startText, startOK := startValue.(string) endText, endOK := endValue.(string) if !startOK || !endOK { return false } start, startErr := time.Parse(time.RFC3339, startText) end, endErr := time.Parse(time.RFC3339, endText) return startErr == nil && endErr == nil && end.After(start) }