diff --git a/backend/go.mod b/backend/go.mod index 6ffb4d0..04e4ccb 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module sale -go 1.24 +go 1.25.0 require ( github.com/PuerkitoBio/goquery v1.9.2 @@ -10,7 +10,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.3.0 github.com/joho/godotenv v1.5.1 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.53.0 gorm.io/driver/mysql v1.6.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 @@ -46,14 +46,20 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.7 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/excelize/v2 v2.11.0 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/backend/go.sum b/backend/go.sum index 6d91d9e..f4f357c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -87,6 +87,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= +github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -100,10 +104,19 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88= +github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= @@ -111,6 +124,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -120,11 +135,15 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -135,6 +154,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -146,6 +167,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/backend/internal/api/handlers/order.go b/backend/internal/api/handlers/order.go index d157c69..3251655 100644 --- a/backend/internal/api/handlers/order.go +++ b/backend/internal/api/handlers/order.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "math" "net/http" "strconv" @@ -11,6 +12,7 @@ import ( "sale/internal/utils" "github.com/gin-gonic/gin" + "github.com/xuri/excelize/v2" ) type CartHandler struct{} @@ -663,7 +665,99 @@ func (h *OrderHandler) Export(c *gin.Context) { utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User"). Order("created_at DESC").Find(&orders) - c.JSON(http.StatusOK, gin.H{"data": orders}) + // 创建 Excel 文件 + f := excelize.NewFile() + defer f.Close() + + sheetName := "订单列表" + f.SetSheetName("Sheet1", sheetName) + + // 设置表头 + headers := []string{"订单ID", "用户名", "订单状态", "商品信息", "商品金额", "运费", "服务费", "通道费", "包装费", "国际计算手续费", "订单总额", "收货人", "联系电话", "收货地址", "快递单号", "创建时间"} + for i, header := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheetName, cell, header) + } + + // 设置表头样式 + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true}, + Fill: excelize.Fill{Type: "pattern", Color: []string{"#4472C4"}, Pattern: 1}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + }) + f.SetRowStyle(sheetName, 1, 1, headerStyle) + + // 状态映射 + statusMap := map[string]string{ + "pending_payment": "待支付", "pending_confirm": "待确认", "pending_ship": "待发货", + "shipped": "已发货", "completed": "已完成", "refunding": "退款中", "refunded": "已退款", "cancelled": "已取消", + } + + // 填充数据 + for i, order := range orders { + row := i + 2 + + // 商品信息 + var productsStr string + for _, item := range order.OrderItems { + if productsStr != "" { + productsStr += "; " + } + productsStr += fmt.Sprintf("%s x%d", item.Product.Name, item.Quantity) + } + + // 收货地址 + var addressStr string + if order.ShippingAddress != nil { + addressStr = order.ShippingAddress.Province + order.ShippingAddress.City + order.ShippingAddress.District + order.ShippingAddress.Address + } + + // 填充单元格 + f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), order.ID) + f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), order.User.Username) + f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), statusMap[order.Status]) + f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), productsStr) + f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), order.Subtotal) + f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), order.ShippingFee) + f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), order.ServiceFee) + f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), order.ChannelFee) + f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), order.PackagingFee) + f.SetCellValue(sheetName, fmt.Sprintf("J%d", row), order.InternationalFee) + f.SetCellValue(sheetName, fmt.Sprintf("K%d", row), order.TotalAmount) + if order.ShippingAddress != nil { + f.SetCellValue(sheetName, fmt.Sprintf("L%d", row), order.ShippingAddress.Name) + f.SetCellValue(sheetName, fmt.Sprintf("M%d", row), order.ShippingAddress.Phone) + f.SetCellValue(sheetName, fmt.Sprintf("N%d", row), addressStr) + } + f.SetCellValue(sheetName, fmt.Sprintf("O%d", row), order.TrackingNumber) + f.SetCellValue(sheetName, fmt.Sprintf("P%d", row), order.CreatedAt.Format("2006-01-02 15:04:05")) + } + + // 设置列宽 + f.SetColWidth(sheetName, "A", "A", 10) + f.SetColWidth(sheetName, "B", "B", 15) + f.SetColWidth(sheetName, "C", "C", 10) + f.SetColWidth(sheetName, "D", "D", 40) + f.SetColWidth(sheetName, "E", "K", 12) + f.SetColWidth(sheetName, "L", "M", 15) + f.SetColWidth(sheetName, "N", "N", 40) + f.SetColWidth(sheetName, "O", "O", 20) + f.SetColWidth(sheetName, "P", "P", 20) + + // 设置响应头 + filename := fmt.Sprintf("orders_%s.xlsx", time.Now().Format("20060102150405")) + c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + c.Header("Content-Transfer-Encoding", "binary") + + // 写入响应 + buf, err := f.WriteToBuffer() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Excel file"}) + return + } + + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes()) } func (h *OrderHandler) CancelOrder(c *gin.Context) { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 73d8131..cb5b99e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -150,7 +150,7 @@ export const adminApi = { getOrders: (params?: any) => api.get('/admin/orders', { params }), getOrderById: (id: number) => api.get(`/admin/orders/${id}`), - exportOrders: () => api.get('/admin/orders/export'), + exportOrders: () => api.get('/admin/orders/export', { responseType: 'blob' }), processRefund: (id: number, data: any) => api.put(`/admin/orders/${id}/refund`, data), updateOrderStatus: (id: number, data: any) => api.put(`/admin/orders/${id}/status`, data), diff --git a/frontend/src/views/admin/Orders.vue b/frontend/src/views/admin/Orders.vue index a7267ad..37ac2e7 100644 --- a/frontend/src/views/admin/Orders.vue +++ b/frontend/src/views/admin/Orders.vue @@ -254,8 +254,21 @@ async function processRefund(id: number, status: string) { } async function exportOrders() { - await adminApi.exportOrders() - ElMessage.success('导出完成') + try { + const res = await adminApi.exportOrders() + const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `orders_${new Date().toISOString().slice(0, 10)}.xlsx` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + ElMessage.success('导出完成') + } catch { + ElMessage.error('导出失败') + } } onMounted(fetchOrders)