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
+79
View File
@@ -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()
}
}
+81
View File
@@ -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()
}
}
+34
View File
@@ -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()
}
}
+88
View File
@@ -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 生成请求IDbase36 紧凑字符串)
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),
)
}
}