整改三期:退款、后台增改删、悬赏
退款原来只有会员那条路,而且只能全额、只改本地状态。现在代喂任务的退款 能分几次退到退完:每一笔单独记一行(谁退的、多少、走哪条通道、为什么), 订单上的 refunded_cent 跟着累加,退完且未结算的任务才关闭。渠道接通时按 金额原路退回并带独立幂等键,没接通或沙箱支付的登记为"线下已退"——假装 调用了渠道比说实话更糟。已结算的任务不能顺手退:钱在喂养者账上,要退只能 由平台承担,必须显式勾选。 后台从"只能通过或驳回"变成能改能删:宠物档案可代建、可编辑、可软删除 (有进行中任务的删不掉),送养与配种能改文案和城市、能下架能删,代喂任务 能改备注、能取消(托管中的钱必须先退)。每一个写操作都进审计日志。 悬赏按方案里的 A 做:主人自愿加的钱跟着任务一起托管,结算时全额给喂养者, 平台服务费只算在基础报酬上。抽悬赏是喂养者一眼能算出来的事。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ebf7189533
commit
a889cb42e4
@@ -234,14 +234,20 @@ func (a *App) settlePayment(ctx context.Context, notice paymentNotifyRequest, ra
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *App) createGatewayRefund(ctx context.Context, orderNo, providerOrderNo string, amountCent int) error {
|
||||
// createGatewayRefund asks the channel to send money back. refundNo is the
|
||||
// idempotency key: partial refunds happen more than once on the same order, so
|
||||
// keying on the order alone would make the second one a no-op at the provider.
|
||||
func (a *App) createGatewayRefund(ctx context.Context, orderNo, providerOrderNo, refundNo string, amountCent int) error {
|
||||
endpoint := a.configPlain(ctx, "payment.gateway.refund_url", "")
|
||||
if endpoint == "" || (a.config.Environment == "production" && !validHTTPSURL(endpoint)) {
|
||||
return fmt.Errorf("payment refund gateway is not configured")
|
||||
}
|
||||
if refundNo == "" {
|
||||
refundNo = "refund:" + orderNo
|
||||
}
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"orderNo": orderNo, "providerOrderNo": providerOrderNo, "amountCent": amountCent,
|
||||
"currency": "CNY", "reason": "admin_requested",
|
||||
"orderNo": orderNo, "providerOrderNo": providerOrderNo, "refundNo": refundNo,
|
||||
"amountCent": amountCent, "currency": "CNY", "reason": "admin_requested",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -253,7 +259,7 @@ func (a *App) createGatewayRefund(ctx context.Context, orderNo, providerOrderNo
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Authorization", "Bearer "+a.configPlain(ctx, "payment.gateway.token", ""))
|
||||
request.Header.Set("Idempotency-Key", "refund:"+orderNo)
|
||||
request.Header.Set("Idempotency-Key", refundNo)
|
||||
timeout, _ := strconv.Atoi(a.configPlain(ctx, "payment.gateway.timeout_seconds", "10"))
|
||||
if timeout < 3 || timeout > 30 {
|
||||
timeout = 10
|
||||
@@ -318,7 +324,7 @@ func (a *App) requestLiveRefund(w http.ResponseWriter, r *http.Request, orderID
|
||||
fail(w, http.StatusInternalServerError, 50001, "创建退款申请失败")
|
||||
return
|
||||
}
|
||||
if err = a.createGatewayRefund(r.Context(), orderNo, providerOrderNo, amountCent); err != nil {
|
||||
if err = a.createGatewayRefund(r.Context(), orderNo, providerOrderNo, fmt.Sprintf("refund:%d:full", orderID), amountCent); err != nil {
|
||||
a.audit(r, "refund_request_failed", "order", orderID, map[string]any{"userId": userID, "error": err.Error()})
|
||||
fail(w, http.StatusBadGateway, 50003, "退款网关请求失败,订单已保留为退款处理中,可安全重试")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user