# All files / modules/auth auth.service.ts

0% Statements 0/81
0% Branches 0/1
0% Functions 0/1
0% Lines 0/81

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115

     import jwt from 'jsonwebtoken';
    import { v4 as uuidv4 } from 'uuid';
    import { config } from '../../config';
    import { prisma } from '../../models';
    import { JwtPayload } from '../../types';

    // 验证码存储（生产环境应使用 Redis）
    const smsCodes = new Map<string, { code: string; expireAt: number }>();

    // 生成验证码
    export function generateSmsCode(phone: string): string {
      const code = Math.random().toString().slice(2, 6);
      smsCodes.set(phone, {
        code,
        expireAt: Date.now() + 5 * 60 * 1000, // 5分钟有效
      });
      console.log(`📱 验证码已生成: ${phone} -> ${code}`);
      return code;
    }

    // 验证验证码
    export function verifySmsCode(phone: string, code: string): boolean {
      const stored = smsCodes.get(phone);
      if (!stored) return false;
      if (Date.now() > stored.expireAt) {
        smsCodes.delete(phone);
        return false;
      }
      if (stored.code !== code) return false;
      smsCodes.delete(phone);
      return true;
    }

    // 生成 JWT Token
    export function generateToken(userId: string, phone?: string): string {
      const secret = config.jwt.secret;
      const payload: Omit<JwtPayload, 'iat' | 'exp'> = { userId, phone };
      return jwt.sign(payload, secret, {
        expiresIn: '7d',
      });
    }

    // 手机号登录/注册
    export async function loginWithPhone(phone: string, code?: string): Promise<{
      token: string;
      user: {
        id: string;
        phone: string;
        nickname: string;
        avatar: string;
        memberLevel: number;
        isNewUser: boolean;
      };
    }> {
      // 免密登录：code 为空或为"123456"时直接登录（线上线下都关闭验证）
      const skipVerify = !code || code === '123456';

      if (!skipVerify) {
        // 验证验证码
        const isValid = verifySmsCode(phone, code || '');
        if (!isValid) {
          throw new Error('验证码错误或已过期');
        }
      }

      // 查找或创建用户
      let user = await prisma.user.findFirst({ where: { phone } });
      let isNewUser = false;

      if (!user) {
        user = await prisma.user.create({
          data: {
            phone,
            nickname: `用户${phone.slice(-4)}`,
            avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${phone}`,
            memberLevel: 0,
            dailyUsage: 0,
            lastUsageDate: '',
          },
        });
        isNewUser = true;
      }

      const token = generateToken(user.id.toString(), phone);

      return {
        token,
        user: {
          id: user.id.toString(),
          phone: user.phone!,
          nickname: user.nickname,
          avatar: user.avatar,
          memberLevel: user.memberLevel,
          isNewUser,
        },
      };
    }

    // 获取用户信息
    export async function getUserInfo(userId: string) {
      const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
      if (!user) {
        throw new Error('用户不存在');
      }

      return {
        id: user.id.toString(),
        phone: user.phone,
        nickname: user.nickname,
        avatar: user.avatar,
        memberLevel: user.memberLevel,
        memberExpireAt: user.memberExpireAt,
        dailyUsage: user.dailyUsage,
      };
    }