docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"simple-memo/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"simple-memo/global"
|
||||
)
|
||||
|
||||
func Encryption() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 只处理POST
|
||||
if c.Request.Method == "POST" {
|
||||
body, err := ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
global.Logger.Error("读取请求体失败",
|
||||
append(utils.LogContextFields(c), zap.Error(err))...,
|
||||
)
|
||||
utils.Fail(c, "读取请求体失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if len(body) == 0 {
|
||||
utils.Fail(c, "请求体为空")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 {data:"加密串"}
|
||||
var req map[string]string
|
||||
if err = json.Unmarshal(body, &req); err != nil {
|
||||
global.Logger.Error("解析请求体失败",
|
||||
append(utils.LogContextFields(c), zap.Error(err))...,
|
||||
)
|
||||
utils.Fail(c, "参数格式错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
encryptedData, ok := req["data"]
|
||||
if !ok {
|
||||
utils.Fail(c, "参数格式错误")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
baseFields := utils.LogContextFields(c)
|
||||
|
||||
// ====================== 日志:打印【加密串】 ======================
|
||||
global.Logger.Info("【请求加密数据】",
|
||||
append(baseFields,
|
||||
zap.String("encrypt_data", encryptedData),
|
||||
)...,
|
||||
)
|
||||
|
||||
// 解密
|
||||
decrypted, err := utils.AESDecrypt(encryptedData)
|
||||
if err != nil {
|
||||
utils.Fail(c, "解密失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// ====================== 日志:打印【解密后的明文】 ======================
|
||||
global.Logger.Info("【请求解密明文】",
|
||||
append(baseFields,
|
||||
zap.String("decrypt_data", string(decrypted)),
|
||||
)...,
|
||||
)
|
||||
|
||||
// 重新赋值给Body
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(decrypted))
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// JWT鉴权:验证token并把userID存入上下文
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"simple-memo/global"
|
||||
"simple-memo/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func JWTAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从header取token
|
||||
token := c.GetHeader("token")
|
||||
if token == "" {
|
||||
// 检查是否为网页端页面路由(/web/*)
|
||||
isWebPage := len(c.Request.URL.Path) >= 5 && c.Request.URL.Path[:5] == "/web/"
|
||||
if isWebPage {
|
||||
// 网页端页面:重定向到登录页
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
} else {
|
||||
// API接口:返回JSON错误
|
||||
utils.Fail(c, "请先登录")
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 token 是否在黑名单中
|
||||
ctx := context.Background()
|
||||
exists, err := global.Redis.Exists(ctx, "token_blacklist:"+token).Result()
|
||||
if err != nil {
|
||||
requestID, _ := c.Get("request_id")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
route, _ := c.Get("route")
|
||||
global.Logger.Warn("检查token黑名单失败",
|
||||
zap.String("request_id", requestID.(string)),
|
||||
zap.String("route", route.(string)),
|
||||
zap.String("client_ip", clientIP.(string)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if exists == 1 {
|
||||
// 检查是否为网页端页面路由
|
||||
isWebPage := len(c.Request.URL.Path) >= 5 && c.Request.URL.Path[:5] == "/web/"
|
||||
if isWebPage {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
} else {
|
||||
utils.Fail(c, "登录已过期", 1000)
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析
|
||||
claims, err := utils.ParseToken(token)
|
||||
if err != nil {
|
||||
// 检查是否为网页端页面路由
|
||||
isWebPage := len(c.Request.URL.Path) >= 5 && c.Request.URL.Path[:5] == "/web/"
|
||||
if isWebPage {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
} else {
|
||||
utils.Fail(c, "登录已过期", 1000)
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 把userID和token存入上下文
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("token", token)
|
||||
|
||||
// 同时写入标准 context,供整条链路使用
|
||||
ctx = context.WithValue(c.Request.Context(), utils.UserIDKey, claims.UserID)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// IP限流:防刷、高并发保护
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"simple-memo/global"
|
||||
"simple-memo/utils"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Limiter() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
key := "limiter:" + c.ClientIP()
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 10秒内请求次数+1
|
||||
global.Redis.Incr(ctx, key)
|
||||
global.Redis.Expire(ctx, key, 10*time.Second)
|
||||
|
||||
// 转成数字再比较
|
||||
countStr := global.Redis.Get(ctx, key).Val()
|
||||
count, _ := strconv.Atoi(countStr)
|
||||
|
||||
// 10秒最多 100 次(正常开发完全够用)
|
||||
if count > 100 {
|
||||
utils.Fail(c, "请求过于频繁")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// 请求日志中间件:记录请求ID、路由、客户端IP等关键信息
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"simple-memo/global"
|
||||
"simple-memo/utils"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sony/sonyflake"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// requestIDSF 用于生成全局唯一的请求ID
|
||||
var requestIDSF *sonyflake.Sonyflake
|
||||
|
||||
func init() {
|
||||
requestIDSF = sonyflake.NewSonyflake(sonyflake.Settings{})
|
||||
}
|
||||
|
||||
// genRequestID 基于 sonyflake 生成请求ID(base36 紧凑字符串)
|
||||
func genRequestID() string {
|
||||
if requestIDSF == nil {
|
||||
return fmt.Sprintf("%x", time.Now().UnixNano())
|
||||
}
|
||||
id, err := requestIDSF.NextID()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%x", time.Now().UnixNano())
|
||||
}
|
||||
// base36 编码:紧凑且只含 [0-9a-z]
|
||||
return strconv.FormatUint(id, 36)
|
||||
}
|
||||
|
||||
// RequestLog 请求日志中间件
|
||||
func RequestLog() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
// 优先从请求头获取 request-id,否则自动生成
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = genRequestID()
|
||||
}
|
||||
|
||||
// 客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 路由路径
|
||||
route := c.Request.URL.Path
|
||||
|
||||
// 请求方法
|
||||
method := c.Request.Method
|
||||
|
||||
// 写入标准 context,供整条链路使用
|
||||
ctx := context.WithValue(c.Request.Context(), utils.RequestIdKey, requestID)
|
||||
ctx = context.WithValue(ctx, utils.RequestRouteKey, route)
|
||||
ctx = context.WithValue(ctx, utils.ClientIPKey, clientIP)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
// 同时记录到 gin context,兼容老代码
|
||||
c.Set("request_id", requestID)
|
||||
c.Set("client_ip", clientIP)
|
||||
c.Set("route", route)
|
||||
|
||||
// 打印请求进入日志
|
||||
global.Logger.Info("【请求进入】",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("route", route),
|
||||
zap.String("method", method),
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
)
|
||||
|
||||
c.Next()
|
||||
|
||||
// 请求结束后打印耗时日志
|
||||
duration := time.Since(start)
|
||||
global.Logger.Info("【请求结束】",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("route", route),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Duration("duration", duration),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user