From 811ed3a41f66c1a097cdb0b914cd59a678ca5c55 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 6 May 2025 21:16:23 +0800 Subject: [PATCH] feat: support refund --- backend/app/http/admin/orders.go | 14 ++++++-- backend/app/http/admin/routes.gen.go | 5 +++ backend/app/http/posts.go | 12 ++++++- backend/config.toml | 2 +- backend/providers/wepay/pay.go | 44 ++++++++++++++++++++++---- frontend/admin/src/pages/OrderPage.vue | 36 ++++++++++++++++++++- 6 files changed, 101 insertions(+), 12 deletions(-) diff --git a/backend/app/http/admin/orders.go b/backend/app/http/admin/orders.go index 602832a..51afd9e 100644 --- a/backend/app/http/admin/orders.go +++ b/backend/app/http/admin/orders.go @@ -1,6 +1,8 @@ package admin import ( + "fmt" + "quyun/app/models" "quyun/app/requests" "quyun/database/fields" @@ -30,7 +32,7 @@ func (ctl *orders) List(ctx fiber.Ctx, pagination *requests.Pagination, query *O } // Refund -// @Router /admin/orders/{id}/refund [post] +// @Router /admin/orders/:id/refund [post] // @Bind id path func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error { order, err := models.Orders.GetByID(ctx.Context(), id) @@ -51,7 +53,15 @@ func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error { TransactionID(order.TransactionID). CNYRefundAmount(refundTotal, refundTotal). RefundReason("管理员退款"). - RefundGoodsInfo(post.Title) + RefundGoods([]wepay.RefundGoodsInfo{ + { + MerchantGoodsID: fmt.Sprintf("%d", order.PostID), + GoodsName: post.Title, + RefundQuantity: 1, + RefundAmount: refundTotal, + UnitPrice: order.Price, + }, + }) }) if err != nil { return err diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go index baf1ec3..26d81fb 100644 --- a/backend/app/http/admin/routes.gen.go +++ b/backend/app/http/admin/routes.gen.go @@ -63,6 +63,11 @@ func (r *Routes) Register(router fiber.Router) { Query[OrderListQuery]("query"), )) + router.Post("/admin/orders/:id/refund", Func1( + r.orders.Refund, + PathParam[int64]("id"), + )) + // 注册路由组: posts router.Get("/admin/posts", DataFunc2( r.posts.List, diff --git a/backend/app/http/posts.go b/backend/app/http/posts.go index d5220dc..05aed8d 100644 --- a/backend/app/http/posts.go +++ b/backend/app/http/posts.go @@ -2,6 +2,7 @@ package http import ( _ "embed" + "fmt" "strconv" "time" @@ -272,13 +273,22 @@ func (ctl *posts) Buy(ctx fiber.Ctx, id int64, user *model.Users) (*wechat.JSAPI return nil, errors.Wrap(err, "订单创建失败") } + payPrice := post.Price * int64(post.Discount) / 100 prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), func(bm *wepay.BodyMap) { bm. Expire(30 * time.Minute). Description(post.Title). OutTradeNo(order.OrderNo). Payer(user.OpenID). - CNYAmount(post.Price * int64(post.Discount) / 100) + CNYAmount(payPrice). + Detail([]wepay.GoodsInfo{ + { + GoodsName: post.Title, + UnitPrice: payPrice, + MerchantGoodsID: fmt.Sprintf("%d", post.ID), + Quantity: 1, + }, + }) }) if err != nil { log.Errorf("wepay.V3TransactionJsapi err: %v", err) diff --git a/backend/config.toml b/backend/config.toml index bbc5890..812883e 100644 --- a/backend/config.toml +++ b/backend/config.toml @@ -3,7 +3,7 @@ Mode = "development" # Mode = "prod" BaseURI = "baseURI" StoragePath = "/Users/rogee/Projects/self/quyun/fixtures" -DistAdmin = "frontend/wechat/admin" +DistAdmin = "frontend/admin/dist" DistWeChat = "frontend/wechat/dist" [Http] diff --git a/backend/providers/wepay/pay.go b/backend/providers/wepay/pay.go index 9d59222..30bd9c6 100644 --- a/backend/providers/wepay/pay.go +++ b/backend/providers/wepay/pay.go @@ -83,7 +83,7 @@ func (pay *PrepayData) PaySignOfJSAPI() (*wechat.JSAPIPayParams, error) { } func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOrderResponse, error) { - bm := NewBodyMap(c.config) + bm := NewRefundBodyMap(c.config) f(bm) resp, err := c.payClient.V3Refund(ctx, bm.bm) @@ -91,6 +91,11 @@ func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOr return nil, err } + if resp.Code != wechat.Success { + log.Errorf("WePay Refund error: %s", resp.Error) + return nil, errors.New(resp.Error) + } + return resp.Response, nil } @@ -184,6 +189,14 @@ type BodyMap struct { bm gopay.BodyMap } +func NewRefundBodyMap(c *w.Config) *BodyMap { + bm := make(gopay.BodyMap) + bm.Set("notify_url", c.Pay.NotifyURL) + return &BodyMap{ + bm: bm, + } +} + func NewBodyMap(c *w.Config) *BodyMap { bm := make(gopay.BodyMap) bm.Set("appid", c.AppID). @@ -253,13 +266,17 @@ func (b *BodyMap) CNYRefundAmount(total, refund int64) *BodyMap { return b.RefundAmount(total, refund, CNY) } +type RefundGoodsInfo struct { + MerchantGoodsID string `json:"merchant_goods_id"` + GoodsName string `json:"goods_name"` + RefundQuantity int64 `json:"refund_quantity"` + RefundAmount int64 `json:"refund_amount"` + UnitPrice int64 `json:"unit_price"` +} + // RefundGoodsInfo -func (b *BodyMap) RefundGoodsInfo(name string) *BodyMap { - return b.Set("goods_detail", []map[string]any{ - { - "goods_name": name, - }, - }) +func (b *BodyMap) RefundGoods(goods []RefundGoodsInfo) *BodyMap { + return b.Set("goods_detail", goods) } // Amount @@ -275,6 +292,19 @@ func (b *BodyMap) CNYAmount(total int64) *BodyMap { return b.Amount(total, CNY) } +type GoodsInfo struct { + MerchantGoodsID string `json:"merchant_goods_id"` + GoodsName string `json:"goods_name"` + Quantity int64 `json:"quantity"` + UnitPrice int64 `json:"unit_price"` +} + +func (b *BodyMap) Detail(goods []GoodsInfo) *BodyMap { + return b.SetBodyMap("detail", func(bm gopay.BodyMap) { + bm.Set("goods_detail", goods) + }) +} + // Payer func (b *BodyMap) Payer(spOpenId string) *BodyMap { return b.SetBodyMap("payer", func(bm gopay.BodyMap) { diff --git a/frontend/admin/src/pages/OrderPage.vue b/frontend/admin/src/pages/OrderPage.vue index 066b3fe..3713c67 100644 --- a/frontend/admin/src/pages/OrderPage.vue +++ b/frontend/admin/src/pages/OrderPage.vue @@ -2,6 +2,7 @@ import { orderService } from '@/api/orderService'; import { formatDate } from '@/utils/date'; import Badge from 'primevue/badge'; +import Button from 'primevue/button'; import Column from 'primevue/column'; import ConfirmDialog from 'primevue/confirmdialog'; import DataTable from 'primevue/datatable'; @@ -17,6 +18,7 @@ const confirm = useConfirm(); const globalFilterValue = ref(''); const loading = ref(false); +const refunding = ref(false); const searchTimeout = ref(null); const filters = ref({ global: { value: null, matchMode: 'contains' }, @@ -57,7 +59,31 @@ const getFinalPrice = (price, discount) => { return price - getDiscountAmount(price, discount); }; +const handleRefund = (id) => { + confirm.require({ + message: '确定要对此订单进行退款操作吗?', + header: '退款确认', + icon: 'pi pi-exclamation-triangle', + rejectProps: { + label: '取消', + icon: 'pi pi-times', + outlined: true, + size: 'small' + }, + acceptProps: { + label: '确认', + icon: 'pi pi-check', + size: 'small' + }, + acceptClass: 'p-button-success', + accept: () => { + refundOrder(id); + } + }); +}; + const refundOrder = async (id) => { + refunding.value = true; try { await orderService.refund(id) fetchOrders(); @@ -65,7 +91,7 @@ const refundOrder = async (id) => { console.error('Failed to refund orders:', error); toast.add({ severity: 'error', summary: '错误', detail: ' 退款失败', life: 3000 }); } finally { - loading.value = false; + refunding.value = false; } } @@ -194,6 +220,14 @@ onMounted(() => { + + +