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 listShippingOrdersRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Status *int32 `form:"status"` // 1待发货 2已发货 3已签收 4异常 UserID *int64 `form:"user_id"` BatchNo string `form:"batch_no"` ExpressNo string `form:"express_no"` StartDate string `form:"start_date"` EndDate string `form:"end_date"` } type ShippingOrderGroup struct { GroupKey string `json:"group_key"` // 分组键(用于批量操作) BatchNo string `json:"batch_no"` // 批次号 ExpressCode string `json:"express_code"` // 快递公司编码 ExpressNo string `json:"express_no"` // 运单号 Status int32 `json:"status"` // 状态(取最大值) Count int64 `json:"count"` // 商品数量 TotalPrice int64 `json:"total_price"` // 总价格 UserID int64 `json:"user_id"` // 用户ID UserNickname string `json:"user_nickname"` // 用户昵称 AddressID int64 `json:"address_id"` // 地址ID AddressInfo string `json:"address_info"` // 地址信息 ShippedAt *time.Time `json:"shipped_at,omitempty"` ReceivedAt *time.Time `json:"received_at,omitempty"` CreatedAt time.Time `json:"created_at"` RecordIDs []int64 `json:"record_ids"` // 发货记录ID列表 InventoryIDs []int64 `json:"inventory_ids"` // 资产ID列表 ProductIDs []int64 `json:"product_ids"` // 商品ID列表 Products []struct { ID int64 `json:"id"` Name string `json:"name"` Image string `json:"image"` Price int64 `json:"price"` } `json:"products"` // 商品详情列表 } type listShippingOrdersResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []*ShippingOrderGroup `json:"list"` } // ListShippingOrders 发货订单列表(按批次号/运单号聚合) // @Summary 发货订单列表 // @Description 按批次号或运单号聚合显示发货记录,支持筛选状态、用户、时间等 // @Tags 管理端.发货管理 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param page query int false "页码,默认1" // @Param page_size query int false "每页数量,最多100,默认20" // @Param status query int false "状态:1待发货 2已发货 3已签收 4异常" // @Param user_id query int false "用户ID" // @Param batch_no query string false "批次号" // @Param express_no query string false "运单号" // @Param start_date query string false "开始日期 2006-01-02" // @Param end_date query string false "结束日期 2006-01-02" // @Success 200 {object} listShippingOrdersResponse // @Failure 400 {object} code.Failure // @Router /api/admin/shipping/orders [get] func (h *handler) ListShippingOrders() core.HandlerFunc { return func(ctx core.Context) { req := new(listShippingOrdersRequest) rsp := new(listShippingOrdersResponse) 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.ShippingRecords.WithContext(ctx.RequestContext()).ReadDB() if req.Status != nil { q = q.Where(h.readDB.ShippingRecords.Status.Eq(*req.Status)) } if req.UserID != nil { q = q.Where(h.readDB.ShippingRecords.UserID.Eq(*req.UserID)) } if req.BatchNo != "" { q = q.Where(h.readDB.ShippingRecords.BatchNo.Eq(req.BatchNo)) } if req.ExpressNo != "" { q = q.Where(h.readDB.ShippingRecords.ExpressNo.Eq(req.ExpressNo)) } if req.StartDate != "" { if t, err := time.Parse("2006-01-02", req.StartDate); err == nil { q = q.Where(h.readDB.ShippingRecords.CreatedAt.Gte(t)) } } if req.EndDate != "" { if t, err := time.Parse("2006-01-02", req.EndDate); err == nil { t = t.Add(24 * time.Hour).Add(-time.Second) q = q.Where(h.readDB.ShippingRecords.CreatedAt.Lte(t)) } } // 获取所有符合条件的记录 rows, err := q.Order(h.readDB.ShippingRecords.CreatedAt.Desc(), h.readDB.ShippingRecords.ID.Desc()).Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30001, err.Error())) return } // 按批次号/运单号分组 type acc struct { status int32 totalPrice int64 shippedAt *time.Time receivedAt *time.Time createdAt time.Time userID int64 addressID int64 recordIDs []int64 inv []int64 pid []int64 } m := make(map[string]*acc) meta := make(map[string]struct{ code, no, batch string }) order := make([]string, 0) // 保持顺序 for _, r := range rows { // 分组优先级:运单号 > 批次号 > 记录ID key := "" if r.ExpressNo != "" { key = "E|" + r.ExpressCode + "|" + r.ExpressNo } else if r.BatchNo != "" { key = "B|" + r.BatchNo } else { key = "_" + strconv.FormatInt(r.ID, 10) } if _, ok := m[key]; !ok { m[key] = &acc{ createdAt: r.CreatedAt, userID: r.UserID, addressID: r.AddressID, } meta[key] = struct{ code, no, batch string }{r.ExpressCode, r.ExpressNo, r.BatchNo} order = append(order, key) } a := m[key] if a.status == 0 || r.Status >= a.status { a.status = r.Status } a.totalPrice += r.Price if !r.ShippedAt.IsZero() { t := r.ShippedAt a.shippedAt = &t } if !r.ReceivedAt.IsZero() { t := r.ReceivedAt a.receivedAt = &t } a.recordIDs = append(a.recordIDs, r.ID) a.inv = append(a.inv, r.InventoryID) if r.ProductID > 0 { a.pid = append(a.pid, r.ProductID) } } // 分页处理 total := int64(len(order)) start := (req.Page - 1) * req.PageSize end := start + req.PageSize if start >= len(order) { start = len(order) } if end > len(order) { end = len(order) } pageKeys := order[start:end] // 构建返回结果 items := make([]*ShippingOrderGroup, 0, len(pageKeys)) for _, k := range pageKeys { a := m[k] md := meta[k] // 获取用户信息 var userNickname string if a.userID > 0 { if user, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.Eq(a.userID)).First(); user != nil { userNickname = user.Nickname } } // 获取地址信息 var addressInfo string if a.addressID > 0 { if addr, _ := h.readDB.UserAddresses.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserAddresses.ID.Eq(a.addressID)).First(); addr != nil { addressInfo = addr.Province + addr.City + addr.District + addr.Address + " " + addr.Name + " " + addr.Mobile } } // 获取商品信息(去重) pidSet := make(map[int64]struct{}) for _, pid := range a.pid { pidSet[pid] = struct{}{} } var products []struct { ID int64 `json:"id"` Name string `json:"name"` Image string `json:"image"` Price int64 `json:"price"` } for pid := range pidSet { if prod, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(pid)).First(); prod != nil { products = append(products, struct { ID int64 `json:"id"` Name string `json:"name"` Image string `json:"image"` Price int64 `json:"price"` }{ ID: prod.ID, Name: prod.Name, Image: prod.ImagesJSON, // 商品图片JSON Price: prod.Price, }) } } items = append(items, &ShippingOrderGroup{ GroupKey: k, BatchNo: md.batch, ExpressCode: md.code, ExpressNo: md.no, Status: a.status, Count: int64(len(a.inv)), TotalPrice: a.totalPrice, UserID: a.userID, UserNickname: userNickname, AddressID: a.addressID, AddressInfo: addressInfo, ShippedAt: a.shippedAt, ReceivedAt: a.receivedAt, CreatedAt: a.createdAt, RecordIDs: a.recordIDs, InventoryIDs: a.inv, ProductIDs: a.pid, Products: products, }) } rsp.Page = req.Page rsp.PageSize = req.PageSize rsp.Total = total rsp.List = items ctx.Payload(rsp) } } type updateShippingRequest struct { RecordIDs []int64 `json:"record_ids"` // 发货记录ID列表 ExpressCode string `json:"express_code"` // 快递公司编码 ExpressNo string `json:"express_no"` // 运单号 Status *int32 `json:"status"` // 状态 } type updateShippingResponse struct { Success bool `json:"success"` UpdatedCount int64 `json:"updated_count"` } // UpdateShippingBatch 批量更新发货信息 // @Summary 批量更新发货信息 // @Description 为多条发货记录填写运单号或更新状态 // @Tags 管理端.发货管理 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param RequestBody body updateShippingRequest true "请求参数" // @Success 200 {object} updateShippingResponse // @Failure 400 {object} code.Failure // @Router /api/admin/shipping/orders/batch [put] func (h *handler) UpdateShippingBatch() core.HandlerFunc { return func(ctx core.Context) { req := new(updateShippingRequest) rsp := new(updateShippingResponse) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if len(req.RecordIDs) == 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "record_ids不能为空")) return } if len(req.RecordIDs) > 100 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "单次最多处理100条记录")) return } updates := make(map[string]any) if req.ExpressCode != "" { updates["express_code"] = req.ExpressCode } if req.ExpressNo != "" { updates["express_no"] = req.ExpressNo } if req.Status != nil { updates["status"] = *req.Status if *req.Status == 2 { updates["shipped_at"] = time.Now() } else if *req.Status == 3 { updates["received_at"] = time.Now() } } updates["updated_at"] = time.Now() result, err := h.writeDB.ShippingRecords.WithContext(ctx.RequestContext()). Where(h.writeDB.ShippingRecords.ID.In(req.RecordIDs...)). Updates(updates) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 30002, err.Error())) return } rsp.Success = true rsp.UpdatedCount = result.RowsAffected ctx.Payload(rsp) } } // GetShippingOrderDetail 获取发货订单详情 // @Summary 获取发货订单详情 // @Description 根据发货记录ID获取详情 // @Tags 管理端.发货管理 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param id path int true "发货记录ID" // @Success 200 {object} model.ShippingRecords // @Failure 400 {object} code.Failure // @Router /api/admin/shipping/orders/{id} [get] func (h *handler) GetShippingOrderDetail() core.HandlerFunc { return func(ctx core.Context) { idStr := ctx.Param("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的ID")) return } record, err := h.readDB.ShippingRecords.WithContext(ctx.RequestContext()).ReadDB(). Where(h.readDB.ShippingRecords.ID.Eq(id)).First() if err != nil || record == nil { ctx.AbortWithError(core.Error(http.StatusNotFound, 30003, "发货记录不存在")) return } // 获取关联信息 type detailResponse struct { *model.ShippingRecords User *model.Users `json:"user"` Address *model.UserAddresses `json:"address"` Product *model.Products `json:"product"` } rsp := &detailResponse{ShippingRecords: record} if record.UserID > 0 { rsp.User, _ = h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.Eq(record.UserID)).First() } if record.AddressID > 0 { rsp.Address, _ = h.readDB.UserAddresses.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserAddresses.ID.Eq(record.AddressID)).First() } if record.ProductID > 0 { rsp.Product, _ = h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(record.ProductID)).First() } ctx.Payload(rsp) } }