2026-07-28 13:55:40 +08:00
|
|
|
|
package fin
|
2026-07-28 10:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"io"
|
2026-08-16 23:55:50 +08:00
|
|
|
|
"strconv"
|
|
|
|
|
|
"strings"
|
2026-07-29 14:03:10 +08:00
|
|
|
|
"time"
|
2026-07-28 10:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|
|
|
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
2026-08-16 23:55:50 +08:00
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
2026-07-28 10:42:26 +08:00
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
|
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-16 23:55:50 +08:00
|
|
|
|
// 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, "、")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:42:26 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-29 14:03:10 +08:00
|
|
|
|
if !validSettlementPeriod(input["period_start"], input["period_end"]) {
|
|
|
|
|
|
return errors.New("invalid settlement period")
|
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-29 14:03:10 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|