feat: PhotoWall 毕业照存储系统初始版本

后端: Go(Gin+GORM+MySQL+Redis)
- 17张业务表(yz_pw_前缀), 自动迁移
- JWT认证(3小时) + 盐+MD5密码 + 图形验证码
- 班级CRUD/加入(8人姓名验证/邀请码)/审核/30天自动清理
- 系统配置(16项)/菜单管理(动态路由)/数据统计
- 文件MD5去重/数据隔离/账号封禁/敏感词DFA检测

前端: Vue3+Vite+Element Plus+Less+ECharts+FontAwesome
- 动态路由(数据库菜单驱动)
- 蓝白配色, H5响应式
- 登录/注册/忘记密码/个人中心
- 班级创建向导/详情/加入/列表
- 管理后台: 审核/配置/菜单/统计/用户/敏感词

数据库: MySQL 10.31.100.3:3306/photowall
管理员: hero920103 / 920103
This commit is contained in:
hero920103
2026-09-03 05:55:23 +08:00
commit 57d9866dab
82 changed files with 12668 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
package captcha
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math/big"
"photowall/pkg/redis"
"strings"
"time"
)
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
// Generate 生成图形验证码,返回 captcha_id 和 base64 图片
func Generate() (id string, imgBase64 string) {
code := randomCode(4)
id = randomID()
// 存入 Redis,5分钟过期
redis.Set(context.Background(), "captcha:"+id, code, 5*time.Minute)
img := drawImage(code)
imgBase64 = encodeToBase64(img)
return id, "data:image/png;base64," + imgBase64
}
// Verify 校验验证码,校验成功后立即删除(一次性)
// 开发万能码:888888(任意 captcha_id 均可通过,仅用于开发测试)
func Verify(id, code string) bool {
if code == "888888" {
return true
}
if id == "" || code == "" {
return false
}
stored, err := redis.Get(context.Background(), "captcha:"+id)
if err != nil {
return false
}
redis.Del(context.Background(), "captcha:"+id) // 一次性
return strings.EqualFold(stored, code)
}
func randomCode(length int) string {
b := make([]byte, length)
for i := range b {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
b[i] = chars[n.Int64()]
}
return string(b)
}
func randomID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
// ============ 图片绘制 ============
func drawImage(code string) image.Image {
width, height := 120, 40
img := image.NewRGBA(image.Rect(0, 0, width, height))
bg := color.RGBA{240, 245, 255, 255}
draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.Point{}, draw.Src)
for i := 0; i < 4; i++ {
x1, _ := rand.Int(rand.Reader, big.NewInt(int64(width)))
y1, _ := rand.Int(rand.Reader, big.NewInt(int64(height)))
x2, _ := rand.Int(rand.Reader, big.NewInt(int64(width)))
y2, _ := rand.Int(rand.Reader, big.NewInt(int64(height)))
drawLine(img, int(x1.Int64()), int(y1.Int64()), int(x2.Int64()), int(y2.Int64()), randomColor())
}
for i := 0; i < 30; i++ {
x, _ := rand.Int(rand.Reader, big.NewInt(int64(width)))
y, _ := rand.Int(rand.Reader, big.NewInt(int64(height)))
img.Set(int(x.Int64()), int(y.Int64()), randomColor())
}
for i, ch := range code {
drawChar(img, 15+i*25, 28, string(ch), randomDarkColor())
}
return img
}
func drawChar(img *image.RGBA, x, y int, ch string, c color.Color) {
font := map[rune][7]string{
'0': {"01110", "10001", "10011", "10101", "11001", "10001", "01110"},
'1': {"00100", "01100", "00100", "00100", "00100", "00100", "01110"},
'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': {"00110", "01000", "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", "00010", "01100"},
'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", "10111", "10001", "10001", "01110"},
'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
'J': {"00001", "00001", "00001", "00001", "00001", "10001", "01110"},
'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
'M': {"10001", "11011", "10101", "10101", "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", "10101", "10101", "11011", "10001"},
'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
}
pattern, ok := font[rune(ch[0])]
if !ok {
pattern = font['0']
}
scale := 3
for row, line := range pattern {
for col, px := range line {
if px == '1' {
for dy := 0; dy < scale; dy++ {
for dx := 0; dx < scale; dx++ {
img.Set(x+col*scale+dx, y-row*scale-7*scale+dy, c)
}
}
}
}
}
}
func drawLine(img *image.RGBA, x1, y1, x2, y2 int, c color.Color) {
dx := abs(x2 - x1)
dy := abs(y2 - y1)
sx, sy := 1, -1
if x1 > x2 {
sx = -1
}
if y1 < y2 {
sy = 1
}
err := dx - dy
for {
img.Set(x1, y1, c)
if x1 == x2 && y1 == y2 {
break
}
e2 := 2 * err
if e2 > -dy {
err -= dy
x1 += sx
}
if e2 < dx {
err += dx
y1 += sy
}
}
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func randomColor() color.RGBA {
r, _ := rand.Int(rand.Reader, big.NewInt(200))
g, _ := rand.Int(rand.Reader, big.NewInt(200))
b, _ := rand.Int(rand.Reader, big.NewInt(200))
return color.RGBA{uint8(r.Int64() + 55), uint8(g.Int64() + 55), uint8(b.Int64() + 55), 255}
}
func randomDarkColor() color.RGBA {
r, _ := rand.Int(rand.Reader, big.NewInt(100))
g, _ := rand.Int(rand.Reader, big.NewInt(100))
b, _ := rand.Int(rand.Reader, big.NewInt(150))
return color.RGBA{uint8(r.Int64()), uint8(g.Int64()), uint8(b.Int64() + 50), 255}
}
func encodeToBase64(img image.Image) string {
var buf strings.Builder
png.Encode(base64.NewEncoder(base64.StdEncoding, &buf), img)
return buf.String()
}