feat: H5 沉默期延期微信支付改为微信内直接拉起(JSAPI)
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- H5 下单带 app_type 且选微信、商户为微信直连时,复用套餐购买的 JSAPI 预下单,返回 pay_config - 调起参数保存在支付单,延期订单详情可继续支付;查单、回调、超时关单、作废沿用原流程 - 后台生成二维码、支付宝、富友商户保持原方式
This commit is contained in:
@@ -45,6 +45,7 @@ type ExtensionService struct {
|
||||
runtime *merchantpayment.RuntimeLoader
|
||||
payments PaymentPorts
|
||||
refunder refundchannel.Refunder
|
||||
jsapi JSAPIPayer
|
||||
logger *zap.Logger
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
64
internal/application/packagesilence/online_jsapi.go
Normal file
64
internal/application/packagesilence/online_jsapi.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package packagesilence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// jsapiContentPrefix 标记支付单 qr_content 里保存的是微信 JSAPI 调起参数(JSON),而不是二维码 / 支付链接。
|
||||
const jsapiContentPrefix = "jsapi:"
|
||||
|
||||
// JSAPIPayer 为 C 端客户按已冻结商户创建微信 JSAPI 预下单(复用套餐购买的公众号 / 小程序支付)。
|
||||
type JSAPIPayer interface {
|
||||
CreateJSAPIPayConfig(ctx context.Context, customerID uint, appType string, config *model.WechatConfig,
|
||||
paymentNo, description string, amount int64) (*dto.ClientPayConfig, error)
|
||||
}
|
||||
|
||||
// SetJSAPIPayer 注入微信 JSAPI 预下单能力;未注入时 H5 微信延期仍走 H5 支付链接。
|
||||
func (s *ExtensionService) SetJSAPIPayer(payer JSAPIPayer) { s.jsapi = payer }
|
||||
|
||||
// useJSAPI 判断本次下单是否走微信内直接支付:H5 客户、微信支付、带应用类型、商户为微信直连(v3 / v2)。
|
||||
// 富友商户仍走扫码,避免查单与下单协议不一致。
|
||||
func (s *ExtensionService) useJSAPI(source string, req dto.SilenceOnlineOrderRequest, config *model.WechatConfig) bool {
|
||||
return s.jsapi != nil && source == constants.SilenceOrderSourceClient && req.PaymentMethod == model.PaymentByWechat &&
|
||||
strings.TrimSpace(req.AppType) != "" && config != nil &&
|
||||
(config.ProviderType == model.ProviderTypeWechat || config.ProviderType == model.ProviderTypeWechatV2)
|
||||
}
|
||||
|
||||
// createJSAPIContent 预下单并把调起参数编码成支付单内容,便于"继续支付"复用(预支付有效期 2 小时,长于支付截止)。
|
||||
func (s *ExtensionService) createJSAPIContent(ctx context.Context, customerID uint, appType string, config *model.WechatConfig, payment *model.Payment) (string, error) {
|
||||
payConfig, err := s.jsapi.CreateJSAPIPayConfig(ctx, customerID, appType, config, payment.PaymentNo, "沉默期延期", payment.Amount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if payConfig == nil || strings.TrimSpace(payConfig.PackageVal) == "" {
|
||||
return "", errors.New(errors.CodeWechatPayFailed, "微信支付未返回调起参数")
|
||||
}
|
||||
raw, err := json.Marshal(payConfig)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errors.CodeInternalError, err, "编码微信支付参数失败")
|
||||
}
|
||||
return jsapiContentPrefix + string(raw), nil
|
||||
}
|
||||
|
||||
// ApplyPaymentContent 把支付单内容写到响应:JSAPI 参数放 pay_config,其余(二维码 / 支付链接)放 qr_content。
|
||||
func ApplyPaymentContent(resp *dto.SilenceExtensionOrderResponse, content string) {
|
||||
if resp == nil || content == "" {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(content, jsapiContentPrefix) {
|
||||
var payConfig dto.ClientPayConfig
|
||||
if err := json.Unmarshal([]byte(strings.TrimPrefix(content, jsapiContentPrefix)), &payConfig); err == nil {
|
||||
resp.PayConfig = &payConfig
|
||||
resp.QRContent = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
resp.QRContent = content
|
||||
}
|
||||
57
internal/application/packagesilence/online_jsapi_test.go
Normal file
57
internal/application/packagesilence/online_jsapi_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package packagesilence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
type fakeJSAPIPayer struct{}
|
||||
|
||||
func (fakeJSAPIPayer) CreateJSAPIPayConfig(context.Context, uint, string, *model.WechatConfig, string, string, int64) (*dto.ClientPayConfig, error) {
|
||||
return &dto.ClientPayConfig{PackageVal: "prepay_id=wx1"}, nil
|
||||
}
|
||||
|
||||
func TestUseJSAPI(t *testing.T) {
|
||||
s := &ExtensionService{jsapi: fakeJSAPIPayer{}}
|
||||
wechatReq := dto.SilenceOnlineOrderRequest{PaymentMethod: model.PaymentByWechat, AppType: "official_account"}
|
||||
tests := []struct {
|
||||
name string
|
||||
svc *ExtensionService
|
||||
source string
|
||||
req dto.SilenceOnlineOrderRequest
|
||||
config *model.WechatConfig
|
||||
want bool
|
||||
}{
|
||||
{"H5 微信直连 v2", s, constants.SilenceOrderSourceClient, wechatReq, &model.WechatConfig{ProviderType: model.ProviderTypeWechatV2}, true},
|
||||
{"H5 微信直连 v3", s, constants.SilenceOrderSourceClient, wechatReq, &model.WechatConfig{ProviderType: model.ProviderTypeWechat}, true},
|
||||
{"富友仍走扫码", s, constants.SilenceOrderSourceClient, wechatReq, &model.WechatConfig{ProviderType: model.ProviderTypeFuiou}, false},
|
||||
{"后台二维码", s, constants.SilenceOrderSourceAdmin, wechatReq, &model.WechatConfig{ProviderType: model.ProviderTypeWechatV2}, false},
|
||||
{"未带应用类型", s, constants.SilenceOrderSourceClient, dto.SilenceOnlineOrderRequest{PaymentMethod: model.PaymentByWechat}, &model.WechatConfig{ProviderType: model.ProviderTypeWechatV2}, false},
|
||||
{"支付宝", s, constants.SilenceOrderSourceClient, dto.SilenceOnlineOrderRequest{PaymentMethod: model.PaymentByAlipay, AppType: "official_account"}, &model.WechatConfig{ProviderType: "alipay"}, false},
|
||||
{"未注入", &ExtensionService{}, constants.SilenceOrderSourceClient, wechatReq, &model.WechatConfig{ProviderType: model.ProviderTypeWechatV2}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.svc.useJSAPI(tt.source, tt.req, tt.config); got != tt.want {
|
||||
t.Fatalf("useJSAPI() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPaymentContent(t *testing.T) {
|
||||
resp := &dto.SilenceExtensionOrderResponse{}
|
||||
ApplyPaymentContent(resp, `jsapi:{"app_id":"wxA","package":"prepay_id=wx1","pay_sign":"S"}`)
|
||||
if resp.PayConfig == nil || resp.PayConfig.PackageVal != "prepay_id=wx1" || resp.PayConfig.AppID != "wxA" || resp.QRContent != "" {
|
||||
t.Fatalf("jsapi content not mapped: %+v", resp)
|
||||
}
|
||||
link := &dto.SilenceExtensionOrderResponse{}
|
||||
ApplyPaymentContent(link, "https://wx.tenpay.com/checkmweb?x=1")
|
||||
if link.QRContent != "https://wx.tenpay.com/checkmweb?x=1" || link.PayConfig != nil {
|
||||
t.Fatalf("link content not mapped: %+v", link)
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,20 @@ func (s *ExtensionService) CreateOnlineOrder(ctx context.Context, command Online
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if s.useJSAPI(source, req, config) {
|
||||
// H5 客户在微信内用微信支付:走 JSAPI 直接拉起,不生成 H5 支付链接(H5 链接在微信内无法支付)
|
||||
content, jsapiErr := s.createJSAPIContent(ctx, command.Scope.CustomerID, req.AppType, config, payment)
|
||||
if jsapiErr != nil {
|
||||
_ = s.closeOrder(ctx, order.ID, "微信支付参数生成失败")
|
||||
return nil, jsapiErr
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ? AND (qr_content IS NULL OR qr_content = '')", payment.ID, model.PaymentRecordStatusPending).
|
||||
Update("qr_content", content).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "保存支付内容失败")
|
||||
}
|
||||
return s.orderResponse(ctx, order, true)
|
||||
}
|
||||
paymentResult, err := adapter.CreatePaymentURL(ctx, agentrechargeApp.OnlinePaymentRequest{
|
||||
PaymentID: payment.ID, PaymentNo: payment.PaymentNo, CorrelationID: payment.PaymentNo,
|
||||
Description: "沉默期延期", Amount: payment.Amount, ExpireAt: *payment.ExpireAt,
|
||||
|
||||
@@ -67,8 +67,9 @@ func (s *ExtensionService) orderResponse(ctx context.Context, order *model.Packa
|
||||
resp.ThirdPartyTradeNo = payment.ThirdPartyTradeNo
|
||||
resp.PaymentNo = payment.PaymentNo
|
||||
}
|
||||
if includeQR && order.Status == constants.SilenceOrderStatusPendingPayment && payment.Status == model.PaymentRecordStatusPending && resp.QRContent == "" {
|
||||
resp.QRContent = payment.QRContent
|
||||
if includeQR && order.Status == constants.SilenceOrderStatusPendingPayment && payment.Status == model.PaymentRecordStatusPending &&
|
||||
resp.QRContent == "" && resp.PayConfig == nil {
|
||||
ApplyPaymentContent(resp, payment.QRContent)
|
||||
resp.PaymentNo = payment.PaymentNo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
deps.Redis,
|
||||
deps.Logger,
|
||||
)
|
||||
// 沉默期延期的 H5 微信支付复用套餐购买的 JSAPI 预下单(微信内直接拉起支付)
|
||||
if svc.PackageSilence != nil && svc.PackageSilence.Extensions != nil {
|
||||
svc.PackageSilence.Extensions.SetJSAPIPayer(clientOrderService)
|
||||
}
|
||||
systemConfigRegistry := deps.SystemConfigRegistry
|
||||
if systemConfigRegistry == nil {
|
||||
systemConfigRegistry = systemConfigInfra.NewRegistry()
|
||||
|
||||
@@ -159,6 +159,7 @@ type SilenceOnlineOrderRequest struct {
|
||||
PackageUsageID uint `json:"package_usage_id" validate:"required,min=1" required:"true" description:"套餐记录 ID"`
|
||||
Days int `json:"days" validate:"required,min=1,max=3650" required:"true" description:"输入的延期天数"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat alipay" required:"true" description:"支付方式:wechat / alipay(经商户池路由)"`
|
||||
AppType string `json:"app_type" validate:"omitempty,oneof=official_account miniapp" description:"H5 微信支付时的应用类型:official_account / miniapp;传入时在微信内直接拉起支付(JSAPI),返回 pay_config"`
|
||||
RequestID string `json:"request_id" validate:"required,min=1,max=64" required:"true" description:"下单幂等键"`
|
||||
EvidenceRefs []string `json:"evidence_refs" validate:"omitempty,max=10,dive,max=500" description:"客户申请记录或截图对象存储 Key(后台创建时填写)"`
|
||||
}
|
||||
@@ -178,51 +179,52 @@ type SilenceOfflineOrderRequest struct {
|
||||
|
||||
// SilenceExtensionOrderResponse 延期订单。
|
||||
type SilenceExtensionOrderResponse struct {
|
||||
ID uint `json:"id" description:"订单 ID"`
|
||||
OrderNo string `json:"order_no" description:"订单号"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
PackageUsageID uint `json:"package_usage_id" description:"套餐记录 ID"`
|
||||
PackageID uint `json:"package_id" description:"套餐 ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称快照"`
|
||||
AssetType string `json:"asset_type" description:"资产类型 iot_card / device"`
|
||||
AssetID uint `json:"asset_id" description:"资产 ID"`
|
||||
AssetIdentifier string `json:"asset_identifier" description:"资产标识快照"`
|
||||
ShopID uint `json:"shop_id" description:"下单时资产所属店铺"`
|
||||
CustomerID *uint `json:"customer_id,omitempty" description:"H5 下单的客户"`
|
||||
Source string `json:"source" description:"来源:client / admin / offline"`
|
||||
SourceName string `json:"source_name" description:"来源名称"`
|
||||
InputDays int `json:"input_days" description:"输入天数"`
|
||||
Months int `json:"months" description:"计费月数"`
|
||||
ExtensionDays int `json:"extension_days" description:"计费 / 延长天数"`
|
||||
UnitPrice int64 `json:"unit_price" description:"单价(分/月)"`
|
||||
Amount int64 `json:"amount" description:"应付金额(分)"`
|
||||
PolicyName string `json:"policy_name" description:"政策名称快照"`
|
||||
BeforeDeadlineAt time.Time `json:"before_deadline_at" description:"下单时截止时间"`
|
||||
ExpectedDeadlineAt time.Time `json:"expected_deadline_at" description:"预计新截止时间"`
|
||||
Status string `json:"status" description:"状态:pending_payment/pending_approval/rejected/paid/completed/closed/exception/refunded"`
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式:wechat/alipay/offline"`
|
||||
PaymentMerchantName string `json:"payment_merchant_name,omitempty" description:"收款商户名称(取已支付或已退款支付单快照)"`
|
||||
ThirdPartyTradeNo string `json:"third_party_trade_no,omitempty" description:"交易流水号"`
|
||||
PaymentNo string `json:"payment_no,omitempty" description:"支付单号"`
|
||||
QRContent string `json:"qr_content,omitempty" description:"支付内容(待支付时返回,前端渲染为二维码或跳转链接)"`
|
||||
OfflinePaymentMethodName string `json:"offline_payment_method_name,omitempty" description:"线下收款方式"`
|
||||
OfflineTradeNo string `json:"offline_trade_no,omitempty" description:"线下交易单号"`
|
||||
PaymentVoucherKeys []string `json:"payment_voucher_keys,omitempty" description:"线下收款凭证"`
|
||||
EvidenceRefs []string `json:"evidence_refs,omitempty" description:"申请依据"`
|
||||
ApprovalRequired bool `json:"approval_required" description:"是否需要审批"`
|
||||
ApprovalInstanceID *uint `json:"approval_instance_id,omitempty" description:"审批实例 ID"`
|
||||
PaidAmount *int64 `json:"paid_amount,omitempty" description:"实付金额(分)"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" description:"支付截止时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"延期生效时间"`
|
||||
ExceptionCode string `json:"exception_code,omitempty" description:"异常原因码"`
|
||||
ExceptionSummary string `json:"exception_summary,omitempty" description:"异常说明"`
|
||||
RefundNo string `json:"refund_no,omitempty" description:"原路退款单号(仅执行异常)"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty" description:"退款金额"`
|
||||
RefundedAt *time.Time `json:"refunded_at,omitempty" description:"退款时间"`
|
||||
CreatedBy *uint `json:"created_by,omitempty" description:"后台创建人"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
ID uint `json:"id" description:"订单 ID"`
|
||||
OrderNo string `json:"order_no" description:"订单号"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
PackageUsageID uint `json:"package_usage_id" description:"套餐记录 ID"`
|
||||
PackageID uint `json:"package_id" description:"套餐 ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称快照"`
|
||||
AssetType string `json:"asset_type" description:"资产类型 iot_card / device"`
|
||||
AssetID uint `json:"asset_id" description:"资产 ID"`
|
||||
AssetIdentifier string `json:"asset_identifier" description:"资产标识快照"`
|
||||
ShopID uint `json:"shop_id" description:"下单时资产所属店铺"`
|
||||
CustomerID *uint `json:"customer_id,omitempty" description:"H5 下单的客户"`
|
||||
Source string `json:"source" description:"来源:client / admin / offline"`
|
||||
SourceName string `json:"source_name" description:"来源名称"`
|
||||
InputDays int `json:"input_days" description:"输入天数"`
|
||||
Months int `json:"months" description:"计费月数"`
|
||||
ExtensionDays int `json:"extension_days" description:"计费 / 延长天数"`
|
||||
UnitPrice int64 `json:"unit_price" description:"单价(分/月)"`
|
||||
Amount int64 `json:"amount" description:"应付金额(分)"`
|
||||
PolicyName string `json:"policy_name" description:"政策名称快照"`
|
||||
BeforeDeadlineAt time.Time `json:"before_deadline_at" description:"下单时截止时间"`
|
||||
ExpectedDeadlineAt time.Time `json:"expected_deadline_at" description:"预计新截止时间"`
|
||||
Status string `json:"status" description:"状态:pending_payment/pending_approval/rejected/paid/completed/closed/exception/refunded"`
|
||||
StatusName string `json:"status_name" description:"状态名称"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式:wechat/alipay/offline"`
|
||||
PaymentMerchantName string `json:"payment_merchant_name,omitempty" description:"收款商户名称(取已支付或已退款支付单快照)"`
|
||||
ThirdPartyTradeNo string `json:"third_party_trade_no,omitempty" description:"交易流水号"`
|
||||
PaymentNo string `json:"payment_no,omitempty" description:"支付单号"`
|
||||
QRContent string `json:"qr_content,omitempty" description:"支付内容(待支付时返回,前端渲染为二维码或跳转链接)"`
|
||||
PayConfig *ClientPayConfig `json:"pay_config,omitempty" description:"微信内直接支付(JSAPI)参数,H5 带 app_type 下微信单时返回,前端用它拉起支付"`
|
||||
OfflinePaymentMethodName string `json:"offline_payment_method_name,omitempty" description:"线下收款方式"`
|
||||
OfflineTradeNo string `json:"offline_trade_no,omitempty" description:"线下交易单号"`
|
||||
PaymentVoucherKeys []string `json:"payment_voucher_keys,omitempty" description:"线下收款凭证"`
|
||||
EvidenceRefs []string `json:"evidence_refs,omitempty" description:"申请依据"`
|
||||
ApprovalRequired bool `json:"approval_required" description:"是否需要审批"`
|
||||
ApprovalInstanceID *uint `json:"approval_instance_id,omitempty" description:"审批实例 ID"`
|
||||
PaidAmount *int64 `json:"paid_amount,omitempty" description:"实付金额(分)"`
|
||||
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" description:"支付截止时间"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" description:"延期生效时间"`
|
||||
ExceptionCode string `json:"exception_code,omitempty" description:"异常原因码"`
|
||||
ExceptionSummary string `json:"exception_summary,omitempty" description:"异常说明"`
|
||||
RefundNo string `json:"refund_no,omitempty" description:"原路退款单号(仅执行异常)"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty" description:"退款金额"`
|
||||
RefundedAt *time.Time `json:"refunded_at,omitempty" description:"退款时间"`
|
||||
CreatedBy *uint `json:"created_by,omitempty" description:"后台创建人"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
// SilenceSpecialItem 特殊延期的一条明细。
|
||||
|
||||
@@ -136,7 +136,7 @@ func (q *Query) GetOrder(ctx context.Context, id uint, customerID uint) (*dto.Si
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询延期支付单失败")
|
||||
}
|
||||
if err == nil {
|
||||
item.QRContent = payment.QRContent
|
||||
packagesilenceapp.ApplyPaymentContent(item, payment.QRContent)
|
||||
item.PaymentNo = payment.PaymentNo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,3 +182,29 @@ func payFirstNonEmpty(values ...string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateJSAPIPayConfig 为 C 端客户按已选定(已冻结)的商户配置创建微信 JSAPI 预下单,返回前端调起支付的参数。
|
||||
// 供套餐以外的 C 端在线支付复用(如沉默期延期):openid 取客户在当前应用(公众号 / 小程序)下的授权。
|
||||
func (s *Service) CreateJSAPIPayConfig(ctx context.Context, customerID uint, appType string, config *model.WechatConfig,
|
||||
paymentNo, description string, amount int64) (*dto.ClientPayConfig, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig)
|
||||
}
|
||||
_, appID, err := s.loadWechatAuthorization(ctx, appType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openID, err := s.resolveCustomerOpenID(ctx, customerID, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider, err := s.newPaymentProvider(config, appID, appType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := provider.CreateJSAPIPayment(ctx, paymentNo, description, openID, int(amount))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildClientPayConfigFromResult(result), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user