Files
platforms/backend/api/internal/logic/platform/fin/fin.go
2026-08-16 23:55:50 +08:00

221 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}