bindbox-game/internal/api/admin/shipping_orders_admin.go
邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

395 lines
12 KiB
Go
Raw Permalink 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 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)
}
}