feat: support refund

This commit is contained in:
Rogee
2025-05-06 21:16:23 +08:00
parent 533c9b70af
commit 811ed3a41f
6 changed files with 101 additions and 12 deletions

View File

@@ -1,6 +1,8 @@
package admin package admin
import ( import (
"fmt"
"quyun/app/models" "quyun/app/models"
"quyun/app/requests" "quyun/app/requests"
"quyun/database/fields" "quyun/database/fields"
@@ -30,7 +32,7 @@ func (ctl *orders) List(ctx fiber.Ctx, pagination *requests.Pagination, query *O
} }
// Refund // Refund
// @Router /admin/orders/{id}/refund [post] // @Router /admin/orders/:id/refund [post]
// @Bind id path // @Bind id path
func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error { func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error {
order, err := models.Orders.GetByID(ctx.Context(), id) 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). TransactionID(order.TransactionID).
CNYRefundAmount(refundTotal, refundTotal). CNYRefundAmount(refundTotal, refundTotal).
RefundReason("管理员退款"). 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 { if err != nil {
return err return err

View File

@@ -63,6 +63,11 @@ func (r *Routes) Register(router fiber.Router) {
Query[OrderListQuery]("query"), Query[OrderListQuery]("query"),
)) ))
router.Post("/admin/orders/:id/refund", Func1(
r.orders.Refund,
PathParam[int64]("id"),
))
// 注册路由组: posts // 注册路由组: posts
router.Get("/admin/posts", DataFunc2( router.Get("/admin/posts", DataFunc2(
r.posts.List, r.posts.List,

View File

@@ -2,6 +2,7 @@ package http
import ( import (
_ "embed" _ "embed"
"fmt"
"strconv" "strconv"
"time" "time"
@@ -272,13 +273,22 @@ func (ctl *posts) Buy(ctx fiber.Ctx, id int64, user *model.Users) (*wechat.JSAPI
return nil, errors.Wrap(err, "订单创建失败") return nil, errors.Wrap(err, "订单创建失败")
} }
payPrice := post.Price * int64(post.Discount) / 100
prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), func(bm *wepay.BodyMap) { prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), func(bm *wepay.BodyMap) {
bm. bm.
Expire(30 * time.Minute). Expire(30 * time.Minute).
Description(post.Title). Description(post.Title).
OutTradeNo(order.OrderNo). OutTradeNo(order.OrderNo).
Payer(user.OpenID). 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 { if err != nil {
log.Errorf("wepay.V3TransactionJsapi err: %v", err) log.Errorf("wepay.V3TransactionJsapi err: %v", err)

View File

@@ -3,7 +3,7 @@ Mode = "development"
# Mode = "prod" # Mode = "prod"
BaseURI = "baseURI" BaseURI = "baseURI"
StoragePath = "/Users/rogee/Projects/self/quyun/fixtures" StoragePath = "/Users/rogee/Projects/self/quyun/fixtures"
DistAdmin = "frontend/wechat/admin" DistAdmin = "frontend/admin/dist"
DistWeChat = "frontend/wechat/dist" DistWeChat = "frontend/wechat/dist"
[Http] [Http]

View File

@@ -83,7 +83,7 @@ func (pay *PrepayData) PaySignOfJSAPI() (*wechat.JSAPIPayParams, error) {
} }
func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOrderResponse, error) { func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOrderResponse, error) {
bm := NewBodyMap(c.config) bm := NewRefundBodyMap(c.config)
f(bm) f(bm)
resp, err := c.payClient.V3Refund(ctx, bm.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 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 return resp.Response, nil
} }
@@ -184,6 +189,14 @@ type BodyMap struct {
bm gopay.BodyMap 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 { func NewBodyMap(c *w.Config) *BodyMap {
bm := make(gopay.BodyMap) bm := make(gopay.BodyMap)
bm.Set("appid", c.AppID). bm.Set("appid", c.AppID).
@@ -253,13 +266,17 @@ func (b *BodyMap) CNYRefundAmount(total, refund int64) *BodyMap {
return b.RefundAmount(total, refund, CNY) 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 // RefundGoodsInfo
func (b *BodyMap) RefundGoodsInfo(name string) *BodyMap { func (b *BodyMap) RefundGoods(goods []RefundGoodsInfo) *BodyMap {
return b.Set("goods_detail", []map[string]any{ return b.Set("goods_detail", goods)
{
"goods_name": name,
},
})
} }
// Amount // Amount
@@ -275,6 +292,19 @@ func (b *BodyMap) CNYAmount(total int64) *BodyMap {
return b.Amount(total, CNY) 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 // Payer
func (b *BodyMap) Payer(spOpenId string) *BodyMap { func (b *BodyMap) Payer(spOpenId string) *BodyMap {
return b.SetBodyMap("payer", func(bm gopay.BodyMap) { return b.SetBodyMap("payer", func(bm gopay.BodyMap) {

View File

@@ -2,6 +2,7 @@
import { orderService } from '@/api/orderService'; import { orderService } from '@/api/orderService';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import Badge from 'primevue/badge'; import Badge from 'primevue/badge';
import Button from 'primevue/button';
import Column from 'primevue/column'; import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog'; import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable'; import DataTable from 'primevue/datatable';
@@ -17,6 +18,7 @@ const confirm = useConfirm();
const globalFilterValue = ref(''); const globalFilterValue = ref('');
const loading = ref(false); const loading = ref(false);
const refunding = ref(false);
const searchTimeout = ref(null); const searchTimeout = ref(null);
const filters = ref({ const filters = ref({
global: { value: null, matchMode: 'contains' }, global: { value: null, matchMode: 'contains' },
@@ -57,7 +59,31 @@ const getFinalPrice = (price, discount) => {
return price - getDiscountAmount(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) => { const refundOrder = async (id) => {
refunding.value = true;
try { try {
await orderService.refund(id) await orderService.refund(id)
fetchOrders(); fetchOrders();
@@ -65,7 +91,7 @@ const refundOrder = async (id) => {
console.error('Failed to refund orders:', error); console.error('Failed to refund orders:', error);
toast.add({ severity: 'error', summary: '错误', detail: ' 退款失败', life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: ' 退款失败', life: 3000 });
} finally { } finally {
loading.value = false; refunding.value = false;
} }
} }
@@ -194,6 +220,14 @@ onMounted(() => {
</div> </div>
</template> </template>
</Column> </Column>
<Column field="actions" header="操作">
<template #body="{ data }">
<Button v-if="data.status === 7" icon="pi pi-replay" severity="danger" class="text-nowrap!"
size="small" @click="handleRefund(data.id)" :loading="refunding">
退款
</Button>
</template>
</Column>
</DataTable> </DataTable>
</div> </div>
</div> </div>