pay360

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 19 Imported by: 0

README

pay360

github.com/gtkit/pay360 封装 360 联运(软件管家)开放平台 OPENAPI 的服务端接口,面向需要对接 360 联运支付、退款、订单、代扣、签约、发票的 Go 服务端程序。

它统一处理:签名生成、access_token 生命周期(3 小时有效,且“新申请使旧失效”)、Header-Tid 排障头、错误码语义,让调用方专注业务。

特性

  • 覆盖文档第四章全部服务端接口(出站 + 入站回调验签),以及客诉接口文档的多笔退款、投诉回复/完结与投诉 webhook 验签。
  • access_token 两层缓存:默认进程内无锁内存缓存 + 单飞刷新;可注入自定义 TokenCache(如 Redis)支持多实例共享。
  • 可注入 TokenRefreshLock 做多副本刷新单飞,避免“新 token 作废旧 token”导致实例间互相影响。
  • 每次调用均可获取 Header-Tid(成功与错误路径)。
  • 类型化错误码,支持 errors.Is 判定。
  • 并发安全:Client 构造后只读,可跨 goroutine 共享。
  • JSON 使用 github.com/gtkit/json/v2,HTTP 使用 github.com/gtkit/httpc

安装

go get github.com/gtkit/pay360

要求 Go 1.26+。

初始化

c, err := pay360.New("your-appid", 123456, "your-appsecret")
if err != nil {
    log.Fatal(err)
}

qid 必须为正数。凭据 appidqidappsecret 请勿硬编码,从配置或密钥管理服务读取。

可选项
Option 说明
WithBaseURL(u) 覆盖接口域名(默认 https://api.openstore.360.cn),主要用于测试
WithHTTPClient(h) 注入自定义 *httpc.Client(超时、传输等)
WithTokenCache(tc) 注入自定义 access_token 缓存(多实例共享)
WithTokenRefreshLock(l) 注入自定义 access_token 刷新锁(多副本单飞)
WithTokenRefreshAhead(d) token 提前刷新安全边界(默认 5 分钟)
WithVendorKey(k) 360 下发的厂商密钥,用于投诉 webhook 验签(未设置时用 appsecret)
WithClock(now) 注入时间源,主要用于测试

httpc 自身不提供日志。如需请求日志,构造 *httpc.Client 时用 httpc.WithTransport 注入带日志的 http.RoundTripper,再经 WithHTTPClient 注入本客户端。

前端订单参数(createOrder)

前端 SDK360.createOrder 的下单由 JSSDK 直连完成,但订单数据应由服务端生成唯一 order_id、组装并留存后下发给前端。CreateOrderParams 提供类型化构造、条件校验与序列化(不发请求):

p := pay360.CreateOrderParams{
    OrderID:     genOrderID(),                                  // 业务生成,需唯一
    OrderAmount: 1,                                             // 单位:分
    CreateTime:  strconv.FormatInt(time.Now().Unix(), 10),     // 10 位时间戳
    UserID:      "user-1",
    ProductID:   "vip-1",
    ProductName: "会员月卡",
}
data, err := p.MarshalForSDK() // 下发给前端,前端传入 SDK360.createOrder

开启代扣时设 AutoPayStatus = pay360.AutoPayEnabled 并填写一组代扣字段,Validate/MarshalForSDK 会强制「代扣必填」校验并自动组装 extautopay_mode)。包内提供 OrderStatus*PayChannel*PeriodType*OrderPayType*AutoPayEnabled/DisabledAutopayMode* 等枚举常量,供服务端解析回调/查询及与前端约定时共用。

任务单(任务系统,文档 3.2/3.5)独立于代扣:设 OrderPayType = pay360.OrderPayTypeTask 并填写 TaskID 即可构造纯任务单,无须开启代扣,且任务单允许 OrderAmount 为 0:

p := pay360.CreateOrderParams{
    OrderID:      genOrderID(),
    OrderAmount:  0, // 任务单允许 0 金额
    CreateTime:   strconv.FormatInt(time.Now().Unix(), 10),
    UserID:       "user-1",
    ProductID:    "task-product-1",
    ProductName:  "任务奖励",
    OrderPayType: pay360.OrderPayTypeTask,
    TaskID:       "task-36",
}

出站接口

方法 说明
Refund(ctx, RefundRequest) 订单退款申请
QueryOrder(ctx, OrderQueryRequest) 订单查询(返回 OrderQueryResponseIsPaid() 判定支付成功)
DoPost(ctx, DoPostRequest) 厂商侧发起代扣
CancelSign(ctx, CancelSignRequest) 厂商侧取消签约
PlainInvoice(ctx, PlainInvoiceRequest) 普票开具
PlainInvoiceCancel(ctx, orderID) 普票红冲
SpecialInvoice(ctx, SpecialInvoiceRequest) 专票开具(返回 source_id
QuerySpecialInvoice(ctx, requestType, sourceID) 专票查询(开票/红冲进度,requestType 仅允许 SpecialInvoiceQueryIssueSpecialInvoiceQueryCancel
SpecialInvoiceCancel(ctx, SpecialInvoiceCancelRequest) 专票红冲

多数方法返回 (headerTid string, err error);返回数据的方法(查询、开具)返回带 HeaderTid 字段的结果结构。

发票相关的文档取值提供常量:InvoiceRedCategorySeller(红冲类别 1 销方红冲)、InvoiceRedReasonMistake(红冲原因 INVOICE_MISTAKE)、InvoiceCustomTypeEnterprise(商户类型 1 企业)。

tid, err := c.Refund(ctx, pay360.RefundRequest{
    OrderID:      "order-1",
    OrderAmount:  100, // 单位:分
    UserID:       "user-1",
    RefundReason: "用户申请退款",
})

o, err := c.QueryOrder(ctx, pay360.OrderQueryRequest{OrderID: "order-1", UserID: "user-1"})
if err == nil && o.IsPaid() {
    // 发放权益
}

入站回调(厂商订单推送)

在你的 HTTP handler 中验签、解析并响应:

func handle360Callback(c *pay360.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)

        // 验签失败返回 pay360.ErrCallbackSign;
        // 验签通过但 app_id/qid 与本客户端凭据不一致返回 pay360.ErrCallbackMismatch
        cb, err := c.VerifyCallback(body)
        if err != nil {
            http.Error(w, "invalid callback", http.StatusBadRequest)
            return
        }

        switch cb.CallbackType {
        case pay360.CallbackOrderStatus: // 1 普通支付/退款,cb.IsPaid() 为 true 时下发权益
        case pay360.CallbackAutopay:     // 2 代扣推送,据 cb.Extra.MfrOrderID 创建订单
        case pay360.CallbackSign:        // 3 签约/取消签约通知
        }

        // 权益处理完成后返回标准成功响应,请确保 10s 内响应,否则会被重复推送
        out, _ := json.Marshal(pay360.AckSuccess())
        w.Header().Set("Content-Type", "application/json")
        _, _ = w.Write(out)
    }
}

务必校验一致性VerifyCallback 保证请求确实来自 360(签名正确),并已内置核对 app_id/qid 与本客户端凭据一致(不一致返回 ErrCallbackMismatch)。但金额与订单数据本包不持有——发放权益前,你仍需核对 cb.MfrOrderAmountcb.MfrOrderID 与本地订单是否相符,再决定是否发放。

客诉接口

对应《软件管家客诉接口》文档(OPENAPI 补充版本),公共参数与签名复用同一套机制。

出站
方法 说明
RefundOrders(ctx, RefundOrdersRequest) 多笔订单退款(v2,OrderIDs 列表逗号拼接发送,与 v1 Refund 并存)
ComplainReply(ctx, ComplainReplyRequest) 投诉回复(SourceComplainSourceNormal/ComplainSourceRefund
ComplainFinish(ctx, ComplainFinishRequest) 投诉完结(完结处理码 05 由包内固定填充)
tid, err := c.RefundOrders(ctx, pay360.RefundOrdersRequest{
    OrderIDs:     []string{"order-1", "order-2"},
    UserID:       "user-1",
    OrderAmount:  9900, // 单位:分
    RefundReason: "用户申请退款",
})

tid, err = c.ComplainReply(ctx, pay360.ComplainReplyRequest{
    ComplainNo: "C202605140001",
    Content:    "您好,问题已收到,我们会尽快处理。",
    Source:     pay360.ComplainSourceNormal,
})

退款成功后,如该订单关联投诉且关联订单均已关闭,平台会自动完结投诉。错误码哨兵:ErrRateLimited(10018)、ErrComplainNotFound(10034)、ErrComplainReplyLimit(10035)。

实测注意:RefundOrders 对不存在的订单返回 errno=10003 msg="数据不存在",与主文档错误表中 10003(厂商信息不存在)同码不同义;ErrVendorNotFound 仅按错误码判等,在 v2 退款场景下请结合 APIError.Msg 判断。

投诉 webhook(入站)

360 会向你在联运后台配置的回调地址推送投诉事件(COMPLAINT_CREATED/REPLY_ADDED/STATUS_CHANGED)。验签规则与订单推送不同:仅 data(请求原文)与 timestamp 参与签名,密钥优先厂商密钥(WithVendorKey 配置,未配置回落 appsecret):

func handleComplaint(c *pay360.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)

        wh, err := c.VerifyComplaintWebhook(body) // 验签失败返回 pay360.ErrComplaintWebhookSign
        if err != nil {
            http.Error(w, "invalid sign", http.StatusBadRequest)
            return
        }

        // 平台按 HTTP 状态码判定:返回任意 2xx 即成功,失败最多重试 3 次(超时 5 秒)。
        // 请按 wh.MessageID 做幂等;处理较慢时建议先落库返回 2xx,再异步处理。
        switch wh.EventType {
        case pay360.ComplaintEventCreated:       // 新投诉,wh.ComplainInfo
        case pay360.ComplaintEventReplyAdded:    // 新增回复,wh.ReplyInfo
        case pay360.ComplaintEventStatusChanged: // 状态变更,wh.ComplainInfo.CurrStatus
        }
        w.WriteHeader(http.StatusOK)
    }
}

注意:ComplainInfo 字段服务端按 omitempty 序列化,空值/0 可能整字段缺失;Images 为 base64 原始内容,展示时需自行补充 MIME 前缀。

错误处理

业务错误为 *pay360.APIError,可用哨兵判定:

if errors.Is(err, pay360.ErrAccessToken) { /* 10012 */ }
if errors.Is(err, pay360.ErrOrderNotFound) { /* 100005 */ }

var apiErr *pay360.APIError
if errors.As(err, &apiErr) {
    log.Printf("code=%d msg=%s header_tid=%s", apiErr.Code, apiErr.Msg, apiErr.HeaderTid)
}

access_token 缓存与多实例

默认情况下,token 缓存在进程内:读取无锁,刷新单飞,到期前 5 分钟自动刷新。单实例无需任何配置。

多实例(多进程)部署时,由于“新申请 token 会使旧 token 失效”,各实例必须共享同一份 token,否则会互相作废。此时注入自定义 TokenCache

type TokenCache interface {
    Load(ctx context.Context) (token string, expireAt time.Time, ok bool, err error)
    Store(ctx context.Context, token string, expireAt time.Time) error
}

type TokenRefreshLock interface {
    Lock(ctx context.Context, fn func(context.Context) error) error
}

重要:共享存储必须配合 WithTokenRefreshLock。刷新流程会先获取锁,再在锁内二次读取共享缓存;若其它副本已经刷新并写入安全期 token,当前副本会直接复用,不再调用 auth。

当业务接口返回 errno=10012ErrAccessToken)时,本包会强制刷新一次 access_token 并用新 token 重试该业务请求一次。该重试仅覆盖鉴权失败场景;业务错误、网络错误和其它平台错误不会自动重试。

Redis 实现范式(伪代码):

type redisCache struct{ rdb *redis.Client }

func (r *redisCache) Load(ctx context.Context) (string, time.Time, bool, error) {
    // GET token 与 expireAt;未命中返回 ok=false
}
func (r *redisCache) Store(ctx context.Context, token string, expireAt time.Time) error {
    // SET token 与 expireAt(带 TTL)
}

type redisLock struct{ rdb *redis.Client }

func (r *redisLock) Lock(ctx context.Context, fn func(context.Context) error) error {
    // SETNX 获取锁;获取成功后执行 fn;defer 释放锁
}

c, _ := pay360.New(
    appid, qid, secret,
    pay360.WithTokenCache(&redisCache{rdb: rdb}),
    pay360.WithTokenRefreshLock(&redisLock{rdb: rdb}),
)

真实环境联调

live_test.go 仅在 -tags livetest 下编译。凭据通过环境变量传入,禁止写入代码或提交到仓库。

基础探测不会使用真实订单,主要验证签名、鉴权、字段类型和接口格式:

PAY360_APPID=... PAY360_QID=... PAY360_APPSECRET=... \
go test -tags livetest -run 'TestLive(Auth|QueryOrder|Probe)' -count=1 -v ./...

真实成功链路用例在缺少环境变量时会自动跳过:

测试 环境变量
已付订单查询 PAY360_LIVE_PAID_ORDER_IDPAY360_LIVE_PAID_USER_ID
专票查询 PAY360_LIVE_SPECIAL_SOURCE_ID,可选 PAY360_LIVE_SPECIAL_QUERY_TYPE

以下测试有副作用,必须显式设置开关:

测试 开关 必填环境变量
真实退款 PAY360_LIVE_ENABLE_REFUND=1 PAY360_LIVE_REFUND_ORDER_IDPAY360_LIVE_REFUND_USER_IDPAY360_LIVE_REFUND_AMOUNT
普票开具 PAY360_LIVE_ENABLE_INVOICE=1 PAY360_LIVE_INVOICE_ORDER_IDPAY360_LIVE_INVOICE_TITLEPAY360_LIVE_INVOICE_EMAIL
专票开具 PAY360_LIVE_ENABLE_INVOICE=1 PAY360_LIVE_SPECIAL_INVOICE_ORDER_IDPAY360_LIVE_SPECIAL_INVOICE_TITLEPAY360_LIVE_SPECIAL_INVOICE_EMAILPAY360_LIVE_SPECIAL_INVOICE_TAX_REGISTER_NOPAY360_LIVE_SPECIAL_INVOICE_ADDRESSPAY360_LIVE_SPECIAL_INVOICE_PHONEPAY360_LIVE_SPECIAL_INVOICE_BANK_NAMEPAY360_LIVE_SPECIAL_INVOICE_BANK_ACCOUNTPAY360_LIVE_SPECIAL_INVOICE_CUSTOM_TYPE
专票红冲 PAY360_LIVE_ENABLE_INVOICE_CANCEL=1 PAY360_LIVE_SPECIAL_CANCEL_INVOICE_NUMPAY360_LIVE_SPECIAL_CANCEL_SOURCE_IDPAY360_LIVE_SPECIAL_CANCEL_ORDER_ID

限流与韧性

本包是无状态的薄封装,不内置限流、熔断、重试——这些有状态、依赖部署拓扑的韧性策略应由调用方在服务层处理(如 golang.org/x/time/ratesony/gobreaker)。本包只负责签名、鉴权与请求执行,职责单一。

  • 限频(文档规定,请自行控制):换 token 与退款 3 次/秒、订单查询 5 次/秒、发票相关 3 次/秒。换 token 已被内部缓存+单飞天然限频;业务接口的频率由调用方保证。客诉接口触发限流时平台返回 errno=10018ErrRateLimited)。
  • access_token 失效重试:单实例下 token 不会被外部作废,几乎不会遇到 errno=10012。多实例共享缓存时,若极窄竞态窗口内 token 被其它实例作废,本包会强制刷新并自动重试一次。生产多副本应同时注入 WithTokenCacheWithTokenRefreshLock
  • 回调 body 大小VerifyCallback/ParseCallback 解析调用方传入的 body,请在 HTTP handler 用 http.MaxBytesReader 限制大小,防止超大请求耗内存。
  • 出站响应大小:默认 HTTP 客户端限制响应体 ≤10 MiB,可经 WithHTTPClient 调整。

注意事项

  • 请勿通过客户端轮询订单查询接口;SDK 本身会推送订单状态变更。
  • 通过任务系统完成的订单(任务单)不允许退款,Refund 此类订单会被平台拒绝。
  • 签约/取消签约推送(callback_type=3)中的 trans_time 不携带真实业务时间,为占位值 0001-01-01 00:00:00,请勿当作支付/退款时间落库。
  • 代扣失败重发须使用全新的 AutopayOrderID,复用旧值可能“失败但显示成功”。
  • 回调响应须在 10s 内返回,否则会被重复推送(最多补推 30 次)。
  • 专票申请后需人工审核(一般 5 个工作日内),通过 QuerySpecialInvoice 查询进度。
  • 订单查询为 GET 接口,access_token 会出现在 URL query 中,请注意网关/代理访问日志的脱敏。

许可

MIT,详见 LICENSE

Documentation

Overview

Package pay360 封装 360 联运(软件管家)开放平台 OPENAPI 的服务端接口。

它面向需要对接 360 联运支付/退款/订单/代扣/签约/发票的 Go 服务端程序, 统一处理签名生成、access_token 生命周期、Header-Tid 排障头与错误码语义。

快速开始:

c, err := pay360.New("your-appid", 123456, "your-appsecret")
if err != nil {
    // 处理构造错误
}
tid, err := c.Refund(ctx, pay360.RefundRequest{
    OrderID:      "order-1",
    OrderAmount:  100, // 单位:分
    UserID:       "user-1",
    RefundReason: "用户申请退款",
})

access_token 由 Client 内部缓存并按需刷新;默认实现为无锁内存缓存, 多实例部署可通过 WithTokenCache 注入共享存储实现,详见 TokenCache

Client 构造后字段只读,可安全地在多个 goroutine 间共享。

Index

Examples

Constants

View Source
const (
	CallbackOrderStatus = 1 // 订单状态变更(普通下单扣款、退款)
	CallbackAutopay     = 2 // 代扣推送(自动扣款),需厂商据 order_extra 创建订单
	CallbackSign        = 3 // 签约/取消签约状态通知,无需下发或取消权益
)

回调类型。

View Source
const (
	AutoPayStatusOpen   = 1 // 开通签约
	AutoPayStatusCancel = 2 // 取消签约
)

签约状态(OrderExtra.AutoPayStatus / callback_type=3)。

View Source
const (
	ComplainStatusPending    = 1 // 待处理
	ComplainStatusProcessing = 2 // 处理中
	ComplainStatusFinished   = 3 // 已完结
	ComplainStatusUnfinished = 4 // 未完结/兼容值
)

投诉状态(complain_info.currStatus)。

View Source
const (
	ComplainPlatformOther         = 1 // 其他/未知
	ComplainPlatformAlipay        = 2 // 支付宝
	ComplainPlatformWechat        = 3 // 微信
	ComplainPlatformAlipaySpecial = 4 // 支付宝特殊版
)

投诉来源平台(complain_info.platForm)。

View Source
const (
	ComplainSourceNormal = 1 // 普通回复
	ComplainSourceRefund = 2 // 退款相关回复
)

投诉回复来源(ComplainReplyRequest.Source)。

View Source
const (
	ComplaintEventCreated       = "COMPLAINT_CREATED" // 新投诉创建
	ComplaintEventReplyAdded    = "REPLY_ADDED"       // 投诉新增回复
	ComplaintEventStatusChanged = "STATUS_CHANGED"    // 投诉状态发生变化
)

投诉 webhook 事件类型(ComplaintWebhook.EventType)。

View Source
const (
	OrderStatusPending   = 10 // 待付款(初始状态)
	OrderStatusPaid      = 20 // 付款完成(待通知厂商)
	OrderStatusNotified  = 30 // 待厂商发权益(已通知厂商)
	OrderStatusAfterSale = 40 // 售后中(厂商发起退款)
	OrderStatusCompleted = 50 // 交易完成(厂商已发放)
	OrderStatusCanceled  = 60 // 已取消(支付超时、过期等)
	OrderStatusClosed    = 70 // 交易关闭(退款完成)
)

订单状态码(order_status,用于订单查询响应与订单推送回调)。

其中 20、30、50 均视为支付成功状态(见 OrderQueryResponse.IsPaid)。

View Source
const (
	PayChannelTask          = -1 // 任务单
	PayChannelWechat        = 1  // 微信
	PayChannelAlipay        = 2  // 支付宝
	PayChannelAlipayAutopay = 3  // 支付宝代扣单
)

支付渠道(pay_channel / pay_chanel)。

View Source
const (
	PeriodTypeDay   = 0 // 扣款周期按天计
	PeriodTypeMonth = 1 // 扣款周期按自然月计
)

代扣周期类型(period_type)。

View Source
const (
	OrderPayTypeNormal = 0 // 付费单
	OrderPayTypeTask   = 3 // 任务单
)

订单类型(order_pay_type)。

View Source
const (
	AutoPayDisabled = 0 // 不开启代扣
	AutoPayEnabled  = 1 // 开启代扣
)

是否开启代扣(createOrder 的 auto_pay_status)。

注意:与回调 order_extra 中的 auto_pay_status(AutoPayStatusOpen/AutoPayStatusCancel, 表示签约/取消签约)语义不同。

View Source
const (
	AutopayModeManager = 0 // 管家侧发起代扣续费订单(需固定扣款周期)
	AutopayModeVendor  = 1 // 厂商侧发起代扣续费订单(厂商完全可控,经服务端代扣接口发起)
)

代扣发起方(ext.autopay_mode)。

View Source
const (
	SpecialInvoiceQueryIssue  = "1" // 开票查询
	SpecialInvoiceQueryCancel = "2" // 红冲查询
)

专票查询请求类型。

View Source
const (
	InvoiceRedCategorySeller    = "1"               // 专票红冲类别:销方红冲
	InvoiceRedReasonMistake     = "INVOICE_MISTAKE" // 专票红冲原因(销方红冲必填)
	InvoiceCustomTypeEnterprise = "1"               // 专票商户类型:企业
)

文档明确定义的发票取值常量。仅供组装请求时复用, 请求方法不据此封闭取值集合(文档未声明集合封闭)。

View Source
const Version = "v0.3.0"

Variables

View Source
var (
	ErrParam               = &APIError{Code: 10001, Msg: "参数错误"}
	ErrInternal            = &APIError{Code: 10002, Msg: "服务内部错误"}
	ErrSign                = &APIError{Code: 10006, Msg: "sign 错误"}
	ErrRequestExpired      = &APIError{Code: 10007, Msg: "请求过期"}
	ErrAccessToken         = &APIError{Code: 10012, Msg: "access_token 错误"}
	ErrIllegalAccess       = &APIError{Code: 10014, Msg: "非法访问"}
	ErrContentType         = &APIError{Code: 10015, Msg: "不支持的 content_type"}
	ErrParamType           = &APIError{Code: 10016, Msg: "参数类型错误"}
	ErrRateLimited         = &APIError{Code: 10018, Msg: "触发限流"}
	ErrComplainNotFound    = &APIError{Code: 10034, Msg: "投诉不存在"}
	ErrComplainReplyLimit  = &APIError{Code: 10035, Msg: "投诉回复次数超限"}
	ErrAppNotFound         = &APIError{Code: 100001, Msg: "应用信息不存在"}
	ErrAppOffline          = &APIError{Code: 100002, Msg: "应用信息已下线"}
	ErrVendorNotFound      = &APIError{Code: 100003, Msg: "厂商信息不存在"}
	ErrVendorOffline       = &APIError{Code: 100004, Msg: "厂商信息已下线"}
	ErrOrderNotFound       = &APIError{Code: 100005, Msg: "订单不存在"}
	ErrAppCategoryNotFound = &APIError{Code: 100006, Msg: "应用分类不存在"}
	ErrAuthInfo            = &APIError{Code: 100008, Msg: "认证信息错误"}
	ErrOrderStatusInvalid  = &APIError{Code: 100009, Msg: "订单状态有误,不允许进行此操作"}
)

文档《360联运JSSDK集成说明》第四章错误码表对应的哨兵值。 仅携带 Code,用于 errors.Is 判定。

View Source
var ErrCallbackMismatch = errors.New("pay360: callback appid/qid mismatch")

ErrCallbackMismatch 表示回调验签通过但其中的 app_id/qid 与客户端凭据不一致, 调用方不应处理该回调(它属于其它应用)。

View Source
var ErrCallbackSign = errors.New("pay360: callback sign mismatch")

ErrCallbackSign 表示回调验签失败。

View Source
var ErrComplaintWebhookSign = errors.New("pay360: complaint webhook sign mismatch")

ErrComplaintWebhookSign 表示投诉 webhook 推送验签失败。

Functions

This section is empty.

Types

type APIError

type APIError struct {
	Code      int
	Msg       string
	HeaderTid string
}

APIError 表示 360 平台返回的业务错误(errno 非 0,或部分网关接口的 code 非 0)。

可用 errors.Is 与本包导出的错误码哨兵比较,例如:

if errors.Is(err, pay360.ErrAccessToken) { /* token 失效,触发重试 */ }

HeaderTid 为本次响应头 Header-Tid 的值,便于向 360 反馈问题时快速定位。

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is 仅按错误码判等,忽略 Msg 与 HeaderTid,使哨兵值可用于 errors.Is

type Ack

type Ack struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    string `json:"data"`
}

Ack 为厂商对订单推送回调的响应体。

func AckResponse

func AckResponse(code int, message, data string) Ack

AckResponse 返回自定义响应体(成功时 code 应为 200)。

Example
out, _ := json.Marshal(AckResponse(500, "retry later", "job-1"))
fmt.Println(string(out))
Output:
{"code":500,"message":"retry later","data":"job-1"}

func AckSuccess

func AckSuccess() Ack

AckSuccess 返回标准成功响应 {"code":200,"message":"success","data":""}。 360-Server 收到此响应后会将 order_status 由 30 变更为 50。

Example
out, _ := json.Marshal(AckSuccess())
fmt.Println(string(out))
Output:
{"code":200,"message":"success","data":""}

type Callback

type Callback struct {
	AppID           string `json:"app_id"`
	AgreementNumber string `json:"agreement_number"` // 签约号
	AutoPayStatus   int    `json:"auto_pay_status"`  // 签约状态:1 开通签约,2 取消签约
	BankTradeCode   string `json:"bank_trade_code"`
	CallbackType    int    `json:"callback_type"`
	MfrOrderAmount  int64  `json:"mfr_order_amount"`
	MfrOrderID      string `json:"mfr_order_id"`
	MfrProductID    string `json:"mfr_product_id"`
	MfrProductName  string `json:"mfr_product_name"`
	OrderCode       string `json:"order_code"`
	OrderExtra      string `json:"order_extra"`
	OrderStatus     int    `json:"order_status"`
	PayChannel      int    `json:"pay_channel"`
	Qid             int64  `json:"qid"`
	Sign            string `json:"sign"`
	Timestamp       int64  `json:"timestamp"`
	TransTime       string `json:"trans_time"` // 支付/退款时间;签约、解约推送中不使用,为占位值 0001-01-01 00:00:00

	// Extra 为 OrderExtra 字段解析后的结构(callback_type 为 2/3 时有意义)。
	Extra OrderExtra `json:"-"`
}

Callback 为厂商订单推送回调的解析结果。

AgreementNumber 与 AutoPayStatus 为文档 4.1.2.4 参数表列出的顶层签约字段; 推送示例中同名信息也可能出现在 order_extra 内(见 Extra),两处独立解析。

func ParseCallback

func ParseCallback(body []byte) (*Callback, error)

ParseCallback 解析厂商订单推送回调的请求体,并解析内嵌的 order_extra。 它不做验签;如需验签请使用 Client.VerifyCallback

Example
body := `{"callback_type":2,"mfr_order_id":"parent1","order_extra":"{\"mfr_order_id\":\"child1\"}"}`
cb, _ := ParseCallback([]byte(body))
fmt.Println(cb.CallbackType, cb.Extra.MfrOrderID)
Output:
2 child1

func (Callback) IsPaid added in v0.2.0

func (cb Callback) IsPaid() bool

IsPaid 报告回调订单是否处于支付成功状态(order_status 为 20、30 或 50), 与 OrderQueryResponse.IsPaid 语义一致。

type CancelSignRequest

type CancelSignRequest struct {
	OrderID         string // 签约的订单 ID
	AgreementNumber string // 签约的协议号
}

CancelSignRequest 为厂商侧取消签约的参数。

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client 是 360 联运 OPENAPI 的服务端客户端。

构造后所有字段只读(token 缓存内部自带并发安全),可在多个 goroutine 间共享。

func New

func New(appid string, qid int64, appsecret string, opts ...Option) (*Client, error)

New 创建客户端。appid 与 appsecret 必填,qid 为厂商 ID。

默认使用官方域名、进程内内存 token 缓存与带连接池的默认 HTTP 客户端。 构造期不发起任何网络请求,也不启动后台 goroutine。

func (*Client) CancelSign

func (c *Client) CancelSign(ctx context.Context, req CancelSignRequest) (headerTid string, err error)

CancelSign 厂商侧取消用户代扣签约。返回本次响应的 Header-Tid。

func (*Client) ComplainFinish added in v0.3.0

func (c *Client) ComplainFinish(ctx context.Context, req ComplainFinishRequest) (headerTid string, err error)

ComplainFinish 完结投诉(厂商确认投诉已处理完成后调用)。返回本次响应的 Header-Tid。

func (*Client) ComplainReply added in v0.3.0

func (c *Client) ComplainReply(ctx context.Context, req ComplainReplyRequest) (headerTid string, err error)

ComplainReply 回复用户投诉,回复内容会同步至对应投诉渠道。返回本次响应的 Header-Tid。

func (*Client) DoPost

func (c *Client) DoPost(ctx context.Context, req DoPostRequest) (headerTid string, err error)

DoPost 厂商侧发起一次代扣。返回本次响应的 Header-Tid。

func (*Client) PlainInvoice

func (c *Client) PlainInvoice(ctx context.Context, req PlainInvoiceRequest) (PlainInvoiceResult, error)

PlainInvoice 开具普通发票。

func (*Client) PlainInvoiceCancel

func (c *Client) PlainInvoiceCancel(ctx context.Context, orderID string) (headerTid string, err error)

PlainInvoiceCancel 普票红冲(红冲对应订单的普通发票)。返回本次响应的 Header-Tid。

此接口响应采用 {code,msg} 信封(code 0 表示成功),与多数接口的 errno 信封不同。

func (*Client) QueryOrder

func (c *Client) QueryOrder(ctx context.Context, req OrderQueryRequest) (OrderQueryResponse, error)

QueryOrder 查询订单详情。

注意:请勿通过客户端轮询此接口;SDK 本身会通过订单推送通知状态变更。

func (*Client) QuerySpecialInvoice

func (c *Client) QuerySpecialInvoice(ctx context.Context, requestType, sourceID string) (SpecialInvoiceQueryResult, error)

QuerySpecialInvoice 查询专票开票或红冲进度。 requestType 取 SpecialInvoiceQueryIssueSpecialInvoiceQueryCancel

func (*Client) Refund

func (c *Client) Refund(ctx context.Context, req RefundRequest) (headerTid string, err error)

Refund 申请订单退款。返回本次响应的 Header-Tid。

注意:通过任务系统完成的订单(任务单)不允许退款,平台会拒绝此类请求。

func (*Client) RefundOrders added in v0.3.0

func (c *Client) RefundOrders(ctx context.Context, req RefundOrdersRequest) (headerTid string, err error)

RefundOrders 申请订单退款(v2 接口),支持单笔与多笔。返回本次响应的 Header-Tid。

多笔时平台按逗号拆分去重逐笔处理,任一订单退款失败则本次请求返回失败。 通过任务系统完成的订单(任务单)不允许退款。退款成功后,如订单关联投诉且 关联订单均已关闭,平台会自动完结投诉。与 v1 Client.Refund 并存。

func (*Client) SpecialInvoice

func (c *Client) SpecialInvoice(ctx context.Context, req SpecialInvoiceRequest) (SpecialInvoiceResult, error)

SpecialInvoice 申请开具专用发票,返回 source_id。建议保存返回信息以便后续查询/红冲。

func (*Client) SpecialInvoiceCancel

func (c *Client) SpecialInvoiceCancel(ctx context.Context, req SpecialInvoiceCancelRequest) (headerTid string, err error)

SpecialInvoiceCancel 申请专票红冲。返回本次响应的 Header-Tid。

func (*Client) VerifyCallback

func (c *Client) VerifyCallback(body []byte) (*Callback, error)

VerifyCallback 验签并解析厂商订单推送回调。

验签规则与出站一致(空值不参与),签名盐为本客户端的 appsecret。 验签失败返回 ErrCallbackSign;验签通过但回调中的 app_id/qid 与客户端凭据 不一致时返回 ErrCallbackMismatch。金额与订单数据的一致性核对仍由调用方 在发放权益前完成(本包不持有订单数据)。

func (*Client) VerifyComplaintWebhook added in v0.3.0

func (c *Client) VerifyComplaintWebhook(body []byte) (*ComplaintWebhook, error)

VerifyComplaintWebhook 验签并解析投诉 webhook 推送。

验签规则与 OPENAPI 一致,但参与签名的字段仅有 data 与 timestamp,且 data 取 请求原文中的 JSON 字符串(不重新序列化)。签名密钥优先使用 WithVendorKey 配置的厂商密钥,未配置时使用 appsecret。验签失败返回 ErrComplaintWebhookSign

平台按 HTTP 状态码判定推送结果:处理成功请返回任意 2xx;失败最多重试 3 次, 请按 MessageID 做幂等。

Example
c, _ := New("your-appid", 123456, "your-appsecret")
http.HandleFunc("/union/complain/callback", func(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	wh, err := c.VerifyComplaintWebhook(body)
	if err != nil {
		http.Error(w, "invalid sign", http.StatusBadRequest)
		return
	}
	// 按 wh.MessageID 做幂等;建议先落库后返回 2xx,再异步处理
	switch wh.EventType {
	case ComplaintEventCreated, ComplaintEventReplyAdded, ComplaintEventStatusChanged:
	}
	w.WriteHeader(http.StatusOK)
})
fmt.Println("registered")
Output:
registered

type ComplainFinishRequest added in v0.3.0

type ComplainFinishRequest struct {
	ComplainNo string // 投诉编号
	Content    string // 完结说明,长度 1-100 字符
}

ComplainFinishRequest 为投诉完结参数。完结处理码 code 由本包固定填充 "05"。

type ComplainInfo added in v0.3.0

type ComplainInfo struct {
	Qid                      int64               `json:"qid"`                      // 厂商 ID
	AppID                    string              `json:"appId"`                    // 应用 ID
	ComplainNo               string              `json:"complainNo"`               // 投诉编号
	ComplainTime             string              `json:"complainTime"`             // 投诉时间,YYYY-MM-DD HH:MM:SS
	BankTradeCode            string              `json:"bankTradeCode"`            // 第三方支付流水号
	CurrStatus               int                 `json:"currStatus"`               // 投诉状态,见 ComplainStatus*
	PhoneNo                  string              `json:"phoneNo"`                  // 用户联系方式
	ComplainContent          string              `json:"complainContent"`          // 投诉内容
	PlatForm                 int                 `json:"platForm"`                 // 来源平台,见 ComplainPlatform*
	BankTradeCodes           []string            `json:"bankTradeCodes"`           // 关联的第三方支付流水号列表
	LatestReplyContentDigest string              `json:"latestReplyContentDigest"` // 最新回复摘要
	HasNewMsg                int                 `json:"hasNewMsg"`                // 是否存在新的待处理消息:1 是,0 否
	Images                   []string            `json:"images"`                   // 投诉图片,base64 编码,不带 data:image/... 前缀
	Replies                  []ComplainReplyInfo `json:"replies"`                  // 投诉历史回复列表
}

ComplainInfo 为投诉信息(webhook 的 complain_info)。

服务端以 omitempty 序列化:字段值为空、0、空数组时可能不出现在推送中, 解析后表现为对应零值,不代表平台显式推送了空值。

type ComplainReplyInfo added in v0.3.0

type ComplainReplyInfo struct {
	ComplainNo  string   `json:"complainNo"`  // 投诉编号
	OperateTime string   `json:"operateTime"` // 回复时间
	Operator    string   `json:"operator"`    // 回复操作人
	Content     string   `json:"content"`     // 回复内容
	Images      []string `json:"images"`      // 回复图片,base64 编码,不带 data:image/... 前缀
}

ComplainReplyInfo 为投诉回复信息(webhook 的 reply_info 及 complain_info.replies 元素)。

type ComplainReplyRequest added in v0.3.0

type ComplainReplyRequest struct {
	ComplainNo string // 投诉编号,即 webhook 推送中的 complainNo
	Content    string // 回复内容,长度 1-100 字符
	Source     int    // 回复来源,见 ComplainSourceNormal / ComplainSourceRefund
}

ComplainReplyRequest 为投诉回复参数。

type ComplaintWebhook added in v0.3.0

type ComplaintWebhook struct {
	MessageID      string             `json:"message_id"`      // 推送消息 ID,与请求头 X-Message-Id 一致,请据此做幂等
	EventType      string             `json:"event_type"`      // 事件类型,见 ComplaintEvent*
	PayloadVersion string             `json:"payload_version"` // 推送协议版本,当前固定 v1
	ComplainInfo   *ComplainInfo      `json:"complain_info"`   // 投诉信息
	ReplyInfo      *ComplainReplyInfo `json:"reply_info"`      // 本次新增回复信息

	Timestamp int64 `json:"-"` // 外层推送时间戳,单位秒
}

ComplaintWebhook 为投诉 webhook 推送的解析结果。

ComplainInfo 与 ReplyInfo 按事件类型出现:COMPLAINT_CREATED、REPLY_ADDED、 STATUS_CHANGED 均携带 ComplainInfo;仅 REPLY_ADDED 携带 ReplyInfo。未携带时为 nil。

func ParseComplaintWebhook added in v0.3.0

func ParseComplaintWebhook(body []byte) (*ComplaintWebhook, error)

ParseComplaintWebhook 解析投诉 webhook 推送体。 它不做验签;如需验签请使用 Client.VerifyComplaintWebhook

type CreateOrderParams

type CreateOrderParams struct {
	OrderID     string // 厂商订单号,需保证应用内唯一
	OrderAmount int64  // 订单金额,单位:分
	CreateTime  string // 厂商创建订单的时间戳(10 位,秒级)
	UserID      string // 用户 ID
	ProductID   string // 商品 ID
	ProductName string // 商品描述

	AutoPayStatus int    // 是否开启代扣,见 AutoPayDisabled / AutoPayEnabled
	OrderPayType  int    // 订单类型,见 OrderPayTypeNormal / OrderPayTypeTask
	PeriodType    int    // 代扣周期类型,见 PeriodTypeDay / PeriodTypeMonth
	Period        int    // 代扣周期值(与 PeriodType 组合,如 PeriodTypeDay+90 表示 90 天)
	ExecuteTime   string // 首次扣款时间,格式 yyyy-MM-dd
	AutoPayAmount int64  // 代扣金额,单位:分
	AutopayMode   int    // 代扣发起方,见 AutopayModeManager / AutopayModeVendor
	TaskID        string // 任务 ID,OrderPayType == OrderPayTypeTask 时必填(不要求开启代扣)
}

CreateOrderParams 是前端 SDK360.createOrder 所需的订单参数。

360 联运的下单由前端 JSSDK 直连完成,但订单数据应由厂商服务端生成唯一 order_id、 组装并留存后下发给前端。本结构提供类型化构造、条件校验与序列化,不发起任何请求。

代扣字段(AutoPayStatus 起的一组)仅当开启代扣(AutoPayStatus == AutoPayEnabled)时 生效且必填,校验由 CreateOrderParams.Validate 强制。

任务单(OrderPayType == OrderPayTypeTask)独立于代扣:不开启代扣亦可构造, 此时 TaskID 必填,且 OrderAmount 允许为 0(依据文档任务示例 task_amount 为 0)。

func (CreateOrderParams) MarshalForSDK

func (p CreateOrderParams) MarshalForSDK() ([]byte, error)

MarshalForSDK 校验并序列化为前端 SDK360.createOrder 所需的 JSON。

非代扣付费单只输出基础字段;任务单无论是否开启代扣均附带 order_pay_type 与 task_id; 开启代扣时附带代扣字段及 ext(autopay_mode 的 JSON 字符串)。 数字字段以 JSON number 输出,与 360 前端约定一致。

Example
p := CreateOrderParams{
	OrderID: "order-1", OrderAmount: 1, CreateTime: "1700000000",
	UserID: "user-1", ProductID: "vip-1", ProductName: "会员月卡",
}
data, _ := p.MarshalForSDK()
fmt.Println(string(data))
Output:
{"create_time":"1700000000","order_amount":1,"order_id":"order-1","product_id":"vip-1","product_name":"会员月卡","user_id":"user-1"}

func (CreateOrderParams) Validate

func (p CreateOrderParams) Validate() error

Validate 校验参数。基础字段与订单类型恒校验;任务单强制 task_id 且允许金额为 0; 开启代扣时强制校验代扣相关字段。

type DoPostRequest

type DoPostRequest struct {
	OrderID         string // 签约的订单 ID
	AgreementNumber string // 签约的协议号
	AutopayAmount   int64  // 代扣金额,单位:分
	AutopayOrderID  string // 本次代扣的订单 ID,须保证唯一
}

DoPostRequest 为厂商侧发起代扣的参数。

本接口仅用于厂商服务端主动发起代扣,代扣发起日期与周期由厂商自行设定。 注意:若上一笔代扣失败需重新发起,必须使用一个全新的 AutopayOrderID, 复用旧值可能出现“代扣失败但显示成功”的异常。

type Option

type Option func(*Client)

Option 配置 Client

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL 覆盖接口域名(默认 https://api.openstore.360.cn)。主要用于测试。

func WithClock

func WithClock(now func() time.Time) Option

WithClock 注入时间源,主要用于测试中控制 token 过期与 timestamp。

func WithHTTPClient

func WithHTTPClient(h *httpc.Client) Option

WithHTTPClient 注入自定义的 httpc 客户端(用于自定义超时、传输等)。 如需请求日志,可在构造该客户端时通过 httpc.WithTransport 注入带日志的 RoundTripper。

func WithTokenCache

func WithTokenCache(tc TokenCache) Option

WithTokenCache 注入自定义的 access_token 缓存实现(如基于 Redis 的多实例共享)。

func WithTokenRefreshAhead

func WithTokenRefreshAhead(d time.Duration) Option

WithTokenRefreshAhead 设置 token 提前刷新的安全边界(默认 5 分钟)。 剩余有效期不足该值时,下一次取 token 会触发刷新。

func WithTokenRefreshLock added in v0.2.0

func WithTokenRefreshLock(l TokenRefreshLock) Option

WithTokenRefreshLock 注入 access_token 刷新锁(如基于 Redis/etcd 的分布式锁)。

func WithVendorKey added in v0.3.0

func WithVendorKey(k string) Option

WithVendorKey 设置 360 下发的厂商密钥,用于投诉 webhook 验签 (见 Client.VerifyComplaintWebhook)。未设置时验签使用 appsecret。

type OrderExtra

type OrderExtra struct {
	MfrOrderID      string `json:"mfr_order_id"`
	AgreementNumber string `json:"agreement_number"`
	AutoPayStatus   int    `json:"auto_pay_status"`
}

OrderExtra 为回调中 order_extra 字段(JSON 字符串)解析后的内容。

type OrderQueryRequest

type OrderQueryRequest struct {
	OrderID string // 厂商调用 SDK 传入的订单 ID
	UserID  string // 用户 ID
}

OrderQueryRequest 为订单查询参数。

type OrderQueryResponse added in v0.3.1

type OrderQueryResponse struct {
	MfrOrderID      string `json:"mfr_order_id"`      // 厂商订单编号
	MfrOrderAmount  int64  `json:"mfr_order_amount"`  // 厂商订单金额,单位:分
	MfrCreateTime   string `json:"mfr_create_time"`   // 厂商订单创建时间
	MfrProductID    string `json:"mfr_product_id"`    // 厂商商品 ID
	MfrProductName  string `json:"mfr_product_name"`  // 厂商商品名称
	OrderStatus     int    `json:"order_status"`      // 订单状态,见文档状态码
	PayChannel      int    `json:"pay_chanel"`        // 支付渠道 1-微信 2-支付宝(沿用文档查询响应字段名 pay_chanel)
	OrderCode       string `json:"order_code"`        // 360 订单编号
	BankTradeCode   string `json:"bank_trade_code"`   // 银行流水号
	OrderPayTime    string `json:"order_pay_time"`    // 付款时间
	OrderRefundTime string `json:"order_refund_time"` // 退款时间

	HeaderTid string `json:"-"` // 本次响应的 Header-Tid
}

OrderQueryResponse 为订单查询返回的订单详情。

func (OrderQueryResponse) IsPaid added in v0.3.1

func (o OrderQueryResponse) IsPaid() bool

IsPaid 报告订单是否处于支付成功状态(order_status 为 20、30 或 50)。

type PlainInvoiceRequest

type PlainInvoiceRequest struct {
	OrderID       string // 厂商调用 SDK 传入的订单 ID
	InvoiceTitle  string // 发票抬头
	UserEmail     string // 用户邮箱
	TaxRegisterNo string // 纳税人识别号
	Address       string // 地址
	Phone         string // 电话
	BankName      string // 银行名称
	BankAccount   string // 银行账号
	Remarks       string // 备注
}

PlainInvoiceRequest 为普票开具参数。OrderID、InvoiceTitle、UserEmail 必填,其余可选。

type PlainInvoiceResult

type PlainInvoiceResult struct {
	DownloadURL string `json:"download_url"` // PDF 下载地址
	InvoiceCode string `json:"invoice_code"` // 发票代码
	InvoiceNo   string `json:"invoice_no"`   // 发票号码
	ReceiptURL  string `json:"receipt_url"`  // 收票地址,可据此生成二维码
	SuccessTime string `json:"success_time"` // 开票日期
	VerifyCode  string `json:"verify_code"`  // 校验码

	HeaderTid string `json:"-"`
}

PlainInvoiceResult 为普票开具返回结果。

type RefundOrdersRequest added in v0.3.0

type RefundOrdersRequest struct {
	OrderIDs     []string // 厂商订单 ID 列表,发送时以英文逗号拼接,拼接后总长 1-500 字符
	UserID       string   // 用户 ID(厂商的用户唯一标识),长度 1-50 字符
	OrderAmount  int64    // 订单金额,单位:分,必须大于 0;建议与原订单金额一致
	RefundReason string   // 退款原因,长度 1-200 字符
}

RefundOrdersRequest 为多笔订单退款(v2)参数。

type RefundRequest

type RefundRequest struct {
	OrderID      string // 厂商调用 SDK 传入的订单 ID
	OrderAmount  int64  // 订单金额,单位:分
	UserID       string // 用户 ID(厂商的用户唯一标识)
	RefundReason string // 申请退款说明,长度不超过 200 个字符
}

RefundRequest 为订单退款申请参数。

type SpecialInvoiceCancelRequest

type SpecialInvoiceCancelRequest struct {
	Category   string // 红冲类别,1 销方红冲
	InvoiceNum string // 发票号码
	RedReason  string // 红冲原因,如 INVOICE_MISTAKE
	SourceID   string // 蓝票申请单来源 ID
	OrderID    string // 厂商调用 SDK 传入的订单 ID
}

SpecialInvoiceCancelRequest 为专票红冲参数(销方红冲)。

type SpecialInvoiceQueryResult

type SpecialInvoiceQueryResult struct {
	DownloadURL string `json:"download_url"` // PDF 下载地址
	InvoiceCode string `json:"invoice_code"` // 发票代码
	InvoiceNum  string `json:"invoice_num"`  // 发票号码
	ReceiptURL  string `json:"receipt_url"`  // 收票地址
	Status      string `json:"status"`       // 状态,如 SUCCESS_END
	SuccessTime string `json:"success_time"` // 开票/红冲日期

	HeaderTid string `json:"-"`
}

SpecialInvoiceQueryResult 为专票查询返回结果(开票或红冲)。

type SpecialInvoiceRequest

type SpecialInvoiceRequest struct {
	OrderID       string // 厂商调用 SDK 传入的订单 ID
	InvoiceTitle  string // 发票抬头(不支持个人抬头)
	UserEmail     string // 用户邮箱
	TaxRegisterNo string // 纳税人识别号
	Address       string // 地址
	Phone         string // 电话
	BankName      string // 银行名称
	BankAccount   string // 银行账号
	CustomType    string // 商户类型,1 企业
	Remarks       string // 备注
}

SpecialInvoiceRequest 为专票开具参数。除 Remarks 外均为必填。 专票申请后需人工审核(一般 5 个工作日内),可通过 Client.QuerySpecialInvoice 查询进度。

type SpecialInvoiceResult

type SpecialInvoiceResult struct {
	SourceID string `json:"source_id"` // 蓝票申请单来源 ID,红冲时需要

	HeaderTid string `json:"-"`
}

SpecialInvoiceResult 为专票开具返回结果。

type TokenCache

type TokenCache interface {
	// Load 返回当前缓存的 token 及其过期时间;ok 为 false 表示无缓存。
	Load(ctx context.Context) (token string, expireAt time.Time, ok bool, err error)
	// Store 写入新的 token 及其过期时间。
	Store(ctx context.Context, token string, expireAt time.Time) error
}

TokenCache 抽象 access_token 的持久化,供多实例部署共享 token。

默认实现为进程内无锁内存缓存,适用于单实例。多实例(多进程)场景下, 由于“新申请 token 会使旧 token 失效”,应注入基于共享存储(如 Redis)的实现, 并在刷新处自行加分布式锁或采用单点刷新,避免实例间互相作废 token。

type TokenRefreshLock added in v0.2.0

type TokenRefreshLock interface {
	Lock(ctx context.Context, fn func(context.Context) error) error
}

TokenRefreshLock 抽象 access_token 刷新锁,供多实例部署做跨副本单飞。

Lock 必须在成功获取锁后执行 fn,并在 fn 返回前释放锁。若获取锁失败,应返回错误且不执行 fn。 默认实现不加跨进程锁,仅适用于单实例或调用方不需要跨副本单飞的场景。

Jump to

Keyboard shortcuts

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