156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"net/mail"
|
||
"strings"
|
||
"sync"
|
||
"unicode"
|
||
"unicode/utf8"
|
||
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
var (
|
||
ErrUserExists = errors.New("用户名已存在")
|
||
ErrEmailExists = errors.New("邮箱已被注册")
|
||
ErrInvalidCred = errors.New("用户名或密码错误")
|
||
ErrUserBanned = errors.New("账号已被禁言")
|
||
ErrWeakPassword = errors.New("密码至少 6 位")
|
||
ErrInvalidUsername = errors.New("用户名 2-32 位,支持中文、字母、数字与下划线")
|
||
ErrInvalidEmail = errors.New("邮箱格式不正确")
|
||
ErrPostNotFound = errors.New("帖子不存在")
|
||
ErrCommentNotFound = errors.New("评论不存在")
|
||
ErrPermissionDenied = errors.New("无权操作")
|
||
ErrBoardNotFound = errors.New("板块不存在")
|
||
ErrPostEditLocked = errors.New("帖子已被管理员锁定,无法编辑")
|
||
ErrPostCommentsLocked = errors.New("该帖子已锁定讨论,无法评论")
|
||
ErrPostEditExpired = errors.New("已超过可编辑时限")
|
||
ErrRevisionNotFound = errors.New("历史版本不存在")
|
||
ErrInvalidSetting = errors.New("无效的设置值")
|
||
ErrSearchKeywordTooShort = errors.New("搜索关键词过短")
|
||
ErrSearchKeywordTooLong = errors.New("搜索关键词过长")
|
||
ErrPostTitleTooLong = errors.New("标题过长")
|
||
ErrPostTagsTooLong = errors.New("标签过长")
|
||
ErrPostContentTooLong = errors.New("正文过长")
|
||
ErrCommentTooLong = errors.New("评论内容过长")
|
||
ErrCaptchaInvalid = errors.New("验证码错误或已过期")
|
||
ErrEmailCodeInvalid = errors.New("邮箱验证码错误或已过期")
|
||
ErrEmailCodeCooldown = errors.New("发送过于频繁,请稍后再试")
|
||
ErrMailNotConfigured = errors.New("邮件服务未配置或未启用")
|
||
ErrRegisterClosed = errors.New("论坛暂未开放注册,请联系管理员配置邮件服务")
|
||
)
|
||
|
||
// HashPassword 使用 bcrypt 加密密码
|
||
func HashPassword(password string) (string, error) {
|
||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
return string(bytes), err
|
||
}
|
||
|
||
// CheckPassword 校验密码
|
||
func CheckPassword(hash, password string) bool {
|
||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||
}
|
||
|
||
// ValidateUsername 校验用户名:中文/字母/数字/下划线,2-32 个字符
|
||
func ValidateUsername(username string) error {
|
||
n := utf8.RuneCountInString(username)
|
||
if n < 2 || n > 32 {
|
||
return ErrInvalidUsername
|
||
}
|
||
for _, r := range username {
|
||
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
|
||
continue
|
||
}
|
||
return ErrInvalidUsername
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// NormalizeEmail 规范化邮箱(小写去空格)
|
||
func NormalizeEmail(email string) string {
|
||
return strings.ToLower(strings.TrimSpace(email))
|
||
}
|
||
|
||
// ValidateEmail 校验邮箱格式
|
||
func ValidateEmail(email string) error {
|
||
email = NormalizeEmail(email)
|
||
if email == "" {
|
||
return ErrInvalidEmail
|
||
}
|
||
addr, err := mail.ParseAddress(email)
|
||
if err != nil || addr.Address != email {
|
||
return ErrInvalidEmail
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidatePassword 校验密码强度
|
||
func ValidatePassword(password string, minLen int) error {
|
||
if minLen <= 0 {
|
||
minLen = 6
|
||
}
|
||
if utf8.RuneCountInString(password) < minLen {
|
||
return fmt.Errorf("密码至少 %d 位", minLen)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SensitiveFilter 敏感词过滤器
|
||
type SensitiveFilter struct {
|
||
mu sync.RWMutex
|
||
words []string
|
||
}
|
||
|
||
func NewSensitiveFilter() *SensitiveFilter {
|
||
return &SensitiveFilter{
|
||
words: []string{"违禁词示例", "广告刷单"},
|
||
}
|
||
}
|
||
|
||
// LoadFromFile 从配置文件加载敏感词,每行一个词
|
||
func (f *SensitiveFilter) LoadFromFile(path string) {
|
||
data, err := osReadFile(path)
|
||
if err != nil {
|
||
return
|
||
}
|
||
lines := strings.Split(string(data), "\n")
|
||
var words []string
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if line != "" && !strings.HasPrefix(line, "#") {
|
||
words = append(words, line)
|
||
}
|
||
}
|
||
if len(words) > 0 {
|
||
f.mu.Lock()
|
||
f.words = words
|
||
f.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
func (f *SensitiveFilter) Filter(text string) string {
|
||
f.mu.RLock()
|
||
defer f.mu.RUnlock()
|
||
result := text
|
||
for _, w := range f.words {
|
||
if w == "" {
|
||
continue
|
||
}
|
||
replacement := strings.Repeat("*", utf8.RuneCountInString(w))
|
||
result = strings.ReplaceAll(result, w, replacement)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// osReadFile 避免循环依赖,简单封装
|
||
func osReadFile(path string) ([]byte, error) {
|
||
return readFile(path)
|
||
}
|
||
|
||
// readFile 由 filter_io.go 实现
|
||
var readFile = func(path string) ([]byte, error) {
|
||
return nil, errors.New("not implemented")
|
||
}
|