github.com/chanxuehong/wechat@v0.0.0-20230222024006-36f0325263cd/mp/jssdk/ticket_server.go (about)

     1  package jssdk
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"strconv"
     7  	"sync/atomic"
     8  	"time"
     9  	"unsafe"
    10  
    11  	"github.com/chanxuehong/wechat/mp/core"
    12  )
    13  
    14  // jsapi_ticket 中控服务器接口.
    15  type TicketServer interface {
    16  	Ticket() (ticket string, err error)                            // 请求中控服务器返回缓存的 jsapi_ticket
    17  	RefreshTicket(currentTicket string) (ticket string, err error) // 请求中控服务器刷新 jsapi_ticket
    18  	IIDB04E44A0E1DC11E5ADCEA4DB30FED8E1()                          // 接口标识, 没有实际意义
    19  }
    20  
    21  var _ TicketServer = (*DefaultTicketServer)(nil)
    22  
    23  // DefaultTicketServer 实现了 TicketServer 接口.
    24  //
    25  //	NOTE:
    26  //	1. 用于单进程环境.
    27  //	2. 因为 DefaultTicketServer 同时也是一个简单的中控服务器, 而不是仅仅实现 TicketServer 接口,
    28  //	   所以整个系统只能存在一个 DefaultTicketServer 实例!
    29  type DefaultTicketServer struct {
    30  	coreClient *core.Client
    31  
    32  	refreshTicketRequestChan  chan string              // chan currentTicket
    33  	refreshTicketResponseChan chan refreshTicketResult // chan {ticket, err}
    34  
    35  	ticketCache unsafe.Pointer // *jsapiTicket
    36  }
    37  
    38  // NewDefaultTicketServer 创建一个新的 DefaultTicketServer.
    39  func NewDefaultTicketServer(clt *core.Client) (srv *DefaultTicketServer) {
    40  	if clt == nil {
    41  		panic("nil core.Client")
    42  	}
    43  	srv = &DefaultTicketServer{
    44  		coreClient:                clt,
    45  		refreshTicketRequestChan:  make(chan string),
    46  		refreshTicketResponseChan: make(chan refreshTicketResult),
    47  	}
    48  
    49  	go srv.ticketUpdateDaemon(time.Hour * 24 * time.Duration(100+rand.Int63n(200)))
    50  	return
    51  }
    52  
    53  func (srv *DefaultTicketServer) IIDB04E44A0E1DC11E5ADCEA4DB30FED8E1() {}
    54  
    55  func (srv *DefaultTicketServer) Ticket() (ticket string, err error) {
    56  	if p := (*jsapiTicket)(atomic.LoadPointer(&srv.ticketCache)); p != nil {
    57  		return p.Ticket, nil
    58  	}
    59  	return srv.RefreshTicket("")
    60  }
    61  
    62  type refreshTicketResult struct {
    63  	ticket string
    64  	err    error
    65  }
    66  
    67  func (srv *DefaultTicketServer) RefreshTicket(currentTicket string) (ticket string, err error) {
    68  	srv.refreshTicketRequestChan <- currentTicket
    69  	rslt := <-srv.refreshTicketResponseChan
    70  	return rslt.ticket, rslt.err
    71  }
    72  
    73  func (srv *DefaultTicketServer) ticketUpdateDaemon(initTickDuration time.Duration) {
    74  	tickDuration := initTickDuration
    75  
    76  NEW_TICK_DURATION:
    77  	ticker := time.NewTicker(tickDuration)
    78  	for {
    79  		select {
    80  		case currentTicket := <-srv.refreshTicketRequestChan:
    81  			jsapiTicket, cached, err := srv.updateTicket(currentTicket)
    82  			if err != nil {
    83  				srv.refreshTicketResponseChan <- refreshTicketResult{err: err}
    84  				break
    85  			}
    86  			srv.refreshTicketResponseChan <- refreshTicketResult{ticket: jsapiTicket.Ticket}
    87  			if !cached {
    88  				tickDuration = time.Duration(jsapiTicket.ExpiresIn) * time.Second
    89  				ticker.Stop()
    90  				goto NEW_TICK_DURATION
    91  			}
    92  
    93  		case <-ticker.C:
    94  			jsapiTicket, _, err := srv.updateTicket("")
    95  			if err != nil {
    96  				break
    97  			}
    98  			newTickDuration := time.Duration(jsapiTicket.ExpiresIn) * time.Second
    99  			if abs(tickDuration-newTickDuration) > time.Second*5 {
   100  				tickDuration = newTickDuration
   101  				ticker.Stop()
   102  				goto NEW_TICK_DURATION
   103  			}
   104  		}
   105  	}
   106  }
   107  
   108  func abs(x time.Duration) time.Duration {
   109  	if x >= 0 {
   110  		return x
   111  	}
   112  	return -x
   113  }
   114  
   115  type jsapiTicket struct {
   116  	Ticket    string `json:"ticket"`
   117  	ExpiresIn int64  `json:"expires_in"`
   118  }
   119  
   120  // updateTicket 从微信服务器获取新的 jsapi_ticket 并存入缓存, 同时返回该 jsapi_ticket.
   121  func (srv *DefaultTicketServer) updateTicket(currentTicket string) (ticket *jsapiTicket, cached bool, err error) {
   122  	if currentTicket != "" {
   123  		if p := (*jsapiTicket)(atomic.LoadPointer(&srv.ticketCache)); p != nil && currentTicket != p.Ticket {
   124  			return p, true, nil // 无需更改 p.ExpiresIn 参数值, cached == true 时用不到
   125  		}
   126  	}
   127  
   128  	var incompleteURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token="
   129  	var result struct {
   130  		core.Error
   131  		jsapiTicket
   132  	}
   133  	if err = srv.coreClient.GetJSON(incompleteURL, &result); err != nil {
   134  		atomic.StorePointer(&srv.ticketCache, nil)
   135  		return
   136  	}
   137  	if result.ErrCode != core.ErrCodeOK {
   138  		atomic.StorePointer(&srv.ticketCache, nil)
   139  		err = &result.Error
   140  		return
   141  	}
   142  
   143  	// 由于网络的延时, jsapi_ticket 过期时间留有一个缓冲区
   144  	switch {
   145  	case result.ExpiresIn > 31556952: // 60*60*24*365.2425
   146  		atomic.StorePointer(&srv.ticketCache, nil)
   147  		err = errors.New("expires_in too large: " + strconv.FormatInt(result.ExpiresIn, 10))
   148  		return
   149  	case result.ExpiresIn > 60*60:
   150  		result.ExpiresIn -= 60 * 10
   151  	case result.ExpiresIn > 60*30:
   152  		result.ExpiresIn -= 60 * 5
   153  	case result.ExpiresIn > 60*5:
   154  		result.ExpiresIn -= 60
   155  	case result.ExpiresIn > 60:
   156  		result.ExpiresIn -= 10
   157  	default:
   158  		atomic.StorePointer(&srv.ticketCache, nil)
   159  		err = errors.New("expires_in too small: " + strconv.FormatInt(result.ExpiresIn, 10))
   160  		return
   161  	}
   162  
   163  	ticketCopy := result.jsapiTicket
   164  	atomic.StorePointer(&srv.ticketCache, unsafe.Pointer(&ticketCopy))
   165  	ticket = &ticketCopy
   166  	return
   167  }