Penny Lens 短链接服务详细文档
2026年6月24日 · 11 分钟阅读 · ixNieStudio
详细介绍了 Penny Lens 短链接服务的设计理念、技术实现、功能特性和使用指南,包含短链接生成、管理、统计等核心功能。
Penny Lens 短链接服务详细文档
🎯 服务概述
短链接服务是 Penny Lens Serverless 项目的核心功能之一,提供完整的 URL 缩短、管理和统计服务。该服务支持创建短链接、访问统计、链接管理等功能,为用户提供便捷的链接分享和管理体验。
🏗️ 技术架构
核心组件
src/├── controllers/│ └── ShortUrlController.ts # 短链接控制器├── services/│ └── shortUrlService.ts # 短链接服务├── types/│ └── shortUrlModel.ts # 短链接类型定义├── utils/│ └── shortUrlUtils.ts # 短链接工具函数└── tests/ └── shortUrl-update-test.json # 短链接测试用例数据模型
interface ShortUrl { _id: string; // 短链接ID userId: string; // 用户ID code: string; // 短码(唯一标识) originalUrl: string; // 原始URL title?: string; // 标题 description?: string; // 描述 isPublic: boolean; // 是否公开 accessCount: number; // 访问次数 createdAt: number; // 创建时间 updatedAt: number; // 更新时间}数据库索引
// 短链接相关索引db.shortUrls.createIndex({ "code": 1 }, { unique: true }) // 短码唯一索引db.shortUrls.createIndex({ "userId": 1 }) // 用户ID索引🔧 核心功能
1. 创建短链接
功能描述: 将长URL转换为短链接
实现流程:
- 验证原始URL格式
- 生成唯一短码
- 创建短链接记录
- 返回短链接信息
代码示例:
// 创建短链接请求interface CreateShortUrlRequest { originalUrl: string; // 原始URL title?: string; // 标题 description?: string; // 描述 isPublic?: boolean; // 是否公开 expireAt?: number; // 过期时间}
// 创建短链接实现async function createShortUrl(data: CreateShortUrlRequest, userId: string): Promise<ShortUrl> { // 验证URL格式 const validation = validateUrl(data.originalUrl); if (!validation.isValid) { throw new Error(validation.errors.join(', ')); }
// 生成唯一短码 const code = await generateUniqueCode();
// 创建短链接记录 const shortUrl = await shortUrlService.create({ userId, code, originalUrl: data.originalUrl, title: data.title, description: data.description, isPublic: data.isPublic || false, accessCount: 0, createdAt: Date.now(), updatedAt: Date.now() });
return shortUrl;}
// URL验证function validateUrl(url: string): ValidationResult { const errors: string[] = [];
try { new URL(url); } catch { errors.push('URL格式不正确'); }
// 检查协议 if (!url.startsWith('http://') && !url.startsWith('https://')) { errors.push('URL必须以http://或https://开头'); }
// 检查长度 if (url.length > 2048) { errors.push('URL长度不能超过2048个字符'); }
return { isValid: errors.length === 0, errors };}
// 生成唯一短码async function generateUniqueCode(): Promise<string> { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const codeLength = 6; // 6位短码
let attempts = 0; const maxAttempts = 10;
while (attempts < maxAttempts) { let code = ''; for (let i = 0; i < codeLength; i++) { code += characters.charAt(Math.floor(Math.random() * characters.length)); }
// 检查短码是否已存在 const existing = await shortUrlService.findByCode(code); if (!existing) { return code; }
attempts++; }
throw new Error('无法生成唯一短码,请重试');}2. 短链接查询
功能描述: 根据短码查询原始URL并记录访问
实现流程:
- 根据短码查找短链接
- 检查链接是否有效
- 增加访问计数
- 返回原始URL
代码示例:
// 查询短链接async function getShortUrl(code: string): Promise<string> { const shortUrl = await shortUrlService.findByCode(code);
if (!shortUrl) { throw new Error('短链接不存在'); }
// 检查是否过期 if (shortUrl.expireAt && Date.now() > shortUrl.expireAt) { throw new Error('短链接已过期'); }
// 增加访问计数 await shortUrlService.incrementAccessCount(shortUrl._id);
return shortUrl.originalUrl;}
// 公开查询短链接(无需认证)async function getPublicShortUrl(code: string): Promise<ShortUrl> { const shortUrl = await shortUrlService.findByCode(code);
if (!shortUrl) { throw new Error('短链接不存在'); }
if (!shortUrl.isPublic) { throw new Error('短链接不公开'); }
return shortUrl;}3. 短链接管理
功能描述: 管理用户的短链接列表
代码示例:
// 查询用户短链接列表async function getUserShortUrls(userId: string, params: QueryParams): Promise<Page<ShortUrl>> { const { page = 1, pageSize = 20, keyword, isPublic } = params;
const query: any = { userId };
// 添加搜索条件 if (keyword) { query.$or = [ { title: { $regex: keyword, $options: 'i' } }, { description: { $regex: keyword, $options: 'i' } }, { originalUrl: { $regex: keyword, $options: 'i' } } ]; }
if (isPublic !== undefined) { query.isPublic = isPublic; }
const result = await shortUrlService.query(query, { page, pageSize, sort: { createdAt: -1 } });
return result;}
// 更新短链接async function updateShortUrl(id: string, data: UpdateShortUrlRequest, userId: string): Promise<ShortUrl> { const shortUrl = await shortUrlService.findById(id);
if (!shortUrl) { throw new Error('短链接不存在'); }
if (shortUrl.userId !== userId) { throw new Error('无权限修改此短链接'); }
const updatedShortUrl = await shortUrlService.update(id, { ...data, updatedAt: Date.now() });
return updatedShortUrl;}
// 删除短链接async function deleteShortUrl(id: string, userId: string): Promise<void> { const shortUrl = await shortUrlService.findById(id);
if (!shortUrl) { throw new Error('短链接不存在'); }
if (shortUrl.userId !== userId) { throw new Error('无权限删除此短链接'); }
await shortUrlService.delete(id);}4. 访问统计
功能描述: 提供短链接的访问统计和分析
代码示例:
// 获取访问统计async function getAccessStats(shortUrlId: string, userId: string): Promise<AccessStats> { const shortUrl = await shortUrlService.findById(shortUrlId);
if (!shortUrl) { throw new Error('短链接不存在'); }
if (shortUrl.userId !== userId) { throw new Error('无权限查看此统计'); }
// 获取访问记录(如果有详细记录) const accessRecords = await accessLogService.getByShortUrlId(shortUrlId);
// 计算统计数据 const stats = calculateAccessStats(accessRecords);
return { totalAccess: shortUrl.accessCount, uniqueAccess: stats.uniqueAccess, dailyAccess: stats.dailyAccess, referrers: stats.referrers, userAgents: stats.userAgents, countries: stats.countries };}
// 计算访问统计function calculateAccessStats(accessRecords: AccessRecord[]): AccessStatsData { const uniqueAccess = new Set(accessRecords.map(r => r.ipAddress)).size;
// 按日期统计 const dailyAccess = accessRecords.reduce((acc, record) => { const date = new Date(record.createdAt).toDateString(); acc[date] = (acc[date] || 0) + 1; return acc; }, {} as Record<string, number>);
// 统计来源 const referrers = accessRecords.reduce((acc, record) => { const referrer = record.referrer || 'direct'; acc[referrer] = (acc[referrer] || 0) + 1; return acc; }, {} as Record<string, number>);
// 统计用户代理 const userAgents = accessRecords.reduce((acc, record) => { const ua = record.userAgent || 'unknown'; const browser = extractBrowser(ua); acc[browser] = (acc[browser] || 0) + 1; return acc; }, {} as Record<string, number>);
return { uniqueAccess, dailyAccess, referrers, userAgents, countries: {} // 需要IP地理位置服务 };}🔌 API 接口
1. 创建短链接
请求参数:
{ "action": "shortUrl.create", "params": { "originalUrl": "https://example.com/very-long-url", "title": "示例链接", "description": "这是一个示例链接", "isPublic": true, "expireAt": 1673881678901 }}响应示例:
{ "code": 200, "message": "创建成功", "data": { "_id": "61c0c50b6d1b2c001fd2a345", "userId": "user123", "code": "abc123", "originalUrl": "https://example.com/very-long-url", "title": "示例链接", "description": "这是一个示例链接", "isPublic": true, "accessCount": 0, "createdAt": 1642345678901, "updatedAt": 1642345678901 }}2. 查询短链接
请求参数:
{ "action": "shortUrl.query", "params": { "code": "abc123" }}响应示例:
{ "code": 200, "message": "查询成功", "data": { "_id": "61c0c50b6d1b2c001fd2a345", "code": "abc123", "originalUrl": "https://example.com/very-long-url", "title": "示例链接", "description": "这是一个示例链接", "isPublic": true, "accessCount": 15, "createdAt": 1642345678901, "updatedAt": 1642345678901 }}3. 获取用户短链接列表
请求参数:
{ "action": "shortUrl.getUserUrls", "params": { "page": 1, "pageSize": 20, "keyword": "示例", "isPublic": true }}响应示例:
{ "code": 200, "message": "查询成功", "data": { "list": [ { "_id": "61c0c50b6d1b2c001fd2a345", "code": "abc123", "originalUrl": "https://example.com/very-long-url", "title": "示例链接", "description": "这是一个示例链接", "isPublic": true, "accessCount": 15, "createdAt": 1642345678901, "updatedAt": 1642345678901 } ], "total": 1, "page": 1, "pageSize": 20 }}4. 更新短链接
请求参数:
{ "action": "shortUrl.update", "params": { "id": "61c0c50b6d1b2c001fd2a345", "title": "更新后的标题", "description": "更新后的描述", "isPublic": false }}5. 删除短链接
请求参数:
{ "action": "shortUrl.delete", "params": { "id": "61c0c50b6d1b2c001fd2a345" }}6. 获取访问统计
请求参数:
{ "action": "shortUrl.getStats", "params": { "id": "61c0c50b6d1b2c001fd2a345" }}响应示例:
{ "code": 200, "message": "查询成功", "data": { "totalAccess": 150, "uniqueAccess": 120, "dailyAccess": { "2024-01-01": 10, "2024-01-02": 15, "2024-01-03": 20 }, "referrers": { "direct": 50, "google.com": 30, "facebook.com": 20 }, "userAgents": { "Chrome": 80, "Safari": 40, "Firefox": 30 } }}🛠️ 服务实现
ShortUrlService
功能描述: 短链接服务的核心实现
代码示例:
export class ShortUrlService extends BaseService { private collection = 'shortUrls';
// 创建短链接 async create(data: CreateShortUrlData): Promise<ShortUrl> { const result = await this.db.collection(this.collection).add(data); return { ...data, _id: result.id }; }
// 根据短码查找 async findByCode(code: string): Promise<ShortUrl | null> { const result = await this.db.collection(this.collection) .where({ code }) .get();
if (result.data.length === 0) { return null; }
return result.data[0] as ShortUrl; }
// 根据ID查找 async findById(id: string): Promise<ShortUrl | null> { const result = await this.db.collection(this.collection) .doc(id) .get();
if (!result.data) { return null; }
return result.data as ShortUrl; }
// 查询用户短链接 async queryByUser(userId: string, options: QueryOptions): Promise<Page<ShortUrl>> { const { page = 1, pageSize = 20, keyword, isPublic, sort = { createdAt: -1 } } = options;
let query = this.db.collection(this.collection).where({ userId });
// 添加搜索条件 if (keyword) { query = query.where({ $or: [ { title: { $regex: keyword, $options: 'i' } }, { description: { $regex: keyword, $options: 'i' } }, { originalUrl: { $regex: keyword, $options: 'i' } } ] }); }
if (isPublic !== undefined) { query = query.where({ isPublic }); }
// 分页查询 const result = await query .orderBy('createdAt', 'desc') .skip((page - 1) * pageSize) .limit(pageSize) .get();
// 获取总数 const countResult = await query.count();
return { list: result.data as ShortUrl[], total: countResult.total, page, pageSize }; }
// 更新短链接 async update(id: string, data: Partial<ShortUrl>): Promise<ShortUrl> { await this.db.collection(this.collection) .doc(id) .update(data);
return this.findById(id) as Promise<ShortUrl>; }
// 删除短链接 async delete(id: string): Promise<void> { await this.db.collection(this.collection) .doc(id) .remove(); }
// 增加访问计数 async incrementAccessCount(id: string): Promise<void> { await this.db.collection(this.collection) .doc(id) .update({ accessCount: this.db.command.inc(1), updatedAt: Date.now() }); }
// 批量删除 async batchDelete(ids: string[]): Promise<void> { const batch = this.db.createBatch();
ids.forEach(id => { batch.delete(this.db.collection(this.collection).doc(id)); });
await batch.commit(); }}ShortUrlController
功能描述: 短链接控制器,处理HTTP请求
代码示例:
export class ShortUrlController extends BaseController { private shortUrlService: ShortUrlService;
constructor() { super(); this.shortUrlService = new ShortUrlService(); }
// 创建短链接 async create(params: CreateShortUrlRequest, userInfo: UserResponse): Promise<AppResponse<ShortUrl>> { return this.wrapAsync(async () => { const shortUrl = await this.shortUrlService.create({ userId: userInfo.id, ...params, createdAt: Date.now(), updatedAt: Date.now() });
return this.success(shortUrl, '创建短链接成功'); }); }
// 查询短链接(公开) async query(params: { code: string }): Promise<AppResponse<ShortUrl>> { return this.wrapAsync(async () => { const shortUrl = await this.shortUrlService.findByCode(params.code);
if (!shortUrl) { throw new NotFoundException('短链接不存在'); }
if (!shortUrl.isPublic) { throw new ForbiddenException('短链接不公开'); }
return this.success(shortUrl, '查询成功'); }); }
// 获取用户短链接列表 async getUserUrls(params: GetUserUrlsRequest, userInfo: UserResponse): Promise<AppResponse<Page<ShortUrl>>> { return this.wrapAsync(async () => { const result = await this.shortUrlService.queryByUser(userInfo.id, params); return this.success(result, '查询成功'); }); }
// 更新短链接 async update(params: UpdateShortUrlRequest, userInfo: UserResponse): Promise<AppResponse<ShortUrl>> { return this.wrapAsync(async () => { const shortUrl = await this.shortUrlService.findById(params.id);
if (!shortUrl) { throw new NotFoundException('短链接不存在'); }
if (shortUrl.userId !== userInfo.id) { throw new ForbiddenException('无权限修改此短链接'); }
const updatedShortUrl = await this.shortUrlService.update(params.id, { ...params, updatedAt: Date.now() });
return this.success(updatedShortUrl, '更新成功'); }); }
// 删除短链接 async delete(params: { id: string }, userInfo: UserResponse): Promise<AppResponse<void>> { return this.wrapAsync(async () => { const shortUrl = await this.shortUrlService.findById(params.id);
if (!shortUrl) { throw new NotFoundException('短链接不存在'); }
if (shortUrl.userId !== userInfo.id) { throw new ForbiddenException('无权限删除此短链接'); }
await this.shortUrlService.delete(params.id); return this.success(null, '删除成功'); }); }
// 获取访问统计 async getStats(params: { id: string }, userInfo: UserResponse): Promise<AppResponse<AccessStats>> { return this.wrapAsync(async () => { const shortUrl = await this.shortUrlService.findById(params.id);
if (!shortUrl) { throw new NotFoundException('短链接不存在'); }
if (shortUrl.userId !== userInfo.id) { throw new ForbiddenException('无权限查看此统计'); }
const stats = await this.calculateAccessStats(shortUrl); return this.success(stats, '查询成功'); }); }
// 计算访问统计 private async calculateAccessStats(shortUrl: ShortUrl): Promise<AccessStats> { // 这里可以实现更详细的统计逻辑 return { totalAccess: shortUrl.accessCount, uniqueAccess: Math.floor(shortUrl.accessCount * 0.8), // 估算 dailyAccess: {}, referrers: {}, userAgents: {}, countries: {} }; }}🔧 工具函数
短链接工具
功能描述: 提供短链接相关的工具函数
代码示例:
export const shortUrlUtils = { // 生成短码 generateCode(length: number = 6): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = '';
for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); }
return result; },
// 验证URL格式 validateUrl(url: string): ValidationResult { const errors: string[] = [];
try { const urlObj = new URL(url);
// 检查协议 if (!['http:', 'https:'].includes(urlObj.protocol)) { errors.push('只支持HTTP和HTTPS协议'); }
// 检查主机名 if (!urlObj.hostname) { errors.push('URL必须包含有效的主机名'); }
} catch { errors.push('URL格式不正确'); }
// 检查长度 if (url.length > 2048) { errors.push('URL长度不能超过2048个字符'); }
return { isValid: errors.length === 0, errors }; },
// 提取域名 extractDomain(url: string): string { try { const urlObj = new URL(url); return urlObj.hostname; } catch { return 'unknown'; } },
// 生成短链接URL generateShortUrl(code: string, baseUrl: string = 'https://short.pennylens.com'): string { return `${baseUrl}/${code}`; },
// 检查是否为短链接 isShortUrl(url: string, baseUrl: string = 'https://short.pennylens.com'): boolean { return url.startsWith(baseUrl); },
// 从短链接URL中提取短码 extractCodeFromUrl(url: string, baseUrl: string = 'https://short.pennylens.com'): string | null { if (!this.isShortUrl(url, baseUrl)) { return null; }
const path = url.replace(baseUrl, ''); return path.startsWith('/') ? path.slice(1) : path; },
// 格式化访问统计 formatAccessStats(stats: AccessStats): FormattedStats { return { totalAccess: stats.totalAccess.toLocaleString(), uniqueAccess: stats.uniqueAccess.toLocaleString(), dailyAccess: Object.entries(stats.dailyAccess).map(([date, count]) => ({ date, count: count.toLocaleString() })), topReferrers: Object.entries(stats.referrers) .sort(([,a], [,b]) => b - a) .slice(0, 10) .map(([referrer, count]) => ({ referrer, count: count.toLocaleString() })), topBrowsers: Object.entries(stats.userAgents) .sort(([,a], [,b]) => b - a) .slice(0, 5) .map(([browser, count]) => ({ browser, count: count.toLocaleString() })) }; }};🧪 测试
单元测试
功能描述: 短链接服务的单元测试
代码示例:
// 短链接服务测试describe('ShortUrlService', () => { let shortUrlService: ShortUrlService;
beforeEach(() => { shortUrlService = new ShortUrlService(); });
describe('create', () => { it('should create short URL successfully', async () => { const data = { userId: 'user123', code: 'abc123', originalUrl: 'https://example.com', title: 'Test URL', isPublic: true, accessCount: 0, createdAt: Date.now(), updatedAt: Date.now() };
const result = await shortUrlService.create(data);
expect(result._id).toBeDefined(); expect(result.code).toBe(data.code); expect(result.originalUrl).toBe(data.originalUrl); }); });
describe('findByCode', () => { it('should find short URL by code', async () => { const code = 'abc123'; const shortUrl = await shortUrlService.findByCode(code);
if (shortUrl) { expect(shortUrl.code).toBe(code); } else { expect(shortUrl).toBeNull(); } }); });
describe('incrementAccessCount', () => { it('should increment access count', async () => { const id = 'test-id'; const initialCount = 10;
// 模拟现有记录 jest.spyOn(shortUrlService, 'findById').mockResolvedValue({ _id: id, accessCount: initialCount } as ShortUrl);
await shortUrlService.incrementAccessCount(id);
// 验证访问计数已增加 expect(shortUrlService.findById).toHaveBeenCalledWith(id); }); });});
// 短链接工具测试describe('shortUrlUtils', () => { describe('generateCode', () => { it('should generate code with correct length', () => { const code = shortUrlUtils.generateCode(8); expect(code).toHaveLength(8); expect(/^[A-Za-z0-9]+$/.test(code)).toBe(true); }); });
describe('validateUrl', () => { it('should validate correct URLs', () => { const validUrls = [ 'https://example.com', 'http://test.org/path', 'https://subdomain.example.com:8080/path?query=1' ];
validUrls.forEach(url => { const result = shortUrlUtils.validateUrl(url); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); });
it('should reject invalid URLs', () => { const invalidUrls = [ 'not-a-url', 'ftp://example.com', 'https://', 'a'.repeat(3000) ];
invalidUrls.forEach(url => { const result = shortUrlUtils.validateUrl(url); expect(result.isValid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); }); });
describe('extractDomain', () => { it('should extract domain correctly', () => { expect(shortUrlUtils.extractDomain('https://example.com/path')).toBe('example.com'); expect(shortUrlUtils.extractDomain('http://subdomain.test.org')).toBe('subdomain.test.org'); expect(shortUrlUtils.extractDomain('invalid-url')).toBe('unknown'); }); });});📚 最佳实践
1. 性能优化
- 短码生成: 使用高效的随机算法生成短码
- 数据库索引: 为常用查询字段建立索引
- 缓存策略: 缓存热点短链接数据
- 批量操作: 支持批量删除和管理
2. 安全考虑
- URL验证: 严格验证输入URL的格式和安全性
- 访问控制: 实现基于用户的访问权限控制
- 恶意URL检测: 检测和阻止恶意URL
- 访问限制: 实现访问频率限制
3. 用户体验
- 短码长度: 使用合适的短码长度(6-8位)
- 自定义短码: 支持用户自定义短码(可选)
- 过期管理: 支持设置链接过期时间
- 统计可视化: 提供直观的访问统计图表
4. 扩展性
- 多租户支持: 支持多用户和多组织
- API版本管理: 支持API版本升级
- 插件系统: 支持功能扩展插件
- 监控告警: 实现完整的监控和告警机制
Penny Lens 短链接服务 - 让链接分享更简单、更高效 🔗