221 lines
6.7 KiB
Go
221 lines
6.7 KiB
Go
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)
|
||
}
|