feat(backend): 购买单取消透传 pay v2 cancel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -602,3 +602,58 @@ func (s *PayService) queryOrder(orderNo string) (*payOrderStatus, error) {
|
||||
}
|
||||
return &r.Data, nil
|
||||
}
|
||||
|
||||
// ---------- ⑤ 取消透传(防 pending 单堆积挤占 reconcile 每轮 50 条限额)----------
|
||||
|
||||
// CancelPurchase 取消本店一笔购买单:仅对 pending 单外呼 pay 取消;已终态/不存在均幂等无害。
|
||||
// pay 侧语义是 `UPDATE ... WHERE out_trade_no=? AND status='pending'`,canceled=RowsAffected>0:
|
||||
// - canceled=true:pay 确认取消(钱未扣/未入账),本地条件更新标 failed。
|
||||
// - canceled=false:已支付竞态(取消请求到达 pay 时单已被支付),本地必须保持 pending,
|
||||
// 等 webhook/reconcile 正常入账,不得误标 failed(否则钱已收但门店权益丢失)。
|
||||
func (s *PayService) CancelPurchase(shopID uint64, outTradeNo string) (bool, error) {
|
||||
var p model.LicensePurchase
|
||||
if err := s.db.Where("shop_id = ? AND out_trade_no = ?", shopID, outTradeNo).First(&p).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, ErrPurchaseNotFound
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if p.Status != "pending" { // 非 pending:幂等无害,不外呼 pay
|
||||
return false, nil
|
||||
}
|
||||
|
||||
canceled, err := s.cancelOrder(outTradeNo)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("pay 取消失败: %w", err)
|
||||
}
|
||||
if !canceled {
|
||||
return false, nil
|
||||
}
|
||||
if err := s.db.Model(&model.LicensePurchase{}).Where("id = ? AND status = 'pending'", p.ID).
|
||||
Update("status", "failed").Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// cancelOrder 调 pay 取消单(无签名无请求体)。
|
||||
func (s *PayService) cancelOrder(orderNo string) (bool, error) {
|
||||
resp, err := s.client.Post(s.baseURL+"/api/v2/orders/"+orderNo+"/cancel", "application/json", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("pay HTTP %d", resp.StatusCode)
|
||||
}
|
||||
var r struct {
|
||||
Data struct {
|
||||
Canceled bool `json:"canceled"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return r.Data.Canceled, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user