goalipay

package module
v0.0.0-...-a27f261 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2024 License: MIT Imports: 18 Imported by: 0

README

goalipay

golang封装支付宝支付
术语:
    应用私钥
    应用公钥
    支付宝公钥
    

查询订单

package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
	"github.com/jellycheng/gosupport/xcrypto"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	cfg.IsVerifySign = true
	pubK, _ := gosupport.FileGetContents("支付宝公钥文件地址")
	pubKStr := xcrypto.RsaContent2PublicKey(pubK)
	var e error
	cfg.AliPayPublicKey, e = xcrypto.DecodePublicKey([]byte(pubKStr))
	if e != nil {
		fmt.Println(e)
		return
	}
	// 请求参数
	bizMap := gosupport.NewBodyMap()
	bizMap.Set("out_trade_no", "GZ20190908174341CjDk") // 商户订单号
	if resData, bodyStr, err := goalipay.TradeQuery(cfg, bizMap, make(gosupport.BodyMap)); err == nil {
		fmt.Println(bodyStr)
		fmt.Println(fmt.Sprintf("%+v", resData.Response))
		if goalipay.IsError(resData.Response.Code) {
			fmt.Println("查询错误:", resData.Response.Msg)
		} else { // 订单查询结果
			fmt.Println(fmt.Sprintf("订单号: %s, 订单金额:%s,", resData.Response.OutTradeNo, resData.Response.TotalAmount))
		}
	} else {
		fmt.Println(err)
	}
}


发起退款

package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
	"github.com/jellycheng/gosupport/xcrypto"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	cfg.IsVerifySign = true
	pubK, _ := gosupport.FileGetContents("支付宝公钥文件地址")
	pubKStr := xcrypto.RsaContent2PublicKey(pubK)
	var e error
	cfg.AliPayPublicKey, e = xcrypto.DecodePublicKey([]byte(pubKStr))
	if e != nil {
		fmt.Println(e)
		return
	}
	// 请求参数
	bizMap := gosupport.NewBodyMap()
	// 要退款的正向订单号和退款金额
	bizMap.Set("out_trade_no", "GZ20190908174341CjDk").Set("refund_amount", 0.01)
	bizMap.Set("out_request_no", "orn2023" + gosupport.GetRandString(6)) // 退款请求号,唯一
	if resData, bodyStr, err := goalipay.TradeRefund(cfg, bizMap, make(gosupport.BodyMap)); err == nil {
		fmt.Println(bodyStr)
		fmt.Println(fmt.Sprintf("%+v", resData.Response))
		if goalipay.IsError(resData.Response.Code) {
			fmt.Println("发起退款错误:", resData.Response.Msg)
		} else { // 退款发起成功,同步知道退款结果
			fmt.Println(fmt.Sprintf("订单号: %s, 退款金额:%s,", resData.Response.OutTradeNo, resData.Response.RefundFee))
		}
	} else {
		fmt.Println(err)
	}
}

https://opendocs.alipay.com/open/6c0cdd7d_alipay.trade.refund?scene=common&pathHash=4081e89c

退款结果查询

package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
	"github.com/jellycheng/gosupport/xcrypto"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	cfg.IsVerifySign = true
	pubK, _ := gosupport.FileGetContents("支付宝公钥文件地址")
	pubKStr := xcrypto.RsaContent2PublicKey(pubK)
	var e error
	cfg.AliPayPublicKey, e = xcrypto.DecodePublicKey([]byte(pubKStr))
	if e != nil {
		fmt.Println(e)
		return
	}
	// 请求参数
	bizMap := gosupport.NewBodyMap()
	// 支付订单号和退款单号
	bizMap.Set("out_trade_no", "GZ201909081743JVZkjB").Set("out_request_no", "orn2023abc")
	if resData, bodyStr, err := goalipay.TradeFastpayRefundQuery(cfg, bizMap, make(gosupport.BodyMap)); err == nil {
		fmt.Println(bodyStr)
		fmt.Println(fmt.Sprintf("%+v", resData.Response))
		if goalipay.IsError(resData.Response.Code) {
			fmt.Println("退款查询错误:", resData.Response.Msg)
		} else { // 退款查询结果
			fmt.Println(fmt.Sprintf("支付订单号: %s,退款单号: %s, 退款金额:%s,", resData.Response.OutTradeNo, resData.Response.OutRequestNo, resData.Response.RefundAmount))
		}
	} else {
		fmt.Println(err)
	}
}

pc支付文档

拼接pc web支付地址
package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 请求业务参数
	bizMap := gosupport.NewBodyMap()
	bizMap.Set("subject", "购买商品").
		Set("out_trade_no", "GZ201909081743"+gosupport.GetRandString(6)).
		Set("total_amount", "0.01")

	// 公共参数,设置通知地址和跳转地址
	commonMap := gosupport.NewBodyMap()
	commonMap.Set("notify_url", "https://支付成功通知地址").
		Set("return_url", "https://支付成功pc页面跳转地址")

	if pcUrl, err := goalipay.PagePay(cfg, bizMap, commonMap); err == nil {
		fmt.Println(pcUrl)
	} else {
		fmt.Println(err)
	}
}

支付成功后在跳转地址中追加参数如下:
    domain?charset=utf-8&out_trade_no=商户订单号&method=alipay.trade.page.pay.return&total_amount=支付金额&sign=签名&trade_no=支付宝订单号&auth_app_id=应用ID&version=1.0&app_id=应用ID&sign_type=RSA2&seller_id=购买者&timestamp=时间

https://opendocs.alipay.com/open/repo-0038oa

手机支付文档

拼接h5地址
package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 请求参数
	bizMap := gosupport.NewBodyMap()
	bizMap.Set("subject", "购买商品").
		Set("out_trade_no", "ono20230824"+gosupport.GetRandString(6)).
		Set("total_amount", "0.02")

	commonMap := gosupport.NewBodyMap()
	commonMap.Set("notify_url", "https://支付成功通知地址").
		Set("return_url", "https://支付成功h5页面跳转地址")

	if h5Url, err := goalipay.WapPay(cfg, bizMap, commonMap); err == nil {
		fmt.Println(h5Url)
	} else {
		fmt.Println(err)
	}
}

支付成功h5页面跳转地址追加参数如下:
    domain?charset=utf-8&out_trade_no=商户订单号&method=alipay.trade.wap.pay.return&total_amount=金额&sign=签名&trade_no=支付宝单号&auth_app_id=应用ID&version=1.0&app_id=应用ID&sign_type=RSA2&seller_id=购买者ID&timestamp=时间

https://opendocs.alipay.com/open/repo-0038v7

app支付文档

package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
)

func main() {
	appid := "2018091561417031" //应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 请求参数
	bizMap := gosupport.NewBodyMap()
	bizMap.Set("subject", "购买商品").
		Set("out_trade_no", "GZ201909081743JVZkjB").
		Set("total_amount", 0.01)

	commonMap := gosupport.NewBodyMap()
	commonMap.Set("notify_url", "https://支付成功通知地址")
	if resData, err := goalipay.AppPay(cfg, bizMap, commonMap); err == nil {
		fmt.Println(resData) //传给app的内容
	} else {
		fmt.Println(err)
	}
}

https://opendocs.alipay.com/open/repo-0038v9

查询支付宝商户号余额

package main

import (
	"fmt"
	"github.com/jellycheng/goalipay"
	"github.com/jellycheng/gosupport"
	"github.com/jellycheng/gosupport/xcrypto"
)

func main() {
	appid := "" // 开放平台创建的应用ID
	pk, _ := gosupport.FileGetContents("应用私钥文件地址")
	cfg, err := goalipay.NewClient(appid, pk)
	if err != nil {
		fmt.Println(err)
		return
	}
	cfg.IsVerifySign = true
	pubK, _ := gosupport.FileGetContents("支付宝公钥文件地址")
	pubKStr := xcrypto.RsaContent2PublicKey(pubK)
	var e error
	cfg.AliPayPublicKey, e = xcrypto.DecodePublicKey([]byte(pubKStr))
	if e != nil {
		fmt.Println(e)
		return
	}
	// 请求参数
	bizMap := make(gosupport.BodyMap)
	if resData, bodyStr, err := goalipay.BalanceQuery(cfg, bizMap, make(gosupport.BodyMap)); err == nil {
		fmt.Println("支付宝返回原始内容:", bodyStr)
		if goalipay.IsError(resData.Response.Code) {
			fmt.Println("查询错误:", resData.Response.Msg)
		} else { // 查询正确
			fmt.Println(fmt.Sprintf("查询结果: %+v", resData.Response))
		}
	} else {
		fmt.Println(err)
	}

}


工具


支付宝提供密钥工具:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438
api文档:https://open.alipay.com/api

Documentation

Index

Constants

View Source
const (
	// 签名算法
	RSA  = "RSA"
	RSA2 = "RSA2"
	SM2  = "SM2"

	JsonFormat = "JSON"
	UTF8       = "utf-8"

	SUCCESS = "success"
	FAIL    = "fail"
	OK      = "ok"

	AlipayGatewayUrl = "https://openapi.alipay.com/gateway.do"
)

Variables

View Source
var (
	MissParamErr  = errors.New("缺少必需的参数")
	SignErr       = errors.New("签名错误")
	VerifySignErr = errors.New("签名验证错误")
)

Functions

func AppPay

func AppPay(config *AplipayConfig, bizMap gosupport.BodyMap, commonMap gosupport.BodyMap) (string, error)

获取app下单参数:https://opendocs.alipay.com/open/cd12c885_alipay.trade.app.pay?scene=20&pathHash=c0e35284 alipay.trade.app.pay(app支付接口2.0)

func CommonParamsHandle

func CommonParamsHandle(alipayCfg *AplipayConfig, bizMap gosupport.BodyMap, commonMap gosupport.BodyMap) (gosupport.BodyMap, error)

公共参数

func EncodeAliPaySignParams

func EncodeAliPaySignParams(bm gosupport.BodyMap) string

func GetAlipaySign

func GetAlipaySign(bm gosupport.BodyMap, signType string, privateKey *rsa.PrivateKey) (sign string, err error)

func IsError

func IsError(code string) bool

func NotifyContentToBodyMapByUrlValues

func NotifyContentToBodyMapByUrlValues(valMap url.Values) gosupport.BodyMap

func PagePay

func PagePay(config *AplipayConfig, bizMap gosupport.BodyMap, commonMap gosupport.BodyMap) (string, error)

pc端下单地址

func PinBizContent

func PinBizContent(bizmap gosupport.BodyMap) string

func ReturnSuccess

func ReturnSuccess() string

func VerifyNotifySign

func VerifyNotifySign(notifyContent interface{}, aliPayPublicKey *rsa.PublicKey) (bool, error)

func VerifySignByCert

func VerifySignByCert(sign, signData string, signType string, aliPayPublicKey *rsa.PublicKey) error

验证api数据:https://opendocs.alipay.com/common/02mse7#自行实现验签

func WapPay

func WapPay(config *AplipayConfig, bizMap gosupport.BodyMap, commonMap gosupport.BodyMap) (string, error)

获取h5下单链接地址

Types

type AplipayConfig

type AplipayConfig struct {
	AppId              string
	SignType           string
	Charset            string
	PrivateKey         *rsa.PrivateKey // 应用私钥
	AliPayPublicKey    *rsa.PublicKey  // 支付宝证书公钥内容
	AliPayPublicCertSN string
	ReturnUrl          string
	NotifyUrl          string
	Format             string
	IsVerifySign       bool // 是否验证支付宝响应结果签名
}

func NewClient

func NewClient(appid, privateKey string) (*AplipayConfig, error)

func (*AplipayConfig) AutoVerifySignByCert

func (m *AplipayConfig) AutoVerifySignByCert(sign, signData string, signDataErr error) (err error)

func (*AplipayConfig) GetSignData

func (m *AplipayConfig) GetSignData(bodyStr string, alipayCertSN string) (signData string, err error)

func (*AplipayConfig) SetCharset

func (m *AplipayConfig) SetCharset(charset string) *AplipayConfig

设置编码格式

func (*AplipayConfig) SetNotifyUrl

func (m *AplipayConfig) SetNotifyUrl(url string) *AplipayConfig

设置支付宝通知地址

func (*AplipayConfig) SetReturnUrl

func (m *AplipayConfig) SetReturnUrl(url string) *AplipayConfig

设置支付后的ReturnUrl

func (*AplipayConfig) SetSignType

func (m *AplipayConfig) SetSignType(signType string) *AplipayConfig

type BalanceQueryDto

type BalanceQueryDto struct {
	ErrorResponseDto
	FreezeAmount    string `json:"freeze_amount"`           //冻结金额,单位(元)
	TotalAmount     string `json:"total_amount"`            //支付宝账户余额,单位(元)
	AvailableAmount string `json:"available_amount"`        //账户可用余额,单位(元)
	SettleAmount    string `json:"settle_amount,omitempty"` //待结算金额,单位(元),可选
}

type BalanceQueryResponseDto

type BalanceQueryResponseDto struct {
	Response     *BalanceQueryDto `json:"alipay_data_bill_balance_query_response"`
	AlipayCertSn string           `json:"alipay_cert_sn,omitempty"`
	SignData     string           `json:"-"`
	Sign         string           `json:"sign"`
}

账户余额查询响应参数

func BalanceQuery

func BalanceQuery(config *AplipayConfig, bizMap gosupport.BodyMap, commonMap gosupport.BodyMap) (*BalanceQueryResponseDto, string, error)

账户余额查询: https://opendocs.alipay.com/open/01inen#查询账户当前余额

type DepositBackInfo

type DepositBackInfo struct {
	HasDepositBack     string `json:"has_deposit_back,omitempty"`
	DbackStatus        string `json:"dback_status,omitempty"`
	DbackAmount        string `json:"dback_amount,omitempty"`
	BankAckTime        string `json:"bank_ack_time,omitempty"`
	EstBankReceiptTime string `json:"est_bank_receipt_time,omitempty"`
}

type ErrorResponseDto

type ErrorResponseDto struct {
	Code    string `json:"code"`
	Msg     string `json:"msg"`
	SubCode string `json:"sub_code,omitempty"`
	SubMsg  string `json:"sub_msg,omitempty"`
}

type HbFqPayInfo

type HbFqPayInfo struct {
	UserInstallNum   string `json:"user_install_num,omitempty"`
	CreditPayMode    string `json:"credit_pay_mode,omitempty"`
	CreditBizOrderId string `json:"credit_biz_order_id,omitempty"`
	HybAmount        string `json:"hyb_amount,omitempty"`
}

type RefundPresetPaytool

type RefundPresetPaytool struct {
	Amount         []string `json:"amount,omitempty"`
	AssertTypeCode string   `json:"assert_type_code,omitempty"`
}

type RefundRoyalty

type RefundRoyalty struct {
	RefundAmount  string `json:"refund_amount,omitempty"`
	RoyaltyType   string `json:"royalty_type,omitempty"`
	ResultCode    string `json:"result_code,omitempty"`
	TransOut      string `json:"trans_out,omitempty"`
	TransOutEmail string `json:"trans_out_email,omitempty"`
	TransIn       string `json:"trans_in,omitempty"`
	TransInEmail  string `json:"trans_in_email,omitempty"`
}

type TradeFastpayRefundQueryResponseDto

type TradeFastpayRefundQueryResponseDto struct {
	Response     *TradeRefundQueryDto `json:"alipay_trade_fastpay_refund_query_response"`
	AlipayCertSn string               `json:"alipay_cert_sn,omitempty"`
	SignData     string               `json:"-"`
	Sign         string               `json:"sign"`
}

退款查询返回

type TradeFundBill

type TradeFundBill struct {
	FundChannel string `json:"fund_channel,omitempty"` // 同步通知里是 fund_channel
	Amount      string `json:"amount,omitempty"`
	RealAmount  string `json:"real_amount,omitempty"`
	FundType    string `json:"fund_type,omitempty"`
}

退款查询、交易查询 返回

type TradeQueryDto

type TradeQueryDto struct {
	ErrorResponseDto
	TradeNo             string           `json:"trade_no,omitempty"`
	OutTradeNo          string           `json:"out_trade_no,omitempty"`
	BuyerLogonId        string           `json:"buyer_logon_id,omitempty"`
	TradeStatus         string           `json:"trade_status,omitempty"`
	TotalAmount         string           `json:"total_amount,omitempty"`
	TransCurrency       string           `json:"trans_currency,omitempty"`
	SettleCurrency      string           `json:"settle_currency,omitempty"`
	SettleAmount        string           `json:"settle_amount,omitempty"`
	PayCurrency         string           `json:"pay_currency,omitempty"`
	PayAmount           string           `json:"pay_amount,omitempty"`
	SettleTransRate     string           `json:"settle_trans_rate,omitempty"`
	TransPayRate        string           `json:"trans_pay_rate,omitempty"`
	BuyerPayAmount      string           `json:"buyer_pay_amount,omitempty"`
	PointAmount         string           `json:"point_amount,omitempty"`
	InvoiceAmount       string           `json:"invoice_amount,omitempty"`
	SendPayDate         string           `json:"send_pay_date,omitempty"`
	ReceiptAmount       string           `json:"receipt_amount,omitempty"`
	StoreId             string           `json:"store_id,omitempty"`
	TerminalId          string           `json:"terminal_id,omitempty"`
	FundBillList        []*TradeFundBill `json:"fund_bill_list"`
	StoreName           string           `json:"store_name,omitempty"`
	BuyerUserId         string           `json:"buyer_user_id,omitempty"`
	ChargeAmount        string           `json:"charge_amount,omitempty"`
	ChargeFlags         string           `json:"charge_flags,omitempty"`
	SettlementId        string           `json:"settlement_id,omitempty"`
	TradeSettleInfo     *TradeSettleInfo `json:"trade_settle_info,omitempty"`
	AuthTradePayMode    string           `json:"auth_trade_pay_mode,omitempty"`
	BuyerUserType       string           `json:"buyer_user_type,omitempty"`
	MdiscountAmount     string           `json:"mdiscount_amount,omitempty"`
	DiscountAmount      string           `json:"discount_amount,omitempty"`
	Subject             string           `json:"subject,omitempty"`
	Body                string           `json:"body,omitempty"`
	AlipaySubMerchantId string           `json:"alipay_sub_merchant_id,omitempty"`
	ExtInfos            string           `json:"ext_infos,omitempty"`
	HbFqPayInfo         *HbFqPayInfo     `json:"hb_fq_pay_info,omitempty"`
	CreditPayMode       string           `json:"credit_pay_mode,omitempty"`
	CreditBizOrderId    string           `json:"credit_biz_order_id,omitempty"`
}

type TradeQueryResponseDto

type TradeQueryResponseDto struct {
	Response     *TradeQueryDto `json:"alipay_trade_query_response"`
	AlipayCertSn string         `json:"alipay_cert_sn,omitempty"`
	SignData     string         `json:"-"`
	Sign         string         `json:"sign"`
}

type TradeRefundDto

type TradeRefundDto struct {
	ErrorResponseDto
	TradeNo                      string                 `json:"trade_no,omitempty"`
	OutTradeNo                   string                 `json:"out_trade_no,omitempty"`
	BuyerLogonId                 string                 `json:"buyer_logon_id,omitempty"`
	FundChange                   string                 `json:"fund_change,omitempty"`
	RefundFee                    string                 `json:"refund_fee,omitempty"`
	RefundDetailItemList         []*TradeFundBill       `json:"refund_detail_item_list,omitempty"`
	StoreName                    string                 `json:"store_name,omitempty"`
	BuyerUserId                  string                 `json:"buyer_user_id,omitempty"`
	SendBackFee                  string                 `json:"send_back_fee,omitempty"`
	OpenId                       string                 `json:"open_id,omitempty"`
	RefundCurrency               string                 `json:"refund_currency,omitempty"`
	GmtRefundPay                 string                 `json:"gmt_refund_pay,omitempty"`
	RefundPresetPaytoolList      []*RefundPresetPaytool `json:"refund_preset_paytool_list,omitempty"`
	RefundChargeAmount           string                 `json:"refund_charge_amount,omitempty"`
	RefundSettlementId           string                 `json:"refund_settlement_id,omitempty"`
	PresentRefundBuyerAmount     string                 `json:"present_refund_buyer_amount,omitempty"`
	PresentRefundDiscountAmount  string                 `json:"present_refund_discount_amount,omitempty"`
	PresentRefundMdiscountAmount string                 `json:"present_refund_mdiscount_amount,omitempty"`
	HasDepositBack               string                 `json:"has_deposit_back,omitempty"`
	RefundHybAmount              string                 `json:"refund_hyb_amount,omitempty"`
}

type TradeRefundQueryDto

type TradeRefundQueryDto struct {
	ErrorResponseDto
	TradeNo              string           `json:"trade_no,omitempty"`
	OutTradeNo           string           `json:"out_trade_no,omitempty"`
	OutRequestNo         string           `json:"out_request_no,omitempty"`
	RefundReason         string           `json:"refund_reason,omitempty"`
	TotalAmount          string           `json:"total_amount,omitempty"`
	RefundAmount         string           `json:"refund_amount,omitempty"`
	RefundStatus         string           `json:"refund_status,omitempty"`
	RefundRoyaltys       []*RefundRoyalty `json:"refund_royaltys,omitempty"`
	GmtRefundPay         string           `json:"gmt_refund_pay,omitempty"`
	RefundDetailItemList []*TradeFundBill `json:"refund_detail_item_list,omitempty"`
	SendBackFee          string           `json:"send_back_fee,omitempty"`
	DepositBackInfo      *DepositBackInfo `json:"deposit_back_info,omitempty"`
}

type TradeRefundResponseDto

type TradeRefundResponseDto struct {
	Response     *TradeRefundDto `json:"alipay_trade_refund_response"`
	AlipayCertSn string          `json:"alipay_cert_sn,omitempty"`
	SignData     string          `json:"-"`
	Sign         string          `json:"sign"`
}

发起退款 返回

type TradeSettleDetail

type TradeSettleDetail struct {
	OperationType     string `json:"operation_type,omitempty"`
	OperationSerialNo string `json:"operation_serial_no,omitempty"`
	OperationDt       string `json:"operation_dt,omitempty"`
	TransOut          string `json:"trans_out,omitempty"`
	TransIn           string `json:"trans_in,omitempty"`
	Amount            string `json:"amount,omitempty"`
	OriTransOut       string `json:"ori_trans_out,omitempty"`
	OriTransIn        string `json:"ori_trans_in,omitempty"`
}

type TradeSettleInfo

type TradeSettleInfo struct {
	TradeSettleDetailList []*TradeSettleDetail `json:"trade_settle_detail_list,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL