package admin import ( "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" ) type listShippingStatsRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` ProductID *int64 `form:"product_id"` ProductName string `form:"product_name"` UserID *int64 `form:"user_id"` UserName string `form:"user_name"` ExpressCode string `form:"express_code"` ExpressNo string `form:"express_no"` OrderID *int64 `form:"order_id"` OrderNo string `form:"order_no"` OrderSourceType *int32 `form:"order_source_type"` Payer string `form:"payer"` Keyword string `form:"keyword"` } type listShippingStatsResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []*model.OpsShippingStats `json:"list"` } // ListShippingStats 发货统计列表 func (h *handler) ListShippingStats() core.HandlerFunc { return func(ctx core.Context) { req := new(listShippingStatsRequest) rsp := new(listShippingStatsResponse) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 100 { req.PageSize = 100 } q := h.readDB.OpsShippingStats.WithContext(ctx.RequestContext()).ReadDB() // 精确匹配 if req.ProductID != nil { q = q.Where(h.readDB.OpsShippingStats.ProductID.Eq(*req.ProductID)) } if req.UserID != nil { q = q.Where(h.readDB.OpsShippingStats.UserID.Eq(*req.UserID)) } if req.OrderID != nil { q = q.Where(h.readDB.OpsShippingStats.OrderID.Eq(*req.OrderID)) } if req.OrderSourceType != nil { q = q.Where(h.readDB.OpsShippingStats.OrderSourceType.Eq(*req.OrderSourceType)) } if req.Payer != "" { q = q.Where(h.readDB.OpsShippingStats.Payer.Eq(req.Payer)) } // 字符串匹配 if req.ProductName != "" { q = q.Where(h.readDB.OpsShippingStats.ProductName.Like("%" + req.ProductName + "%")) } if req.UserName != "" { q = q.Where(h.readDB.OpsShippingStats.UserName.Like("%" + req.UserName + "%")) } if req.ExpressCode != "" { q = q.Where(h.readDB.OpsShippingStats.ExpressCode.Eq(req.ExpressCode)) } if req.ExpressNo != "" { q = q.Where(h.readDB.OpsShippingStats.ExpressNo.Like("%" + req.ExpressNo + "%")) } if req.OrderNo != "" { q = q.Where(h.readDB.OpsShippingStats.OrderNo.Like("%" + req.OrderNo + "%")) } if req.Keyword != "" { like := "%" + req.Keyword + "%" q = q.Where(h.readDB.OpsShippingStats.ProductName.Like(like)). Or(h.readDB.OpsShippingStats.UserName.Like(like)). Or(h.readDB.OpsShippingStats.OrderNo.Like(like)). Or(h.readDB.OpsShippingStats.ExpressNo.Like(like)) } total, err := q.Count() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 40101, err.Error())) return } rows, err := q.Order(h.readDB.OpsShippingStats.CreatedAt.Desc(), h.readDB.OpsShippingStats.ID.Desc()). Offset((req.Page-1)*req.PageSize).Limit(req.PageSize).Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 40102, err.Error())) return } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total rsp.List = rows ctx.Payload(rsp) } } type createShippingStatRequest struct { ProductID int64 `json:"product_id" binding:"required"` ProductName string `json:"product_name" binding:"required"` ProductPriceCents int64 `json:"product_price_cents"` ShippedQty int64 `json:"shipped_qty" binding:"required"` UserID int64 `json:"user_id" binding:"required"` UserName string `json:"user_name" binding:"required"` UserAddressText string `json:"user_address_text"` ExpressCode string `json:"express_code"` ExpressNo string `json:"express_no"` OrderID int64 `json:"order_id"` OrderNo string `json:"order_no"` OrderQty int64 `json:"order_qty"` OrderAmountCents int64 `json:"order_amount_cents"` OrderSourceType int32 `json:"order_source_type"` Payer string `json:"payer"` } type simpleMessageResponseShipping struct { Message string `json:"message"` } // CreateShippingStat 新增发货统计记录 func (h *handler) CreateShippingStat() core.HandlerFunc { return func(ctx core.Context) { var req createShippingStatRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } shippedAt := time.Now() // 若传入了订单ID但未给金额/数量,尝试回填 var orderAmount int64 = req.OrderAmountCents var orderQty int64 = req.OrderQty var orderNo string = req.OrderNo if req.OrderID > 0 && (orderAmount == 0 || orderQty == 0 || orderNo == "") { if ord, e := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(req.OrderID)).First(); e == nil && ord != nil { if orderAmount == 0 { orderAmount = ord.ActualAmount } if orderNo == "" { orderNo = ord.OrderNo } } if orderQty == 0 { _ = h.readDB.OrderItems.WithContext(ctx.RequestContext()).Where(h.readDB.OrderItems.OrderID.Eq(req.OrderID)).Select(h.readDB.OrderItems.Quantity.Sum()).Scan(&orderQty) } } // 自动计算盈亏:行价×发货数量 − (订单实付金额/下单总量)×发货数量(订单可空) var perUnitPaid int64 if orderQty > 0 { perUnitPaid = orderAmount / orderQty } autoProfit := req.ProductPriceCents*req.ShippedQty - perUnitPaid*req.ShippedQty it := &model.OpsShippingStats{ ShippedAt: shippedAt, ProductID: req.ProductID, ProductName: req.ProductName, ProductPriceCents: req.ProductPriceCents, ShippedQty: req.ShippedQty, UserID: req.UserID, UserName: req.UserName, UserAddressText: req.UserAddressText, ExpressCode: req.ExpressCode, ExpressNo: req.ExpressNo, OrderID: req.OrderID, OrderNo: orderNo, OrderQty: orderQty, OrderAmountCents: orderAmount, ProfitLossCents: autoProfit, OrderSourceType: req.OrderSourceType, OrderSourceText: sourceTextFromType(req.OrderSourceType), Payer: req.Payer, } if err := h.writeDB.OpsShippingStats.WithContext(ctx.RequestContext()).Create(it); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 40106, "创建发货统计失败")) return } ctx.Payload(&simpleMessageResponseShipping{Message: "创建成功"}) } } type modifyShippingStatRequest struct { ProductID *int64 `json:"product_id"` ProductName string `json:"product_name"` ProductPriceCents *int64 `json:"product_price_cents"` ShippedQty *int64 `json:"shipped_qty"` UserID *int64 `json:"user_id"` UserName string `json:"user_name"` UserAddressText string `json:"user_address_text"` ExpressCode string `json:"express_code"` ExpressNo string `json:"express_no"` OrderID *int64 `json:"order_id"` OrderNo string `json:"order_no"` OrderQty *int64 `json:"order_qty"` OrderAmountCents *int64 `json:"order_amount_cents"` OrderSourceType *int32 `json:"order_source_type"` Payer string `json:"payer"` } // ModifyShippingStat 修改发货统计记录 func (h *handler) ModifyShippingStat() core.HandlerFunc { return func(ctx core.Context) { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil || id <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递ID")) return } var req modifyShippingStatRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } row, err := h.readDB.OpsShippingStats.WithContext(ctx.RequestContext()).Where(h.readDB.OpsShippingStats.ID.Eq(id)).First() if err != nil || row == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 40107, "记录不存在")) return } // shipped_at 字段从统计维度中移除,不再更新 if req.ProductID != nil { row.ProductID = *req.ProductID } if req.ProductName != "" { row.ProductName = req.ProductName } if req.ProductPriceCents != nil { row.ProductPriceCents = *req.ProductPriceCents } if req.ShippedQty != nil { row.ShippedQty = *req.ShippedQty } if req.UserID != nil { row.UserID = *req.UserID } if req.UserName != "" { row.UserName = req.UserName } if req.UserAddressText != "" { row.UserAddressText = req.UserAddressText } if req.ExpressCode != "" { row.ExpressCode = req.ExpressCode } if req.ExpressNo != "" { row.ExpressNo = req.ExpressNo } if req.OrderID != nil { row.OrderID = *req.OrderID // 回填订单相关字段(若空) if row.OrderID > 0 { if ord, e := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(row.OrderID)).First(); e == nil && ord != nil { if row.OrderNo == "" { row.OrderNo = ord.OrderNo } if row.OrderAmountCents == 0 { row.OrderAmountCents = ord.ActualAmount } } if row.OrderQty == 0 { var totalQty int64 if err := h.readDB.OrderItems.WithContext(ctx.RequestContext()).Where(h.readDB.OrderItems.OrderID.Eq(row.OrderID)).Select(h.readDB.OrderItems.Quantity.Sum()).Scan(&totalQty); err == nil { row.OrderQty = totalQty } } } } if req.OrderNo != "" { row.OrderNo = req.OrderNo } if req.OrderQty != nil { row.OrderQty = *req.OrderQty } if req.OrderAmountCents != nil { row.OrderAmountCents = *req.OrderAmountCents } if req.OrderSourceType != nil { row.OrderSourceType = *req.OrderSourceType row.OrderSourceText = sourceTextFromType(*req.OrderSourceType) } if req.Payer != "" { row.Payer = req.Payer } // 自动计算盈亏(订单金额按单位分摊) var perUnitPaidUpd int64 if row.OrderQty > 0 { perUnitPaidUpd = row.OrderAmountCents / row.OrderQty } row.ProfitLossCents = row.ProductPriceCents*row.ShippedQty - perUnitPaidUpd*row.ShippedQty if err := h.writeDB.OpsShippingStats.Save(row); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 40108, "修改失败")) return } ctx.Payload(&simpleMessageResponseShipping{Message: "修改成功"}) } } // DeleteShippingStat 删除发货统计记录 func (h *handler) DeleteShippingStat() core.HandlerFunc { return func(ctx core.Context) { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil || id <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递ID")) return } del := &model.OpsShippingStats{ID: id} if _, err := h.writeDB.OpsShippingStats.Delete(del); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 40109, "删除失败")) return } ctx.Payload(&simpleMessageResponseShipping{Message: "删除成功"}) } } // GetShippingStat 获取单条发货统计详情 func (h *handler) GetShippingStat() core.HandlerFunc { return func(ctx core.Context) { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil || id <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递ID")) return } row, err := h.readDB.OpsShippingStats.WithContext(ctx.RequestContext()).Where(h.readDB.OpsShippingStats.ID.Eq(id)).First() if err != nil || row == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 40103, "记录不存在")) return } ctx.Payload(row) } } func sourceTextFromType(t int32) string { switch t { case 1: return "淘宝" case 2: return "拼多多" case 3: return "京东" case 4: return "线下" default: return "未知" } }