自定义设计
那封改变一切的邮件 💬
那天早上,我像往常一样打开邮箱,准备处理用户的反馈邮件。大多是些”链接打不开”、“访问速度慢”之类的问题,我已经习惯了。
但有一封邮件让我停下了手头的动作。
发件人:Nike Digital Team 主题:关于您的短链接服务
您好,
我们在社交媒体上看到很多用户在使用您的短链接服务,效果很好。
我们有一个商业需求:我们希望获得 short.url/nike 这样的品牌短链接。 我们的产品链接会定期更新,但短链接可以保持不变,这样在广告投放时更加稳定。
关于价格,我们可以接受按月或按年付费。请告知您的合作方式。
期待您的回复。
—— Nike Digital Team
我的心跳突然加速了。
付费? 这是我的短链接服务第一次有人主动提出要付费!
我立刻意识到:这不是一个简单的功能需求,这是一个商业机会。
自定义短链接——这三个字在我脑海中闪烁。用户想要自己定义的短链接后缀,而不是系统生成的随机字符。
这不仅仅是个技术问题,这是个产品设计问题。我需要设计一套完整的自定义别名系统。
定义规则:什么别名是合法的?📋
我开始思考第一个问题:什么样的自定义别名应该被允许?
如果允许用户输入任意字符,可能会出现安全问题、URL冲突问题、甚至是系统路径混乱的问题。
我拿出一张纸,开始画规则边界:
┌─────────────────────────────────────────────┐
│ 自定义别名验证规则 │
├─────────────────────────────────────────────┤
│ 1. 长度限制:3-30个字符 │
│ 2. 字符集:字母、数字、连字符、下划线 │
│ 3. 保留词:系统保留的敏感词汇 │
│ 4. 重复检查:不能与现有别名冲突 │
│ 5. 敏感词过滤:不当词汇拦截 │
└─────────────────────────────────────────────┘规则1:长度限制
为什么不能太短?
长度 < 3字符:
- 组合太少(26个字母 × 26个字母 = 676种)
- 很容易冲突
- 不够有意义
- 容易与系统路径混淆为什么不能太长?
长度 > 30字符:
- 失去短链接的意义
- 用户体验差
- 违背了"短"的初衷
- 可能超过某些系统的URL长度限制我写下了验证代码:
def validate_alias_length(alias: str) -> tuple[bool, str | None]:
"""验证别名长度
Args:
alias: 用户输入的自定义别名
Returns:
(是否有效, 错误信息)
"""
MIN_LENGTH = 3
MAX_LENGTH = 30
if len(alias) < MIN_LENGTH:
return False, f"别名太短,至少需要{MIN_LENGTH}个字符"
if len(alias) > MAX_LENGTH:
return False, f"别名太长,最多{MAX_LENGTH}个字符"
return True, None
# 测试用例
test_cases = [
("ab", False, "太短"),
("abc", True, "刚好满足最小长度"),
("a" * 30, True, "刚好满足最大长度"),
("a" * 31, False, "太长"),
]
for alias, expected, desc in test_cases:
valid, error = validate_alias_length(alias)
status = "✓" if valid else "✗"
print(f"{status} {alias}: {desc}")
if error:
print(f" 错误: {error}")规则2:字符集限制
允许哪些字符?
我查阅了RFC 3986(URL规范),发现URL路径段中安全使用的字符包括:
- 字母:a-z, A-Z
- 数字:0-9
- 连字符:-
- 下划线:_
- 点号:.(但我不想使用,因为看起来像文件扩展名)
不允许哪些字符?
× 空格:会被编码成 %20,破坏短链接的简洁性
× 中文/日文等非ASCII字符:需要编码,变长
× 特殊字符:/、?、&、#、@等都有URL特殊含义
× 控制字符:可能破坏系统
× 表情符号:编码后极长我写下了字符验证代码:
import re
class AliasCharacterValidator:
"""别名字符验证器"""
# 允许的字符模式
ALLOWED_PATTERN = re.compile(r'^[a-zA-Z0-9-_]+$')
# 不能以这些字符开头或结尾
INVALID_START_END = ['-', '_']
# 不能连续出现这些字符
INVALID_CONSECUTIVE = ['--', '__', '-_', '_-']
@classmethod
def validate(cls, alias: str) -> tuple[bool, list[str]]:
"""验证别名字符
Returns:
(是否有效, 错误列表)
"""
errors = []
# 检查1:只包含允许的字符
if not cls.ALLOWED_PATTERN.match(alias):
errors.append("别名只能包含字母、数字、连字符(-)和下划线(_)")
# 检查2:不能以连字符或下划线开头
if alias[0] in cls.INVALID_START_END:
errors.append("别名不能以连字符或下划线开头")
# 检查3:不能以连字符或下划线结尾
if alias[-1] in cls.INVALID_START_END:
errors.append("别名不能以连字符或下划线结尾")
# 检查4:不能连续使用连字符或下划线
for invalid_seq in cls.INVALID_CONSECUTIVE:
if invalid_seq in alias:
errors.append(f"别名不能包含 '{invalid_seq}' 这样的连续字符")
return len(errors) == 0, errors
# 测试用例
test_cases = [
("nike", True, "纯字母"),
("nike-2024", True, "字母+数字+连字符"),
("my_brand", True, "带下划线"),
("test-url_123", True, "混合使用"),
("-nike", False, "连字符开头"),
("nike-", False, "连字符结尾"),
("nike--shoes", False, "连续连字符"),
("nike url", False, "包含空格"),
("nike.shoes", False, "包含点号"),
("耐克", False, "包含中文"),
("nike/shoes", False, "包含斜杠"),
]
for alias, expected, desc in test_cases:
valid, errors = AliasCharacterValidator.validate(alias)
status = "✓" if valid else "✗"
print(f"{status} {alias}: {desc}")
if errors:
for error in errors:
print(f" 错误: {error}")规则3:保留词检查
这是最棘手的部分。我需要确保用户不能注册与系统路径冲突的别名。
我列出了第一版保留词清单:
# 系统保留词(第一版)
SYSTEM_RESERVED_WORDS = {
# 系统路径
'admin', 'api', 'www', 'mail', 'ftp',
# 认证相关
'login', 'logout', 'register', 'signup', 'signin', 'auth',
# 常见网页路径
'dashboard', 'settings', 'profile', 'account', 'help', 'about',
# 环境标识
'test', 'dev', 'staging', 'prod', 'production', 'development',
# 技术路径
'static', 'assets', 'public', 'private', 'cdn',
'robots.txt', 'sitemap.xml', 'favicon.ico',
# 常见域名前缀
'blog', 'shop', 'store', 'app', 'mobile', 'm',
# 未来可能的付费功能
'premium', 'pro', 'enterprise', 'business',
}
class ReservedWordChecker:
"""保留词检查器"""
def __init__(self):
self.system_words = SYSTEM_RESERVED_WORDS
# 未来可以扩展:从数据库加载动态保留词
self.dynamic_words = set()
def is_reserved(self, alias: str) -> tuple[bool, str | None]:
"""检查别名是否为保留词
Returns:
(是否保留, 原因)
"""
alias_lower = alias.lower()
# 检查精确匹配
if alias_lower in self.system_words:
return True, f"'{alias}' 是系统保留词"
if alias_lower in self.dynamic_words:
return True, f"'{alias}' 是保留词"
# 检查前缀匹配(如:admin-panel, api-v2)
for word in self.system_words:
if alias_lower.startswith(word + '-') or alias_lower.startswith(word + '_'):
return True, f"别名不能以保留词 '{word}' 开头"
return False, None
# 测试用例
checker = ReservedWordChecker()
test_cases = [
("admin", True, "系统保留词"),
("api", True, "系统保留词"),
("test", True, "系统保留词"),
("admin-panel", True, "以保留词开头"),
("api_v2", True, "以保留词开头"),
("nike", False, "正常别名"),
("my-brand", False, "正常别名"),
("custom-admin", False, "包含保留词但不以它开头"),
]
for alias, expected_reserved, desc in test_cases:
is_reserved, reason = checker.is_reserved(alias)
status = "✗ 保留" if is_reserved else "✓ 可用"
print(f"{status}: {alias} - {desc}")
if reason:
print(f" 原因: {reason}")规则4:重复检查
即使别名通过了所有验证规则,我还必须确保它没有被其他用户使用。
def check_alias_duplicate(alias: str) -> tuple[bool, str | None]:
"""检查别名是否已被使用
Returns:
(是否可用, 错误信息)
"""
# 查询数据库
existing = db.query(
"SELECT id, user_id, long_url FROM urls WHERE custom_alias = ?",
(alias,)
)
if existing:
return False, f"别名 '{alias}' 已被使用"
return True, None
# 优化:使用缓存减少数据库查询
def check_alias_duplicate_with_cache(alias: str) -> tuple[bool, str | None]:
"""带缓存的重复检查"""
# 1. 先查Redis缓存
cache_key = f"alias:exists:{alias}"
if redis.exists(cache_key):
return False, f"别名 '{alias}' 已被使用"
# 2. 缓存未命中,查数据库
available, error = check_alias_duplicate(alias)
# 3. 如果已被使用,写入缓存
if not available:
redis.setex(cache_key, 3600, '1') # 缓存1小时
return available, error规则5:敏感词过滤
最后一个安全网:防止用户注册不当的别名。
class ProfanityFilter:
"""敏感词过滤器"""
def __init__(self):
# 简化示例:实际应该使用专业的敏感词库
self.profanity_words = {
'fuck', 'shit', 'damn', 'bitch',
# ... 更多敏感词
}
def contains_profanity(self, alias: str) -> bool:
"""检查别名是否包含敏感词"""
alias_lower = alias.lower()
# 直接匹配
if alias_lower in self.profanity_words:
return True
# 包含匹配
for word in self.profanity_words:
if word in alias_lower:
return True
return False
# 测试
filter = ProfanityFilter()
print(filter.contains_profanity("test")) # False
print(filter.contains_profanity("badword-test")) # True (假设badword在列表中)完整的验证器 🔍
现在我把所有规则整合成一个完整的验证器:
from dataclasses import dataclass
from typing import Optional
@dataclass
class ValidationResult:
"""验证结果"""
is_valid: bool
errors: list[str]
@classmethod
def valid(cls):
return cls(is_valid=True, errors=[])
@classmethod
def invalid(cls, *errors):
return cls(is_valid=False, errors=list(errors))
class CustomAliasValidator:
"""自定义别名验证器"""
def __init__(self):
self.length_validator = validate_alias_length
self.character_validator = AliasCharacterValidator()
self.reserved_checker = ReservedWordChecker()
self.profanity_filter = ProfanityFilter()
def validate(self, alias: str) -> ValidationResult:
"""完整验证别名
Args:
alias: 用户输入的自定义别名
Returns:
ValidationResult
"""
# 规则1:长度检查
valid, error = self.length_validator(alias)
if not valid:
return ValidationResult.invalid(error)
# 规则2:字符检查
valid, errors = self.character_validator.validate(alias)
if not valid:
return ValidationResult.invalid(*errors)
# 规则3:保留词检查
is_reserved, reason = self.reserved_checker.is_reserved(alias)
if is_reserved:
return ValidationResult.invalid(reason)
# 规则4:敏感词检查
if self.profanity_filter.contains_profanity(alias):
return ValidationResult.invalid("别名包含不当词汇")
# 规则5:重复检查
available, error = check_alias_duplicate_with_cache(alias)
if not available:
return ValidationResult.invalid(error)
return ValidationResult.valid()
# 使用示例
validator = CustomAliasValidator()
test_aliases = [
"nike",
"ab",
"nike--shoes",
"admin",
"a" * 35,
"test-url",
]
for alias in test_aliases:
result = validator.validate(alias)
status = "✓" if result.is_valid else "✗"
print(f"{status} {alias}")
if not result.is_valid:
for error in result.errors:
print(f" 错误: {error}")数据库设计:自定义别名需要独立存储 💾
我意识到自定义别名和自动生成的短码是两个不同的体系:
自动生成的短码:
- 长度固定(如6字符)
- 字符集固定(Base62)
- 无特殊含义
- 冲突概率极低
自定义别名:
- 长度不固定(3-30字符)
- 字符集宽松但有限制
- 有商业价值
- 冲突概率较高这意味着我需要设计一个新的表结构,而不是简单地复用现有的urls表。
设计方案1:扩展现有表
-- 在现有 urls 表中添加字段
ALTER TABLE urls ADD COLUMN is_custom BOOLEAN DEFAULT FALSE;
ALTER TABLE urls ADD COLUMN custom_alias VARCHAR(30) UNIQUE;
-- 添加索引
CREATE INDEX idx_custom_alias ON urls(custom_alias);优点:
- 改动最小
- 查询简单(一张表)
缺点:
- 字段冗余(
short_code和custom_alias) - 不够清晰(自定义和自动生成的混在一起)
设计方案2:独立表(我选择这个)
-- 自定义别名表
CREATE TABLE custom_aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, -- 用户ID(支持登录后)
custom_alias VARCHAR(30) UNIQUE NOT NULL,
long_url VARCHAR(2048) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 索引
INDEX idx_user_id (user_id),
INDEX idx_custom_alias (custom_alias),
INDEX idx_created_at (created_at)
);
-- 保留词表
CREATE TABLE reserved_words (
id INTEGER PRIMARY KEY AUTOINCREMENT,
word VARCHAR(50) UNIQUE NOT NULL,
reason VARCHAR(200), -- 保留原因
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER, -- 添加人ID
INDEX idx_word (word)
);
-- 插入初始保留词
INSERT INTO reserved_words (word, reason) VALUES
('admin', '系统管理路径'),
('api', 'API接口路径'),
('www', '保留域名'),
('mail', '邮件服务'),
('ftp', '文件传输'),
('login', '登录页面'),
('logout', '登出页面'),
('register', '注册页面'),
('dashboard', '仪表盘'),
('settings', '设置页面'),
('test', '测试路径'),
('dev', '开发环境'),
('staging', '预发布环境'),
('prod', '生产环境');
-- 自定义别名历史表(审计用)
CREATE TABLE custom_alias_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
custom_alias VARCHAR(30) NOT NULL,
long_url VARCHAR(2048) NOT NULL,
action VARCHAR(20) NOT NULL, -- 'create', 'update', 'delete'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_custom_alias (custom_alias),
INDEX idx_created_at (created_at)
);优点:
- 职责清晰
- 易于扩展(比如未来要给自定义别名加更多属性)
- 便于审计和历史追踪
缺点:
- 查询时需要JOIN两张表(但这个场景很少)
我选择了方案2。
冲突处理:两个人都想要 ‘nike’ 怎么办?⚡
这是一个真实的问题:如果有两个商家都想注册 nike 这个别名,应该给谁?
我设计了一个”先到先得”的机制,配合事务保证原子性:
from flask import Flask, request, jsonify
import sqlite3
app = Flask(__name__)
@app.route('/api/custom-alias', methods=['POST'])
def create_custom_alias():
"""创建自定义别名
先到先得 + 事务保证原子性
"""
data = request.get_json()
custom_alias = data.get('alias', '').strip()
long_url = data.get('url')
# 基础验证
if not custom_alias:
return jsonify({'error': '别名不能为空'}), 400
if not long_url:
return jsonify({'error': 'URL不能为空'}), 400
# 完整验证
validator = CustomAliasValidator()
result = validator.validate(custom_alias)
if not result.is_valid:
return jsonify({
'error': '别名无效',
'details': result.errors
}), 400
# 使用事务创建别名
try:
db.begin_transaction()
# 再次检查是否已被占用(双重检查)
existing = db.query(
"SELECT id FROM custom_aliases WHERE custom_alias = ?",
(custom_alias,)
)
if existing:
db.rollback()
return jsonify({
'error': '别名已被占用',
'message': f"'{custom_alias}' 已被其他用户使用"
}), 409
# 创建别名
db.execute("""
INSERT INTO custom_aliases (custom_alias, long_url, user_id)
VALUES (?, ?, ?)
""", (custom_alias, long_url, 0)) # user_id=0 表示未登录
# 记录历史
db.execute("""
INSERT INTO custom_alias_history
(user_id, custom_alias, long_url, action)
VALUES (?, ?, ?, 'create')
""", (0, custom_alias, long_url))
db.commit()
return jsonify({
'success': True,
'short_url': f'https://short.url/{custom_alias}',
'alias': custom_alias,
'long_url': long_url
}), 201
except Exception as e:
db.rollback()
# 处理唯一约束冲突
if 'UNIQUE constraint failed' in str(e):
return jsonify({
'error': '别名已被占用',
'message': f"'{custom_alias}' 刚刚被其他用户抢注"
}), 409
# 其他错误
return jsonify({
'error': '服务器错误',
'message': str(e)
}), 500并发安全
在高并发场景下,两个用户可能同时注册同一个别名。我需要确保只有一个人能成功。
import threading
# 使用数据库锁机制
def create_custom_alias_with_lock(alias: str, url: str) -> dict:
"""带锁的别名创建
使用数据库的行锁保证并发安全
"""
try:
db.begin_transaction()
# 1. 先尝试锁定该别名(如果存在)
db.execute(
"SELECT id FROM custom_aliases WHERE custom_alias = ? FOR UPDATE",
(alias,)
)
# 2. 再次检查(锁保护下)
existing = db.query(
"SELECT id FROM custom_aliases WHERE custom_alias = ?",
(alias,)
)
if existing:
db.rollback()
return {'success': False, 'error': '别名已被占用'}
# 3. 创建别名(现在是安全的)
db.execute("""
INSERT INTO custom_aliases (custom_alias, long_url)
VALUES (?, ?)
""", (alias, url))
db.commit()
return {'success': True, 'short_url': f'https://short.url/{alias}'}
except Exception as e:
db.rollback()
return {'success': False, 'error': str(e)}用户体验:实时可用性检查 🎨
我花了两天时间优化一个细节:当用户输入自定义别名时,实时显示它是否可用。
这需要前端的防抖(debounce)和后端的高效验证配合。
前端实现
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>创建自定义短链接</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
margin-bottom: 40px;
color: #333;
}
.form-group {
margin-bottom: 30px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.input-group {
display: flex;
align-items: center;
}
.prefix {
background: #f5f5f5;
padding: 12px 16px;
border: 2px solid #ddd;
border-right: none;
border-radius: 8px 0 0 8px;
color: #666;
font-family: 'Courier New', monospace;
font-size: 14px;
}
input[type="text"],
input[type="url"] {
flex: 1;
padding: 12px 16px;
border: 2px solid #ddd;
border-radius: 0 8px 8px 0;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
.status {
margin-top: 12px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.status.available {
color: #10b981;
}
.status.unavailable {
color: #ef4444;
}
.status.checking {
color: #6b7280;
}
.errors {
margin-top: 8px;
font-size: 13px;
color: #ef4444;
}
.error-item {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>✨ 创建自定义短链接</h1>
<form id="shortenForm">
<div class="form-group">
<label for="longUrl">长链接</label>
<input type="url" id="longUrl" placeholder="https://example.com/very/long/url" required>
</div>
<div class="form-group">
<label for="customAlias">自定义别名(可选)</label>
<div class="input-group">
<span class="prefix">short.url/</span>
<input type="text" id="customAlias" placeholder="留空自动生成">
</div>
<div id="aliasStatus" class="status"></div>
<div id="aliasErrors" class="errors"></div>
</div>
<button type="submit" id="submitBtn">创建短链接</button>
</form>
</div>
<script>
const aliasInput = document.getElementById('customAlias');
const statusDiv = document.getElementById('aliasStatus');
const errorsDiv = document.getElementById('aliasErrors');
const submitBtn = document.getElementById('submitBtn');
let checkTimeout = null;
let isAliasAvailable = false;
// 防抖:用户停止输入500ms后才检查
aliasInput.addEventListener('input', function() {
clearTimeout(checkTimeout);
const alias = this.value.trim();
// 清空状态
if (!alias) {
statusDiv.innerHTML = '';
errorsDiv.innerHTML = '';
isAliasAvailable = true; // 空别名表示使用自动生成
submitBtn.disabled = false;
return;
}
// 显示检查中状态
statusDiv.className = 'status checking';
statusDiv.innerHTML = '<div class="spinner"></div> 检查中...';
errorsDiv.innerHTML = '';
submitBtn.disabled = true;
// 500ms后执行检查
checkTimeout = setTimeout(() => {
checkAliasAvailability(alias);
}, 500);
});
async function checkAliasAvailability(alias) {
try {
const response = await fetch(`/api/check-alias?alias=${encodeURIComponent(alias)}`);
const data = await response.json();
if (data.available) {
// 可用
statusDiv.className = 'status available';
statusDiv.innerHTML = '✓ 别名可用';
errorsDiv.innerHTML = '';
isAliasAvailable = true;
submitBtn.disabled = false;
} else {
// 不可用
statusDiv.className = 'status unavailable';
statusDiv.innerHTML = '✗ 别名不可用';
// 显示具体错误
if (data.errors && data.errors.length > 0) {
errorsDiv.innerHTML = data.errors
.map(err => `<div class="error-item">⚠️ ${err}</div>`)
.join('');
} else {
errorsDiv.innerHTML = '';
}
isAliasAvailable = false;
submitBtn.disabled = true;
}
} catch (error) {
statusDiv.className = 'status unavailable';
statusDiv.innerHTML = '✗ 检查失败,请稍后重试';
errorsDiv.innerHTML = '';
isAliasAvailable = false;
submitBtn.disabled = true;
}
}
// 表单提交
document.getElementById('shortenForm').addEventListener('submit', async function(e) {
e.preventDefault();
const longUrl = document.getElementById('longUrl').value;
const customAlias = document.getElementById('customAlias').value.trim();
submitBtn.disabled = true;
submitBtn.textContent = '创建中...';
try {
const response = await fetch('/api/custom-alias', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: longUrl,
alias: customAlias || null
})
});
const data = await response.json();
if (data.success) {
alert(`🎉 短链接创建成功!\n\n${data.short_url}\n\n指向:${data.long_url}`);
// 重置表单
document.getElementById('shortenForm').reset();
statusDiv.innerHTML = '';
errorsDiv.innerHTML = '';
} else {
alert(`❌ 创建失败:${data.error || data.message}`);
}
} catch (error) {
alert(`❌ 网络错误,请稍后重试`);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '创建短链接';
}
});
</script>
</body>
</html>后端API
@app.route('/api/check-alias', methods=['GET'])
def check_alias_available():
"""检查别名是否可用(用于前端实时验证)"""
alias = request.args.get('alias', '').strip()
if not alias:
return jsonify({'available': True, 'errors': []})
# 使用完整验证器
validator = CustomAliasValidator()
result = validator.validate(alias)
return jsonify({
'available': result.is_valid,
'errors': result.errors
})这个交互细节花了两天时间,但效果很好——用户可以立即知道他们想要的别名是否可用,避免了填写完表单后才发现别名被占用的挫败感。
上线第一天:有人注册了50个品牌名 🚨
自定义别名功能上线了。
我满怀期待地打开后台监控,看看有没有人会使用这个新功能。
第一天结束时,我看到了一些数据:
自定义别名注册数量:127个
最热门的别名:
- nike (1次)
- adidas (1次)
- apple (1次)
- samsung (1次)
- test (3次)
- mylink (5次)
- brand123 (2次)看起来还好。
但当我深入查看时,发现了一个问题:有一个用户注册了50个知名品牌的别名。
用户 ID: 18472
注册的别名:
- nike
- adidas
- puma
- reebok
- newbalance
- converse
- asics
- saucony
- ...
(共50个运动品牌名)更糟糕的是,这些别名指向的URL都是一些不知名的电商网站,很明显是抢注行为。
我意识到:这需要防抢注机制。
问题:
- 恶意用户可以批量注册大量品牌名
- 真正的品牌所有者反而无法使用自己的品牌名
- 这可能导致法律纠纷
- 破坏了平台的公信力
我需要解决:
- 限制每个用户的注册数量
- 实施品牌名验证机制
- 建立申诉流程
- 考虑引入付费门槛
这些,将是下一节的内容。
本节小结 📝
完整的别名验证流程
用户输入别名
↓
【规则1】长度检查(3-30字符)
↓
【规则2】字符检查(字母、数字、-、_)
↓
【规则3】保留词检查(系统路径)
↓
【规则4】敏感词检查(不当词汇)
↓
【规则5】重复检查(数据库查询)
↓
【规则6】冲突处理(事务保证)
↓
创建成功 ✅核心代码组件
| 组件 | 职责 |
|---|---|
CustomAliasValidator | 完整的别名验证器 |
AliasCharacterValidator | 字符规则验证 |
ReservedWordChecker | 保留词检查 |
ProfanityFilter | 敏感词过滤 |
custom_aliases 表 | 存储自定义别名 |
reserved_words 表 | 存储保留词 |
数据库表结构
-- 主表
custom_aliases (
id, user_id, custom_alias, long_url,
created_at, updated_at
)
-- 保留词表
reserved_words (
id, word, reason, created_at, created_by
)
-- 历史表
custom_alias_history (
id, user_id, custom_alias, long_url,
action, created_at
)想一想
问题1:品牌验证
Nike团队真的注册了nike这个别名,还是被抢注者抢占了?
提示
考虑:
- 如何验证用户真的是品牌所有者?
- 是否需要上传商标证书?
- 是否需要企业认证?
- 未认证用户是否允许注册品牌名?
我们将在下一节讨论这个问题。
问题2:批量注册防护
如何防止一个用户在短时间内注册大量别名?
提示
考虑:
- 限制每个用户的注册数量(如10个)
- 限制注册频率(如每小时最多5个)
- 短别名(3-4字符)需要额外审核
- 引入付费门槛(减少恶意注册)
问题3:历史遗留
如果某个别名被抢注了,真正的品牌所有者怎么办?
提示
考虑:
- 是否可以强制转移别名?
- 是否需要建立申诉流程?
- 如何证明所有权?
- 是否需要仲裁机制?
(下一节:抢注防护与品牌验证)