docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
This commit is contained in:
@@ -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]
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# 敏感词库
|
||||
# 每行一个敏感词,# 开头的行为注释
|
||||
# 政治敏感词
|
||||
习近平
|
||||
江泽民
|
||||
胡锦涛
|
||||
温家宝
|
||||
李克强
|
||||
法轮功
|
||||
六四
|
||||
天安门
|
||||
台独
|
||||
藏独
|
||||
疆独
|
||||
反共
|
||||
反党
|
||||
|
||||
# 色情相关
|
||||
色情
|
||||
黄色
|
||||
裸体
|
||||
做爱
|
||||
性交
|
||||
强奸
|
||||
乱伦
|
||||
嫖娼
|
||||
卖淫
|
||||
AV
|
||||
av
|
||||
淫秽
|
||||
|
||||
# 暴力相关
|
||||
杀人
|
||||
自杀
|
||||
爆炸
|
||||
恐怖
|
||||
炸弹
|
||||
袭击
|
||||
砍人
|
||||
刺杀
|
||||
|
||||
# 赌博相关
|
||||
赌博
|
||||
赌钱
|
||||
博彩
|
||||
六合彩
|
||||
赌场
|
||||
赌球
|
||||
|
||||
# 毒品相关
|
||||
毒品
|
||||
吸毒
|
||||
贩毒
|
||||
冰毒
|
||||
海洛因
|
||||
大麻
|
||||
摇头丸
|
||||
|
||||
# 侮辱性词汇
|
||||
傻逼
|
||||
操你
|
||||
妈的
|
||||
王八蛋
|
||||
畜生
|
||||
滚蛋
|
||||
去死
|
||||
草泥马
|
||||
他妈的
|
||||
他妈
|
||||
|
||||
# 广告相关
|
||||
代开发票
|
||||
办证
|
||||
刻章
|
||||
枪支
|
||||
迷药
|
||||
催情
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user