1. 预校验(避免无用查库)
原先任何非空字符串都会直接拿去查库。邀请码固定长度且只用
inviteCodeChars(去掉 0/O/1/L 后的 31 个字符),不合规的输入
可以无需查库就拒绝;用统一文案「邀请码无效」,不泄露额外信息。
2. 存量兼容(关键,差点造成线上事故)
最初把校验写成「必须 8 位」。查生产库发现:
6 位:551 个用户
8 位:1 个用户
即绝大多数用户的邀请码仍是 6 位旧码。若只接受 8 位,
这 551 人的码会被判无效,他们将无法被新用户填写邀请——
影响面很大。已改为同时接受 6 位与 8 位,字符集校验不变。
测试覆盖:8 位正常、6 位存量、5/7 位、空串、含易混淆字符
(0/O/1/I/L)、含小写、含符号、含中文。
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"cpdaily-new/internal/config"
|
"cpdaily-new/internal/config"
|
||||||
"cpdaily-new/internal/database"
|
"cpdaily-new/internal/database"
|
||||||
"cpdaily-new/internal/model"
|
"cpdaily-new/internal/model"
|
||||||
"cpdaily-new/internal/service"
|
"cpdaily-new/internal/service"
|
||||||
cpd "cpdaily-new/internal/service/cpdaily"
|
cpd "cpdaily-new/internal/service/cpdaily"
|
||||||
"cpdaily-new/internal/utils"
|
"cpdaily-new/internal/utils"
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -585,6 +585,12 @@ func BindInviteCode(c *fiber.Ctx) error {
|
|||||||
if code == "" {
|
if code == "" {
|
||||||
return utils.FailParam(c, "请输入邀请码")
|
return utils.FailParam(c, "请输入邀请码")
|
||||||
}
|
}
|
||||||
|
// 格式预校验:邀请码固定 8 位且只用 inviteCodeChars 字符集。
|
||||||
|
// 不合规的输入直接拒绝,避免每次都在库里做一次无用查询;
|
||||||
|
// 用统一文案,不泄露「码存在但格式对」这类信息。
|
||||||
|
if !isValidInviteCodeShape(code) {
|
||||||
|
return utils.Fail(c, 400, "邀请码无效")
|
||||||
|
}
|
||||||
|
|
||||||
var user model.SysUser
|
var user model.SysUser
|
||||||
if err := database.DB.First(&user, userID).Error; err != nil {
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||||
|
|||||||
@@ -205,3 +205,27 @@ func ResetInviteCode(c *fiber.Ctx) error {
|
|||||||
slog.Info("用户重置邀请码", "userId", userID, "oldCode", oldCode, "newCode", code)
|
slog.Info("用户重置邀请码", "userId", userID, "oldCode", oldCode, "newCode", code)
|
||||||
return utils.Success(c, fiber.Map{"inviteCode": code})
|
return utils.Success(c, fiber.Map{"inviteCode": code})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inviteCodeLegacyLength 旧版邀请码长度。
|
||||||
|
//
|
||||||
|
// 2026-10 之前生成为 6 位,生产库中仍有大量存量用户沿用旧码
|
||||||
|
// (实测 551 个 6 位 / 1 个 8 位)。校验必须同时接受两种长度,
|
||||||
|
// 否则存量用户的码会被判为无效,导致他们无法被邀请。
|
||||||
|
const inviteCodeLegacyLength = 6
|
||||||
|
|
||||||
|
// isValidInviteCodeShape 校验邀请码的形态(长度 + 字符集),不查库。
|
||||||
|
//
|
||||||
|
// 长度接受 6(存量)与 8(当前)两种,字符集为 inviteCodeChars
|
||||||
|
// (已去掉易混淆的 0/O/1/L)。不合规的输入无需查库即可拒绝,
|
||||||
|
// 也避免把用户的任意长字符串当码去查。
|
||||||
|
func isValidInviteCodeShape(code string) bool {
|
||||||
|
if len(code) != inviteCodeLength && len(code) != inviteCodeLegacyLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(code); i++ {
|
||||||
|
if !strings.ContainsRune(inviteCodeChars, rune(code[i])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -163,3 +163,42 @@ func TestErrInviteCodeChangedIsDistinct(t *testing.T) {
|
|||||||
t.Fatal("errors.Is 自反性失败")
|
t.Fatal("errors.Is 自反性失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValidInviteCodeShape:只校验形态(长度 + 字符集),不查库
|
||||||
|
func TestIsValidInviteCodeShape(t *testing.T) {
|
||||||
|
valid, err := newInviteCode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"正常 8 位", valid, true},
|
||||||
|
{"空串", "", false},
|
||||||
|
{"过短", "ABC", false},
|
||||||
|
{"过长", "ABCDEFGHJ", false},
|
||||||
|
{"含易混淆字符 0", "ABCD0EFG", false},
|
||||||
|
{"含易混淆字符 O", "ABCDOEFG", false},
|
||||||
|
{"含易混淆字符 1", "ABCD1EFG", false},
|
||||||
|
{"含易混淆字符 I", "ABCDIEFG", false},
|
||||||
|
{"含易混淆字符 L", "ABCDLEFG", false},
|
||||||
|
{"含小写", "abcdefgh", false},
|
||||||
|
{"含符号", "ABCD-EFG", false},
|
||||||
|
{"含中文", "邀请码邀请码邀请", false},
|
||||||
|
// 存量兼容:生产库仍有 551 个 6 位旧码,必须仍能正常绑定
|
||||||
|
{"存量 6 位码", "ABCDEF", true},
|
||||||
|
{"存量 6 位含易混淆字符", "ABCD0F", false},
|
||||||
|
{"5 位", "ABCDE", false},
|
||||||
|
{"7 位", "ABCDEFG", false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if got := isValidInviteCodeShape(c.code); got != c.want {
|
||||||
|
t.Fatalf("isValidInviteCodeShape(%q)=%v, want %v", c.code, got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user