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 短链接服务 - 让链接分享更简单、更高效 🔗
