docs(website): 添加简记memo产品原型和UI设计规范文档

- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
This commit is contained in:
sunct
2026-07-31 14:12:32 +08:00
commit 3c3bf53ae4
115 changed files with 21304 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// AES加解密:请求体加密
package utils
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"simple-memo/global"
)
// 注意:这里不再直接初始化,等到配置加载后再赋值
var AESKey []byte
var AESIV []byte
// InitAES 初始化AES密钥(必须在配置加载后调用)
func InitAES() {
AESKey = []byte(global.VP.GetString("aes.key")) // 32位
AESIV = []byte(global.VP.GetString("aes.iv")) // 16位
}
// PKCS7Padding 填充(修复完成)
func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padText...) // 这里加 ... 即可修复
}
// AESEncrypt 加密
func AESEncrypt(origData []byte) (string, error) {
block, err := aes.NewCipher(AESKey)
if err != nil {
return "", err
}
blockSize := block.BlockSize()
origData = PKCS7Padding(origData, blockSize)
blockMode := cipher.NewCBCEncrypter(block, AESIV)
encrypted := make([]byte, len(origData))
blockMode.CryptBlocks(encrypted, origData)
return base64.StdEncoding.EncodeToString(encrypted), nil
}
// AESDecrypt 解密
func AESDecrypt(encrypted string) ([]byte, error) {
block, err := aes.NewCipher(AESKey)
if err != nil {
return nil, err
}
decoded, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return nil, err
}
blockMode := cipher.NewCBCDecrypter(block, AESIV)
orig := make([]byte, len(decoded))
blockMode.CryptBlocks(orig, decoded)
// ========= 这里换成安全版去填充 =========
orig = PKCS7UnPaddingSafe(orig)
return orig, nil
}
// PKCS7UnPadding 去填充(原版保留,不删)
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
if length == 0 {
return []byte{} // 空数据直接返回,不崩溃
}
unPadding := int(origData[length-1])
return origData[:length-unPadding]
}
// PKCS7UnPaddingSafe 安全去填充,永不 panic
func PKCS7UnPaddingSafe(origData []byte) []byte {
length := len(origData)
if length == 0 {
return []byte{}
}
unPadding := int(origData[length-1])
// ========= 安全校验:防止 slice 越界崩溃 =========
if unPadding < 1 || unPadding > length {
return origData
}
return origData[:length-unPadding]
}
+16
View File
@@ -0,0 +1,16 @@
// 链路追踪上下文 key 定义
package utils
// contextKey 私有类型防止外部冲突
type contextKey string
const (
// RequestIdKey 请求ID
RequestIdKey contextKey = "request_id"
// RequestRouteKey 请求路由
RequestRouteKey contextKey = "request_route"
// ClientIPKey 客户端IP
ClientIPKey contextKey = "client_ip"
// UserIDKey 用户ID
UserIDKey contextKey = "user_id"
)
+55
View File
@@ -0,0 +1,55 @@
// utils/date.go
// 日期辅助函数
package utils
import (
"time"
)
// GetCurrentYear 获取当前年份
func GetCurrentYear() int {
return time.Now().Year()
}
// GetCurrentMonth 获取当前月份
func GetCurrentMonth() int {
return int(time.Now().Month())
}
// GetMonthLastDay 获取某年某月的最后一天
func GetMonthLastDay(year, month int) (yearOut, lastDay int) {
// 使用time.Date构建该月第一天的日期
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local)
// 添加一个月,然后减去一天得到最后一天
lastDayDate := firstDay.AddDate(0, 1, -1)
return lastDayDate.Year(), lastDayDate.Day()
}
// FormatDate 格式化日期为字符串
func FormatDate(t time.Time) string {
return t.Format("2006-01-02")
}
// FormatDateTime 格式化日期时间为字符串
func FormatDateTime(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
}
// DurUntilTomorrowZero 计算当前时间到明天零点的间隔
func DurUntilTomorrowZero() time.Duration {
now := time.Now()
tomorrowZero := now.Truncate(24*time.Hour).AddDate(0, 0, 1)
return tomorrowZero.Sub(now)
}
// GetSecondsUntilTomorrowZero 获取当前时间到明天零点之间的秒数
func GetSecondsUntilTomorrowZero() int64 {
now := time.Now()
// 今天零点
todayZero := now.Truncate(24 * time.Hour)
// 明天零点
tomorrowZero := todayZero.AddDate(0, 0, 1)
diff := tomorrowZero.Sub(now)
// 转秒
return int64(time.Duration(diff.Seconds()))
}
+183
View File
@@ -0,0 +1,183 @@
// 邮件发送工具
package utils
import (
"crypto/tls"
"fmt"
"go.uber.org/zap"
"net/smtp"
"simple-memo/global"
"time"
)
// SendEmail 发送邮件 (阿里企业邮箱标准实现)
func SendEmail(to, subject, body string) error {
from := global.VP.GetString("email.from")
smtpHost := global.VP.GetString("email.smtp_host")
smtpPort := global.VP.GetInt("email.smtp_port")
smtpUser := global.VP.GetString("email.smtp_user")
smtpPassword := global.VP.GetString("email.smtp_password")
if smtpUser == "" || smtpPassword == "" {
return fmt.Errorf("邮件配置未完善")
}
auth := smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
msg := []byte(fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s",
from, to, subject, body))
addr := fmt.Sprintf("%s:%d", smtpHost, smtpPort)
var client *smtp.Client
var err error
switch smtpPort {
case 465:
tlsConfig := &tls.Config{
ServerName: smtpHost,
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
global.Logger.Error("TLS连接失败", zap.String("host", smtpHost), zap.Int("port", smtpPort), zap.Error(err))
return fmt.Errorf("TLS连接失败: %v", err)
}
defer conn.Close()
client, err = smtp.NewClient(conn, smtpHost)
if err != nil {
global.Logger.Error("创建SMTP客户端失败", zap.Error(err))
return fmt.Errorf("创建SMTP客户端失败: %v", err)
}
defer client.Quit()
if err = client.Auth(auth); err != nil {
global.Logger.Error("SMTP认证失败", zap.Error(err))
return fmt.Errorf("SMTP认证失败: %v", err)
}
if err = client.Mail(smtpUser); err != nil {
global.Logger.Error("设置发件人失败", zap.Error(err))
return fmt.Errorf("设置发件人失败: %v", err)
}
if err = client.Rcpt(to); err != nil {
global.Logger.Error("设置收件人失败", zap.String("to", to), zap.Error(err))
return fmt.Errorf("设置收件人失败: %v", err)
}
w, err := client.Data()
if err != nil {
global.Logger.Error("获取数据写入器失败", zap.Error(err))
return fmt.Errorf("获取数据写入器失败: %v", err)
}
_, err = w.Write(msg)
if err != nil {
w.Close()
global.Logger.Error("写入邮件内容失败", zap.Error(err))
return fmt.Errorf("写入邮件内容失败: %v", err)
}
err = w.Close()
if err != nil {
global.Logger.Error("关闭写入器失败", zap.Error(err))
return fmt.Errorf("关闭写入器失败: %v", err)
}
global.Logger.Info("邮件发送成功", zap.String("to", to), zap.String("subject", subject))
return nil
case 587:
client, err = smtp.Dial(addr)
if err != nil {
global.Logger.Error("连接SMTP服务器失败", zap.String("addr", addr), zap.Error(err))
return fmt.Errorf("连接SMTP服务器失败: %v", err)
}
defer client.Close()
if err = client.StartTLS(&tls.Config{ServerName: smtpHost}); err != nil {
global.Logger.Error("启动TLS失败", zap.Error(err))
return fmt.Errorf("启动TLS失败: %v", err)
}
if err = client.Auth(auth); err != nil {
global.Logger.Error("SMTP认证失败", zap.Error(err))
return fmt.Errorf("SMTP认证失败: %v", err)
}
if err = client.Mail(smtpUser); err != nil {
global.Logger.Error("设置发件人失败", zap.Error(err))
return fmt.Errorf("设置发件人失败: %v", err)
}
if err = client.Rcpt(to); err != nil {
global.Logger.Error("设置收件人失败", zap.String("to", to), zap.Error(err))
return fmt.Errorf("设置收件人失败: %v", err)
}
w, err := client.Data()
if err != nil {
global.Logger.Error("获取数据写入器失败", zap.Error(err))
return fmt.Errorf("获取数据写入器失败: %v", err)
}
_, err = w.Write(msg)
if err != nil {
w.Close()
global.Logger.Error("写入邮件内容失败", zap.Error(err))
return fmt.Errorf("写入邮件内容失败: %v", err)
}
err = w.Close()
if err != nil {
global.Logger.Error("关闭写入器失败", zap.Error(err))
return fmt.Errorf("关闭写入器失败: %v", err)
}
global.Logger.Info("邮件发送成功", zap.String("to", to), zap.String("subject", subject))
return nil
default:
err = smtp.SendMail(addr, auth, smtpUser, []string{to}, msg)
if err != nil {
global.Logger.Error("SMTP发送失败", zap.String("addr", addr), zap.Error(err))
return fmt.Errorf("SMTP发送失败: %v", err)
}
global.Logger.Info("邮件发送成功", zap.String("to", to), zap.String("subject", subject))
return nil
}
}
// SendEmailCode 发送邮箱验证码
func SendEmailCode(to, code string) error {
subject := "简记memo - 邮箱验证"
body := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>邮箱验证</title></head><body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;"><div style="background:#ffffff;border-radius:8px;padding:24px;"><div style="font-size:18px;font-weight:600;color:#333;margin-bottom:16px;">简记memo</div><p style="color:#555;font-size:14px;line-height:1.6;margin-bottom:16px;">您好,</p><p style="color:#555;font-size:14px;line-height:1.6;margin-bottom:20px;">您正在进行邮箱验证操作,以下是您的验证码:</p><div style="background:#f5f5f5;border-radius:4px;padding:16px 24px;text-align:center;margin-bottom:20px;"><span style="font-size:28px;font-weight:600;color:#333;letter-spacing:4px;">%s</span></div><p style="color:#888;font-size:12px;line-height:1.6;margin-bottom:8px;">验证码有效期为5分钟,请在有效期内完成验证。</p><p style="color:#888;font-size:12px;line-height:1.6;">如果这不是您的操作,请忽略此邮件。</p><div style="margin-top:24px;padding-top:16px;border-top:1px solid #eee;"><p style="color:#aaa;font-size:11px;">简记memo团队</p></div></div></body></html>`, code)
// 使用重试机制发送邮件
var lastErr error
for attempt := 1; attempt <= 3; attempt++ {
err := SendEmail(to, subject, body)
if err == nil {
global.Logger.Info("邮件发送成功", zap.String("to", to), zap.String("subject", subject))
return nil
}
lastErr = err
global.Logger.Warn("邮件发送失败,准备重试",
zap.String("to", to),
zap.Int("attempt", attempt),
zap.Int("maxRetries", 3),
zap.Error(err))
// 等待一段时间后重试(指数退避)
if attempt < 3 {
time.Sleep(time.Duration(attempt) * time.Second)
}
}
return fmt.Errorf("邮件发送失败,已重试3次: %v", lastErr)
}
+83
View File
@@ -0,0 +1,83 @@
// JWT登录鉴权
package utils
import (
"simple-memo/global"
"time"
"github.com/golang-jwt/jwt/v4"
)
// Claims 自定义载荷
type Claims struct {
UserID uint // 用户ID
jwt.RegisteredClaims
}
// GenerateToken 生成Token
func GenerateToken(userID uint) (string, error) {
claims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(global.VP.GetInt("jwt.expire")) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
// 签发
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(global.VP.GetString("jwt.key")))
}
// GenerateTokenWithExpire 生成Token并返回过期时间
func GenerateTokenWithExpire(userID uint) (string, int64, error) {
expireSeconds := global.VP.GetInt("jwt.expire")
if expireSeconds == 0 {
expireSeconds = 7200 // 默认2小时
}
expiresAt := time.Now().Add(time.Duration(expireSeconds) * time.Second).UnixMilli()
claims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.UnixMilli(expiresAt)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(global.VP.GetString("jwt.key")))
return tokenStr, expiresAt, err
}
// GetTokenExpireSeconds 获取配置的token过期秒数
func GetTokenExpireSeconds() int64 {
expireSeconds := global.VP.GetInt("jwt.expire")
if expireSeconds == 0 {
return 7200 // 默认2小时
}
return int64(expireSeconds)
}
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) string {
const charset = "0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[global.Rand.Intn(len(charset))]
}
return string(b)
}
// ParseToken 解析Token
func ParseToken(token string) (*Claims, error) {
t, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(global.VP.GetString("jwt.key")), nil
})
if err != nil {
return nil, err
}
// 校验
if claims, ok := t.Claims.(*Claims); ok && t.Valid {
return claims, nil
}
return nil, err
}
+117
View File
@@ -0,0 +1,117 @@
// 统一返回格式 + 自动加密
package utils
import (
"encoding/json"
"go.uber.org/zap"
"net/http"
"simple-memo/global"
"github.com/gin-gonic/gin"
)
// Response 统一返回结构体
type Response struct {
Code int `json:"code"` // 200成功
Msg string `json:"msg"` // 提示信息
Data interface{} `json:"data,omitempty"`
}
// LogContextFields 从 context 中提取链路日志字段
func LogContextFields(c *gin.Context) []zap.Field {
fields := []zap.Field{}
if c == nil {
return fields
}
ctx := c.Request.Context()
// 优先从标准 context 读取
if v := ctx.Value(RequestIdKey); v != nil {
fields = append(fields, zap.String("request_id", v.(string)))
} else if v, ok := c.Get("request_id"); ok {
fields = append(fields, zap.String("request_id", v.(string)))
}
if v := ctx.Value(RequestRouteKey); v != nil {
fields = append(fields, zap.String("route", v.(string)))
} else if v, ok := c.Get("route"); ok {
fields = append(fields, zap.String("route", v.(string)))
}
if v := ctx.Value(ClientIPKey); v != nil {
fields = append(fields, zap.String("client_ip", v.(string)))
} else if v, ok := c.Get("client_ip"); ok {
fields = append(fields, zap.String("client_ip", v.(string)))
}
if uid := c.GetUint("userID"); uid > 0 {
fields = append(fields, zap.Uint("user_id", uid))
}
return fields
}
// Result 统一返回(根据请求路径决定是否加密)
func Result(c *gin.Context, code int, msg string, data interface{}) {
// 添加缓存控制头,防止浏览器缓存
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
resp := Response{Code: code, Msg: msg, Data: data}
// 先序列化成JSON
jsonStr, _ := json.Marshal(resp)
// ====================== 日志:打印【要加密的明文】 ======================
baseFields := LogContextFields(c)
global.Logger.Info("【返回原始明文】",
append(baseFields,
zap.String("path", c.Request.URL.Path),
zap.String("raw_data", string(jsonStr)),
)...,
)
// 判断是否为网页端接口(/api/web 路径)
isWebAPI := len(c.Request.URL.Path) >= 8 && c.Request.URL.Path[:8] == "/api/web"
if isWebAPI {
// 网页端:直接返回JSON,不加密
c.JSON(http.StatusOK, resp)
} else {
// 小程序端:加密返回
encrypted, err := AESEncrypt(jsonStr)
if err != nil {
global.Logger.Error("加密失败",
append(baseFields, zap.Error(err))...,
)
c.JSON(http.StatusOK, Response{Code: 500, Msg: "加密错误"})
return
}
// ====================== 日志:打印【加密后返回给前端的串】 ======================
global.Logger.Info("【返回加密数据】",
append(baseFields,
zap.String("encrypt_data", encrypted),
)...,
)
c.JSON(http.StatusOK, gin.H{"data": encrypted})
}
}
// Ok 成功
func Ok(c *gin.Context, data interface{}) {
Result(c, 200, "success", data)
}
// Fail 失败
func Fail(c *gin.Context, msg string, code ...int) {
needCode := 400
if len(code) > 0 {
needCode = code[0]
}
Result(c, needCode, msg, nil)
}
+110
View File
@@ -0,0 +1,110 @@
// utils/sensitive.go
// 敏感词过滤工具
package utils
import (
"embed"
"strings"
"sync"
"github.com/importcjj/sensitive"
)
//go:embed sensitive_words.txt
var sensitiveWordsFS embed.FS
var (
filter *sensitive.Filter
filterOnce sync.Once
)
// GetSensitiveFilter 获取敏感词过滤器单例
func GetSensitiveFilter() *sensitive.Filter {
filterOnce.Do(func() {
filter = sensitive.New()
loadDefaultWords()
})
return filter
}
// loadDefaultWords 加载默认敏感词库
func loadDefaultWords() {
// 从嵌入的文件系统读取
data, err := sensitiveWordsFS.ReadFile("sensitive_words.txt")
if err != nil {
// 如果文件不存在,使用内置的基础词库
loadBuiltinWords()
return
}
words := strings.Split(string(data), "\n")
for _, word := range words {
word = strings.TrimSpace(word)
if word != "" && !strings.HasPrefix(word, "#") {
filter.AddWord(word)
}
}
}
// loadBuiltinWords 加载内置基础敏感词
func loadBuiltinWords() {
// 基硎敏感词列表
builtinWords := []string{
// 政治相关
"习近平", "江泽民", "胡锦涛", "温家宝", "李克强",
"法轮功", "六四", "天安门", "台独", "藏独", "疆独",
// 色情相关
"色情", "黄色", "裸体", "做爱", "性交", "强奸", "乱伦",
// 暴力相关
"杀人", "自杀", "爆炸", "恐怖", "炸弹", "袭击",
// 赌博相关
"赌博", "赌钱", "博彩", "六合彩",
// 毒品相关
"毒品", "吸毒", "贩毒", "冰毒", "海洛因",
// 侮辱性词汇
"傻逼", "操你", "妈的", "王八蛋", "畜生",
}
for _, word := range builtinWords {
filter.AddWord(word)
}
}
// FilterSensitive 过滤敏感词,替换为 *
func FilterSensitive(text string) string {
f := GetSensitiveFilter()
return f.Replace(text, '*')
}
// ContainsSensitive 检查是否包含敏感词
func ContainsSensitive(text string) bool {
f := GetSensitiveFilter()
has, _ := f.FindIn(text)
return has
}
// FindAllSensitive 查找所有敏感词
func FindAllSensitive(text string) []string {
f := GetSensitiveFilter()
return f.FindAll(text)
}
// ValidateContent 验证内容,返回是否通过和第一个敏感词
func ValidateContent(text string) (bool, string) {
f := GetSensitiveFilter()
return f.Validate(text)
}
// AddSensitiveWord 动态添加敏感词
func AddSensitiveWord(word string) {
f := GetSensitiveFilter()
f.AddWord(word)
}
// AddSensitiveWords 批量添加敏感词
func AddSensitiveWords(words []string) {
f := GetSensitiveFilter()
for _, word := range words {
f.AddWord(word)
}
}
+77
View File
@@ -0,0 +1,77 @@
# 敏感词库
# 每行一个敏感词,# 开头的行为注释
# 政治敏感词
习近平
江泽民
胡锦涛
温家宝
李克强
法轮功
六四
天安门
台独
藏独
疆独
反共
反党
# 色情相关
色情
黄色
裸体
做爱
性交
强奸
乱伦
嫖娼
卖淫
AV
av
淫秽
# 暴力相关
杀人
自杀
爆炸
恐怖
炸弹
袭击
砍人
刺杀
# 赌博相关
赌博
赌钱
博彩
六合彩
赌场
赌球
# 毒品相关
毒品
吸毒
贩毒
冰毒
海洛因
大麻
摇头丸
# 侮辱性词汇
傻逼
操你
妈的
王八蛋
畜生
滚蛋
去死
草泥马
他妈的
他妈
# 广告相关
代开发票
办证
刻章
枪支
迷药
催情
+37
View File
@@ -0,0 +1,37 @@
package utils
import (
"math/rand"
"simple-memo/global"
"simple-memo/models"
"time"
)
const userCodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
func init() {
rand.Seed(time.Now().UnixNano())
}
func GenerateUserCode() string {
for {
code := generateRandomCode(8)
if isUserCodeUnique(code) {
return code
}
}
}
func generateRandomCode(length int) string {
result := make([]byte, length)
for i := range result {
result[i] = userCodeChars[rand.Intn(len(userCodeChars))]
}
return string(result)
}
func isUserCodeUnique(code string) bool {
var count int64
global.DB.Model(&models.User{}).Where("user_code = ?", code).Count(&count)
return count == 0
}
+120
View File
@@ -0,0 +1,120 @@
// 微信登录工具
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"simple-memo/global"
)
// WXLoginResp 微信登录响应
type WXLoginResp struct {
OpenID string `json:"openid"`
SessionKey string `json:"session_key"`
UnionID string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// WXWebLoginResp 微信网页授权响应
type WXWebLoginResp struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
OpenID string `json:"openid"`
Scope string `json:"scope"`
UnionID string `json:"unionid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
// WXUserInfo 微信用户信息
type WXUserInfo struct {
OpenID string `json:"openid"`
Nickname string `json:"nickname"`
Sex int `json:"sex"`
Province string `json:"province"`
City string `json:"city"`
Country string `json:"country"`
HeadImgURL string `json:"headimgurl"`
Privilege []string `json:"privilege"`
UnionID string `json:"unionid"`
}
// MiniProgramCode2Session 小程序登录:获取openid和session_key
func MiniProgramCode2Session(code string) (WXLoginResp, error) {
appID := global.VP.GetString("wechat.app_id")
appSecret := global.VP.GetString("wechat.app_secret")
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
appID, appSecret, code)
return requestWXAPI(url)
}
// WebCode2Session 网页授权:获取access_token和openid
func WebCode2Session(code string) (WXWebLoginResp, error) {
appID := global.VP.GetString("wechat.app_id")
appSecret := global.VP.GetString("wechat.app_secret")
url := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
appID, appSecret, code)
resp, err := http.Get(url)
if err != nil {
return WXWebLoginResp{}, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return WXWebLoginResp{}, err
}
var wxResp WXWebLoginResp
if err = json.Unmarshal(data, &wxResp); err != nil {
return WXWebLoginResp{}, err
}
if wxResp.ErrCode != 0 {
return WXWebLoginResp{}, fmt.Errorf("微信授权失败:%s", wxResp.ErrMsg)
}
return wxResp, nil
}
// GetWebAuthURL 获取微信网页授权URL
func GetWebAuthURL(state string) string {
appID := global.VP.GetString("wechat.app_id")
redirectURI := global.VP.GetString("wechat.web_redirect_uri")
return fmt.Sprintf("https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect",
appID, redirectURI, state)
}
// requestWXAPI 请求微信API通用方法
func requestWXAPI(url string) (WXLoginResp, error) {
resp, err := http.Get(url)
if err != nil {
return WXLoginResp{}, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return WXLoginResp{}, err
}
var wxResp WXLoginResp
if err = json.Unmarshal(data, &wxResp); err != nil {
return WXLoginResp{}, err
}
if wxResp.ErrCode != 0 {
return WXLoginResp{}, fmt.Errorf("微信登录失败:%s", wxResp.ErrMsg)
}
return wxResp, nil
}