feat: support refund
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user