Files
yuxuanhui 6224ef5980 feat: add growth tracking page and related functionality
- Implemented a new Growth page to track practice time and trends.
- Added API integration for fetching review data.
- Created components for displaying practice statistics and trends.
- Updated navigation titles for the main index and growth pages.
- Removed unused styles from the index page.
- Introduced a Projects management page for adding and editing practice projects.
- Developed a Record form for logging practice sessions with validation.
- Added utility functions for date manipulation and duration formatting.
- Implemented error handling and session management in the practice service.
- Created unit tests for the practice service to ensure reliability.
2026-09-29 13:47:26 +08:00

62 lines
1.9 KiB
Go

package identity
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"time"
)
var ErrInvalidCode = errors.New("WeChat login code is invalid or expired")
var errUnavailable = errors.New("WeChat identity verification is unavailable")
// WeChat exchanges wx.login codes at the server only. Client is an optional
// network dependency for tests; production uses a bounded HTTPS client.
type WeChat struct {
AppID string
AppSecret string
Client *http.Client
}
// Exchange returns an app-scoped openid after WeChat verifies the code. It never
// returns session_key or wraps URL errors, since exchange URLs contain secrets.
func (w *WeChat) Exchange(ctx context.Context, code string) (string, error) {
if w.AppID == "" || w.AppSecret == "" {
return "", errUnavailable
}
query := url.Values{"appid": {w.AppID}, "secret": {w.AppSecret}, "js_code": {code}, "grant_type": {"authorization_code"}}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+query.Encode(), nil)
if err != nil {
return "", errUnavailable
}
client := w.Client
if client == nil {
client = &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
}
response, err := client.Do(request)
if err != nil {
return "", errUnavailable
}
defer response.Body.Close()
if response.StatusCode != 200 {
return "", errUnavailable
}
var result struct {
OpenID string `json:"openid"`
ErrCode int `json:"errcode"`
}
if err = json.NewDecoder(io.LimitReader(response.Body, 16*1024)).Decode(&result); err != nil {
return "", errUnavailable
}
if result.ErrCode == 40029 || result.ErrCode == 40163 {
return "", ErrInvalidCode
}
if result.ErrCode != 0 || result.OpenID == "" {
return "", errUnavailable
}
return w.AppID + ":" + result.OpenID, nil
}