Documentation
¶
Index ¶
- Constants
- Variables
- func BuildSign(params map[string]string, secret string) string
- func CallbackURLs() (notifyURL, returnURL string)
- func DecryptSecret(encoded, key string) (string, error)
- func DeletePaymentConfig(c *gin.Context)
- func DeleteUserPaymentConfig(ctx context.Context, userID uint64) error
- func DispatchReceive(c *gin.Context)
- func EncryptSecret(plaintext, key string) (string, error)
- func GetPaymentConfig(c *gin.Context)
- func HandleExpireStaleOrders(ctx context.Context, _ *asynq.Task) error
- func HandleNotify(ctx context.Context, q map[string]string) (bool, string)
- func HandleNotifyHTTP(c *gin.Context)
- func SaveUserPaymentConfig(ctx context.Context, userID uint64, clientID, clientSecret string) error
- func UpsertPaymentConfig(c *gin.Context)
- func VerifySign(params map[string]string, secret string) bool
- type GetPaymentConfigResponseData
- type HandleNotifyParams
- type OrderStatus
- type PaymentInitiation
- type PaymentOrder
- type ReceiveResponse
- type Response
- type UpsertPaymentConfigRequest
- type UserPaymentConfig
Constants ¶
const ( ErrPaymentDisabled = "支付功能未启用" ErrInvalidAmount = "金额必须大于 0 且最多 2 位小数" ErrPriceRequiresOneForEach = "仅一码一用分发支持设置金额" ErrCreatorNotConfigured = "项目创建者尚未配置支付凭据,无法发起支付" ErrPendingOrderExists = "当前项目存在进行中或已完成订单,不可重复创建" ErrPaymentConfigNotFound = "尚未配置支付凭据" ErrEncryptionKeyMissing = "服务端未配置支付密钥加密密钥" ErrInvalidClientCredentials = "clientID 与 clientSecret 不能为空" ErrOrderNotFound = "订单不存在" ErrOrderExpired = "订单已过期" ErrCannotDeleteHasActive = "存在未结束的付费项目,无法删除支付配置" ErrInvalidPriceDecimals = "金额最多保留 2 位小数" ErrPriceTooLarge = "金额超出允许范围" )
Variables ¶
var ErrOrderNotFoundSentinel = errors.New(ErrOrderNotFound)
ErrOrderNotFoundSentinel 外部判断
Functions ¶
func BuildSign ¶
BuildSign 按易支付/CodePay/VPay 兼容协议生成 MD5 签名(小写十六进制)。 规则:取非空参数,排除 sign 与 sign_type,按 key ASCII 升序,用 k1=v1&k2=v2 拼接, 末尾追加 secret,整体 MD5。
func CallbackURLs ¶
func CallbackURLs() (notifyURL, returnURL string)
CallbackURLs 返回当前平台配置的回调地址,用于前端展示给用户。
func DecryptSecret ¶
DecryptSecret 解密 EncryptSecret 的输出。
func DeletePaymentConfig ¶
DeletePaymentConfig DELETE /api/v1/users/payment-config
func DeleteUserPaymentConfig ¶
DeleteUserPaymentConfig 删除用户支付配置。 若该用户存在 Price>0 且未结束的自有项目则拒绝删除,避免后续领取者无法付款。
func DispatchReceive ¶
DispatchReceive POST /api/v1/projects/:id/receive 运行在 project.ReceiveProjectMiddleware() 之后,已通过资格校验并在 context 注入 project。 付费项目:返回 {require_payment:true, pay_url, ...};前端直接跳转 pay_url。 免费项目:执行原领取事务,返回 {itemContent}。
func EncryptSecret ¶
EncryptSecret 使用 AES-256-GCM 加密明文,输出 base64(nonce|ciphertext|tag)。
func GetPaymentConfig ¶
GetPaymentConfig GET /api/v1/users/payment-config @Summary 获取当前用户的支付配置(不返回明文 secret)
func HandleExpireStaleOrders ¶
HandleExpireStaleOrders 清理长时间未付款的 PENDING 订单。 查询条件:status=PENDING 且 expire_at 超时超过 5 分钟。 额外 5 分钟宽限期确保 epay 的异步 notify 回调在此之前已到达, 避免"cleanup 先置 FAILED + RPush,notify 随后到达发现已无 PENDING 订单"的竞态。
func HandleNotify ¶
HandleNotify 处理异步支付回调。 返回 (success bool, reason string):success=true 表示应返回文本 "success"; 否则返回 "fail",epay 最多重试 5 次,每次间隔由对方决定。 实现关键点:
- 拉起订单 → 验签(使用订单记录的 PayeeID 对应凭据) → 校验 trade_status/pid/money
- 幂等:若订单已是 COMPLETED/REFUNDED 直接 success
- CAS 推进 PENDING → PAID,成功者执行 fulfill 事务
- fulfill 失败 → 调 refund,成功后 RPush item 回 Redis,置 REFUNDED;本次返回 fail 让对方重试, 再次进入时因状态非 PENDING 直接 success
func HandleNotifyHTTP ¶
HandleNotifyHTTP GET /api/v1/payment/notify 易支付兼容回调,返回纯文本 "success" / "fail"。
func SaveUserPaymentConfig ¶
SaveUserPaymentConfig 保存/更新用户的支付凭据,clientSecret 明文进入后会被加密。
func UpsertPaymentConfig ¶
UpsertPaymentConfig PUT /api/v1/users/payment-config
Types ¶
type GetPaymentConfigResponseData ¶
type GetPaymentConfigResponseData struct {
HasConfig bool `json:"has_config"`
ClientID string `json:"client_id"`
SecretLast4 string `json:"secret_last4"`
CallbackNotifyURL string `json:"callback_notify_url"`
CallbackReturnURL string `json:"callback_return_url"`
PaymentEnabled bool `json:"payment_enabled"`
}
GetPaymentConfigResponseData 当前用户支付配置的安全视图
type HandleNotifyParams ¶
HandleNotifyParams 易支付回调需要的全部字段
type OrderStatus ¶
type OrderStatus int8
OrderStatus 订单状态机
PENDING(0) -> PAID(1) -> COMPLETED(2) // 正常路径
-> REFUNDING(3) -> REFUNDED(4) // 发放失败
PENDING -> FAILED(5) // 未付款超时 / 创建失败
const ( OrderStatusPending OrderStatus = 0 OrderStatusPaid OrderStatus = 1 OrderStatusCompleted OrderStatus = 2 OrderStatusRefunding OrderStatus = 3 OrderStatusRefunded OrderStatus = 4 OrderStatusFailed OrderStatus = 5 )
type PaymentInitiation ¶
type PaymentInitiation struct {
OutTradeNo string `json:"out_trade_no"`
PayURL string `json:"pay_url"`
Amount string `json:"amount"`
ExpireAt time.Time `json:"expire_at"`
}
PaymentInitiation 返回给前端的发起支付信息
func InitiatePayment ¶
func InitiatePayment(ctx context.Context, p *project.Project, payer *oauth.User, clientIP string) (*PaymentInitiation, error)
InitiatePayment 为付费项目的一次领取行为创建支付订单,并返回前端可直接跳转的支付 URL。 调用方已通过 ReceiveProjectMiddleware 的前置校验。 流程:载入商户凭据 → Redis LPop 预占 item → 持久化订单 PENDING → 构造 submit URL 返回。 若创建订单失败或拼接失败,需立即把 itemID RPush 回 Redis 以恢复库存。
type PaymentOrder ¶
type PaymentOrder struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
OutTradeNo string `gorm:"size:64;uniqueIndex;not null" json:"out_trade_no"`
TradeNo string `gorm:"size:64;index" json:"trade_no"`
ProjectID string `gorm:"size:64;not null;index:idx_project_payer_status,priority:1" json:"project_id"`
ItemID uint64 `gorm:"index;not null" json:"item_id"`
PayerID uint64 `gorm:"not null;index:idx_project_payer_status,priority:2;index:idx_payer_status,priority:1" json:"payer_id"`
PayeeID uint64 `gorm:"index;not null" json:"payee_id"`
PayeeClientID string `gorm:"size:64" json:"payee_client_id"`
Amount decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"amount"`
Status OrderStatus `` /* 141-byte string literal not displayed */
PaidAt *time.Time `json:"paid_at"`
RefundedAt *time.Time `json:"refunded_at"`
FailReason string `gorm:"size:255" json:"fail_reason"`
ExpireAt time.Time `gorm:"index:idx_status_expire,priority:2" json:"expire_at"`
ClientIP string `gorm:"size:64" json:"client_ip"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
PaymentOrder 支付订单(一次付费领取 = 一个订单)
联合索引:
- idx_project_payer_status (project_id, payer_id, status):查询某用户在某项目的待支付订单
- idx_payer_status (payer_id, status):按用户快速查询待支付订单
- idx_status_expire (status, expire_at):清理任务扫描超时 PENDING 订单
type ReceiveResponse ¶
type ReceiveResponse struct {
ItemContent string `json:"itemContent,omitempty"`
RequirePayment bool `json:"require_payment,omitempty"`
PayURL string `json:"pay_url,omitempty"`
OutTradeNo string `json:"out_trade_no,omitempty"`
Amount string `json:"amount,omitempty"`
ExpireAt string `json:"expire_at,omitempty"`
}
ReceiveResponse 领取接口的统一响应:免费返回 itemContent;付费返回支付跳转信息。
type Response ¶
type Response struct {
ErrorMsg string `json:"error_msg"`
Data interface{} `json:"data"`
}
Response 统一的 payment 响应壳,保持与其他 app 一致
type UpsertPaymentConfigRequest ¶
type UpsertPaymentConfigRequest struct {
ClientID string `json:"client_id" binding:"required,min=1,max=64"`
ClientSecret string `json:"client_secret" binding:"required,min=1,max=256"`
}
UpsertPaymentConfigRequest PUT 请求体
type UserPaymentConfig ¶
type UserPaymentConfig struct {
UserID uint64 `gorm:"primaryKey" json:"user_id"`
ClientID string `gorm:"size:64;not null" json:"client_id"`
ClientSecretEnc string `gorm:"size:512;not null" json:"-"`
SecretLast4 string `gorm:"size:8" json:"secret_last4"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
UserPaymentConfig 用户的商户凭据(一对一绑定 User)
func GetUserPaymentConfig ¶
func GetUserPaymentConfig(ctx context.Context, userID uint64) (*UserPaymentConfig, error)
GetUserPaymentConfig 读取指定用户的支付配置,不存在返回 (nil, nil)。