feat: add ads/sponsor/captcha modules and polish home & chat UI
This commit is contained in:
236
backend/service/captcha.go
Normal file
236
backend/service/captcha.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
captchaTTL = 5 * time.Minute
|
||||
captchaLen = 5
|
||||
captchaCharset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // 去掉易混 I/O/0/1
|
||||
)
|
||||
|
||||
type captchaEntry struct {
|
||||
answer string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// CaptchaStore 内存图形验证码(单实例足够;重启后未用完的码失效)
|
||||
type CaptchaStore struct {
|
||||
mu sync.Mutex
|
||||
data map[string]captchaEntry
|
||||
}
|
||||
|
||||
func NewCaptchaStore() *CaptchaStore {
|
||||
s := &CaptchaStore{data: make(map[string]captchaEntry)}
|
||||
go s.loopPurge()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *CaptchaStore) loopPurge() {
|
||||
t := time.NewTicker(2 * time.Minute)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
s.mu.Lock()
|
||||
now := time.Now()
|
||||
for id, e := range s.data {
|
||||
if now.After(e.expiresAt) {
|
||||
delete(s.data, id)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 生成验证码,返回 id 与 PNG data URL
|
||||
func (s *CaptchaStore) Issue() (id, dataURL string, err error) {
|
||||
answer, err := randomCaptchaText(captchaLen)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
id = hex.EncodeToString(b)
|
||||
img, err := renderCaptchaPNG(answer)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.data[id] = captchaEntry{answer: strings.ToUpper(answer), expiresAt: time.Now().Add(captchaTTL)}
|
||||
s.mu.Unlock()
|
||||
dataURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
|
||||
return id, dataURL, nil
|
||||
}
|
||||
|
||||
// Verify 校验并立即作废(一次性)
|
||||
func (s *CaptchaStore) Verify(id, answer string) bool {
|
||||
id = strings.TrimSpace(id)
|
||||
answer = strings.ToUpper(strings.TrimSpace(answer))
|
||||
if id == "" || answer == "" {
|
||||
return false
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
e, ok := s.data[id]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
delete(s.data, id)
|
||||
if time.Now().After(e.expiresAt) {
|
||||
return false
|
||||
}
|
||||
return e.answer == answer
|
||||
}
|
||||
|
||||
func randomCaptchaText(n int) (string, error) {
|
||||
var b strings.Builder
|
||||
max := big.NewInt(int64(len(captchaCharset)))
|
||||
for i := 0; i < n; i++ {
|
||||
v, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.WriteByte(captchaCharset[v.Int64()])
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func renderCaptchaPNG(text string) ([]byte, error) {
|
||||
const w, h = 140, 44
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
bg := color.RGBA{R: 248, G: 250, B: 252, A: 255}
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
|
||||
|
||||
// 噪点
|
||||
for i := 0; i < 180; i++ {
|
||||
x, _ := rand.Int(rand.Reader, big.NewInt(w))
|
||||
y, _ := rand.Int(rand.Reader, big.NewInt(h))
|
||||
img.Set(int(x.Int64()), int(y.Int64()), color.RGBA{R: 180, G: 190, B: 200, A: 255})
|
||||
}
|
||||
// 干扰线
|
||||
for i := 0; i < 4; i++ {
|
||||
x0, _ := rand.Int(rand.Reader, big.NewInt(w))
|
||||
y0, _ := rand.Int(rand.Reader, big.NewInt(h))
|
||||
x1, _ := rand.Int(rand.Reader, big.NewInt(w))
|
||||
y1, _ := rand.Int(rand.Reader, big.NewInt(h))
|
||||
drawLine(img, int(x0.Int64()), int(y0.Int64()), int(x1.Int64()), int(y1.Int64()), color.RGBA{R: 160, G: 170, B: 185, A: 255})
|
||||
}
|
||||
|
||||
fg := color.RGBA{R: 30, G: 41, B: 59, A: 255}
|
||||
startX := 12
|
||||
for i, ch := range text {
|
||||
ox := startX + i*24
|
||||
oy := 10 + int(i%2)*2
|
||||
drawGlyph(img, ch, ox, oy, fg)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) {
|
||||
dx := abs(x1 - x0)
|
||||
dy := -abs(y1 - y0)
|
||||
sx, sy := 1, 1
|
||||
if x0 >= x1 {
|
||||
sx = -1
|
||||
}
|
||||
if y0 >= y1 {
|
||||
sy = -1
|
||||
}
|
||||
err := dx + dy
|
||||
for {
|
||||
img.Set(x0, y0, c)
|
||||
if x0 == x1 && y0 == y1 {
|
||||
break
|
||||
}
|
||||
e2 := 2 * err
|
||||
if e2 >= dy {
|
||||
err += dy
|
||||
x0 += sx
|
||||
}
|
||||
if e2 <= dx {
|
||||
err += dx
|
||||
y0 += sy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func abs(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 简易 5×7 点阵(仅验证码字符集)
|
||||
var glyphBits = map[rune][7]string{
|
||||
'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
|
||||
'B': {"11110", "10001", "10001", "11110", "10001", "10001", "11110"},
|
||||
'C': {"01111", "10000", "10000", "10000", "10000", "10000", "01111"},
|
||||
'D': {"11110", "10001", "10001", "10001", "10001", "10001", "11110"},
|
||||
'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
|
||||
'F': {"11111", "10000", "10000", "11110", "10000", "10000", "10000"},
|
||||
'G': {"01111", "10000", "10000", "10011", "10001", "10001", "01111"},
|
||||
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
|
||||
'J': {"00111", "00010", "00010", "00010", "00010", "10010", "01100"},
|
||||
'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
|
||||
'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
|
||||
'M': {"10001", "11011", "10101", "10001", "10001", "10001", "10001"},
|
||||
'N': {"10001", "11001", "10101", "10011", "10001", "10001", "10001"},
|
||||
'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
|
||||
'Q': {"01110", "10001", "10001", "10001", "10101", "10010", "01101"},
|
||||
'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
|
||||
'S': {"01111", "10000", "10000", "01110", "00001", "00001", "11110"},
|
||||
'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
|
||||
'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
|
||||
'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
|
||||
'W': {"10001", "10001", "10001", "10001", "10101", "10101", "01010"},
|
||||
'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
|
||||
'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
|
||||
'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
|
||||
'2': {"01110", "10001", "00001", "00010", "00100", "01000", "11111"},
|
||||
'3': {"11110", "00001", "00001", "01110", "00001", "00001", "11110"},
|
||||
'4': {"00010", "00110", "01010", "10010", "11111", "00010", "00010"},
|
||||
'5': {"11111", "10000", "11110", "00001", "00001", "10001", "01110"},
|
||||
'6': {"01110", "10000", "10000", "11110", "10001", "10001", "01110"},
|
||||
'7': {"11111", "00001", "00010", "00100", "01000", "01000", "01000"},
|
||||
'8': {"01110", "10001", "10001", "01110", "10001", "10001", "01110"},
|
||||
'9': {"01110", "10001", "10001", "01111", "00001", "00001", "01110"},
|
||||
}
|
||||
|
||||
func drawGlyph(img *image.RGBA, ch rune, ox, oy int, c color.Color) {
|
||||
bits, ok := glyphBits[ch]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
scale := 2
|
||||
for row, line := range bits {
|
||||
for col, cell := range line {
|
||||
if cell != '1' {
|
||||
continue
|
||||
}
|
||||
for dy := 0; dy < scale; dy++ {
|
||||
for dx := 0; dx < scale; dx++ {
|
||||
img.Set(ox+col*scale+dx, oy+row*scale+dy, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user