Skip to content

代码风格规范

本文档定义了 MagicPush 项目的代码编写风格,包括注释规范、错误处理、异步编程等惯例。


目录


1. 注释规范

1.1 类级注释 — JSDoc

javascript
/**
 * 推送服务
 * 核心调度引擎:编排关键词过滤 → DND检查 → 遍历渠道 → 发送消息 → 记录日志的完整流程
 */
class PushService { }

/**
 * SMTP 邮件适配器
 * 通过 SMTP 协议发送邮件通知
 */
class SmtpChannel extends BaseChannel { }

1.2 方法级注释 — JSDoc(含参数说明)

javascript
/**
 * 发送消息到指定渠道
 * @param {Object} message - 消息对象
 * @param {string} message.title - 消息标题
 * @param {string} message.content - 消息内容
 * @param {string} message.type - 消息类型: text / markdown / html
 * @param {string} [message.url] - 跳转链接(可选)
 * @returns {Promise<Object>} 发送结果 { success, messageId }
 * @throws {Error} 当渠道配置无效或发送失败时抛出
 */
async send(message) {

1.3 行内注释 — 中文简洁说明

javascript
// 提取用户 ID
const userId = req.user.id;

// 检查是否在免打扰时段
if (doNotDisturbService.shouldMute(endpointId)) {
  // 记录跳过原因并返回
  return skippedResult;
}

// TODO: QQ 机器人渠道开发中,待测试后启用
// FIXME: 这里存在并发竞争问题,需要加锁

1.4 特殊标记

标记用途
TODO待完成的功能
FIXME已知问题需要修复
HACK临时 workaround
NOTE重要注意事项
DEPRECATED已废弃的 API

2. 错误处理

2.1 Controller 层 — try/catch + 统一响应

Controller 是请求处理的边界层,必须捕获所有异常并返回标准响应:

javascript
static async getCurrentUser(req, res) {
  try {
    const user = await UserModel.findById(req.user.id);
    if (!user) {
      return ResponseUtil.notFound(res, '用户不存在');
    }
    return ResponseUtil.success(res, {
      id: user.id,
      username: user.username,
      email: user.email,
    });
  } catch (error) {
    logger.error('获取用户信息失败:', error);
    return ResponseUtil.serverError(res, '获取用户信息失败');
  }
}

要点:

  • 每个 Controller 方法都用 try/catch 包裹
  • 业务错误用 ResponseUtil.notFound() / ResponseUtil.badRequest() 返回
  • 异常用 ResponseUtil.serverError() 返回,不暴露内部细节
  • 错误信息使用中文,面向用户友好

2.2 Service 层 — throw Error 向上抛出

Service 层不处理 HTTP 响应,通过抛出异常将错误传递给 Controller:

javascript
class EndpointService {
  static async pushByToken(token, message, clientIp) {
    const endpoint = EndpointModel.findByToken(token);

    // 校验失败,直接抛异常(由 Controller 的 catch 捕获)
    if (!endpoint) {
      throw new Error('无效的接口令牌');
    }

    if (!endpoint.is_active) {
      throw new Error('该接口已被禁用');
    }

    // ... 继续业务逻辑
  }
}

要点:

  • 参数校验不通过时 throw new Error('中文描述')
  • 不在此层做 try/catch,让异常自然上浮到 Controller
  • 错误消息使用中文,直接可展示给前端

2.3 全局错误中间件 — 兜底处理

javascript
// server/src/middleware/error.middleware.js
const errorMiddleware = (err, req, res, next) => {
  // 参数验证错误
  if (err.name === 'ValidationError') {
    return ResponseUtil.validationError(res, err.message);
  }

  // 数据库唯一约束冲突
  if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
    return ResponseUtil.conflict(res, '数据已存在');
  }

  // JWT 相关错误
  if (err.name === 'JsonWebTokenError') {
    return ResponseUtil.unauthorized(res, '无效的认证令牌');
  }
  if (err.name === 'TokenExpiredError') {
    return ResponseUtil.unauthorized(res, '认证令牌已过期');
  }

  // 兜底:服务器内部错误
  logger.error('未处理的异常:', err);
  return ResponseUtil.serverError(res, '服务器内部错误');
};

2.4 数据库迁移 — 静默忽略已存在的字段

javascript
// server/src/database/init.js
try {
  db.exec(`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`);
} catch (e) {
  // 字段已存在,忽略错误(正常情况)
}

3. 异步编程

3.1 后端 — async/await 为主

Controller 和 Service 层统一使用 async/await

javascript
class PushController {
  static async pushByToken(req, res) {
    try {
      const result = await PushService.pushByToken(
        req.params.token,
        req.body,
        req.ip
      );
      return ResponseUtil.success(res, result);
    } catch (error) {
      logger.error('推送失败:', error);
      return ResponseUtil.serverError(res, error.message);
    }
  }
}

3.2 Model 层 — 同步调用(better-sqlite3)

由于使用 better-sqlite3(同步 API),Model 层的方法是同步的:

javascript
class EndpointModel {
  // 同步方法 - 直接返回结果
  static findByToken(token) {
    const stmt = db.prepare(
      'SELECT * FROM endpoints WHERE token = ? AND is_active = 1'
    );
    return stmt.get(token);  // 同步调用,无需 await
  }

  static create(data) {
    const stmt = db.prepare(`
      INSERT INTO endpoints (user_id, name, token, description)
      VALUES (?, ?, ?, ?)
    `);
    const result = stmt.run(data.userId, data.name, data.token, data.description);
    return { id: result.lastInsertRowid, ...data };
  }
}

注意:Model 层返回的是普通对象/数组,Controller 中调用 Model 方法无需 await

3.3 并行执行 — Promise.all / Promise.allSettled

当需要同时发送多个渠道消息时:

javascript
// 并行发送到所有关联渠道(容忍单个失败)
const results = await Promise.allSettled(
  channels.map(channel => this._sendToChannel(channel, message))
);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    // 发送成功 → 记录 success 日志
  } else {
    // 发送失败 → 记录 failed 日志
  }
});

选择策略:

场景使用方式说明
多个独立任务并行Promise.all()任一失败则全部失败
多个独立任务并行(容忍失败)Promise.allSettled()返回每个任务的独立结果
串行依赖任务await a(); await b();b 依赖 a 的结果

3.4 前端 — 完全 async/await

javascript
const loginUser = async (credentials) => {
  try {
    const res = await login(credentials);
    if (res.success) {
      accessToken.value = res.data.accessToken;
      refreshToken.value = res.data.refreshToken;
      localStorage.setItem('accessToken', res.data.accessToken);
    }
    return res;
  } catch (error) {
    console.error('登录失败:', error);
    throw error;
  }
};

4. 模块导入导出

4.1 后端 (CommonJS)

javascript
// 导入
const express = require('express');
const bcrypt = require('bcryptjs');
const { body, validationResult } = require('express-validator');

const BaseChannel = require('./base.channel');
const { UserModel, ChannelModel } = require('../models');
const logger = require('../utils/logger');
const ResponseUtil = require('../utils/response');

// 导出 - 单个类
module.exports = UserController;

// 导出 - 多个对象
module.exports = {
  errorMiddleware,
  notFoundMiddleware,
  getChannelAdapter,
  validateChannelConfig,
};

导入顺序约定:

javascript
// 1. Node.js 内置模块
const path = require('path');

// 2. 第三方 npm 包
const express = require('express');
const jwt = require('jsonwebtoken');

// 3. 内部模块(相对路径)
const logger = require('../utils/logger');
const { UserModel } = require('../models');
const BaseChannel = require('./base.channel');

4.2 前端 (ES Modules)

javascript
// Vue 核心
import { createApp, ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'

// 第三方库
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'

// 项目内部(@/ 别名指向 src/)
import { useAuthStore } from '@/stores/auth'
import MainLayout from '@/components/Layout/MainLayout.vue'
import { login, register } from '@/api/auth'
import request from '@/utils/request'

// 导出
export const useAuthStore = defineStore('auth', () => { ... })
export default router
export function formatDate(date) { ... }
export { getCurrentUser, updateUser }

5. 日志规范

5.1 使用 Winston Logger

javascript
const logger = require('../utils/logger');

5.2 级别选择

级别场景示例
logger.error()错误 + 异常堆栈logger.error('数据库查询失败:', error)
logger.warn()可预期的异常情况logger.warn('用户登录失败:', username)
logger.info()关键业务节点logger.info(\推送成功: ${endpointName} → ${channelType}`)`
logger.debug()开发调试信息logger.debug('请求参数:', req.body)

5.3 日志格式要求

javascript
// 正确:包含上下文信息
logger.error('推送失败:', { endpointId, channelType, error: error.message });

// 正确:关键操作记录
logger.info(`用户 ${username} 创建新渠道: ${channelType}`);

// 禁止:无意义日志
logger.log('here');           // 禁止
console.log('debug');         // 生产代码禁止使用 console.log/console.error

6. 响应格式

所有 API 统一使用 ResponseUtil 工具类返回标准 JSON 格式:

6.1 成功响应

json
{
  "success": true,
  "code": 200,
  "message": "操作成功",
  "data": { ... },
  "timestamp": 1704067200000
}

6.2 错误响应

json
{
  "success": false,
  "code": 400,
  "message": "参数验证失败",
  "data": null,
  "timestamp": 1704067200000
}

6.3 推送专用响应

推送 API 使用特殊的聚合结果格式:

json
{
  "success": true,
  "total": 3,
  "successCount": 2,
  "failedCount": 1,
  "results": [
    { "success": true, "channelId": 1, "channelType": "telegram", "logId": 101 },
    { "success": true, "channelId": 2, "channelType": "bark", "logId": 102 },
    { "success": false, "channelId": 3, "channelType": "feishu", "error": "Webhook 超时" }
  ]
}

6.4 ResponseUtil 方法列表

方法HTTP 状态码用途
ResponseUtil.success(res, data)200成功响应
ResponseUtil.created(res, data)201创建成功
ResponseUtil.noContent(res)204删除成功(无返回体)
ResponseUtil.badRequest(res, message)400参数错误
ResponseUtil.unauthorized(res, message)401未认证
ResponseUtil.forbidden(res, message)403无权限
ResponseUtil.notFound(res, message)404资源不存在
ResponseUtil.conflict(res, message)409资源冲突
ResponseUtil.validationError(res, message)422验证错误
ResponseUtil.serverError(res, message)500服务器内部错误

基于 MIT 许可证开源