refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
395 lines
12 KiB
Go
395 lines
12 KiB
Go
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)
|
||
}
|
||
}
|