auth.service.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import jwt from 'jsonwebtoken';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { config } from '../../config';
  4. import { prisma } from '../../models';
  5. import { JwtPayload } from '../../types';
  6. import { handleInvite } from '../invite/invite.service';
  7. // 验证码存储(生产环境应使用 Redis)
  8. const smsCodes = new Map<string, { code: string; expireAt: number }>();
  9. // 生成验证码
  10. export function generateSmsCode(phone: string): string {
  11. const code = Math.random().toString().slice(2, 6);
  12. smsCodes.set(phone, {
  13. code,
  14. expireAt: Date.now() + 5 * 60 * 1000, // 5分钟有效
  15. });
  16. console.log(`📱 验证码已生成: ${phone} -> ${code}`);
  17. return code;
  18. }
  19. // 验证验证码
  20. export function verifySmsCode(phone: string, code: string): boolean {
  21. const stored = smsCodes.get(phone);
  22. if (!stored) return false;
  23. if (Date.now() > stored.expireAt) {
  24. smsCodes.delete(phone);
  25. return false;
  26. }
  27. if (stored.code !== code) return false;
  28. smsCodes.delete(phone);
  29. return true;
  30. }
  31. // 生成 JWT Token
  32. export function generateToken(userId: string, phone?: string): string {
  33. const secret = config.jwt.secret;
  34. const payload: Omit<JwtPayload, 'iat' | 'exp'> = { userId, phone };
  35. return jwt.sign(payload, secret, {
  36. expiresIn: '7d',
  37. });
  38. }
  39. // 手机号登录/注册
  40. export async function loginWithPhone(phone: string, code?: string, inviteCode?: string): Promise<{
  41. token: string;
  42. user: {
  43. id: string;
  44. phone: string;
  45. nickname: string;
  46. avatar: string;
  47. memberLevel: number;
  48. isNewUser: boolean;
  49. };
  50. }> {
  51. // 免密登录:code 为空或为"123456"时直接登录(线上线下都关闭验证)
  52. const skipVerify = !code || code === '123456';
  53. if (!skipVerify) {
  54. // 验证验证码
  55. const isValid = verifySmsCode(phone, code || '');
  56. if (!isValid) {
  57. throw new Error('验证码错误或已过期');
  58. }
  59. }
  60. // 查找或创建用户
  61. let user = await prisma.user.findFirst({ where: { phone } });
  62. let isNewUser = false;
  63. if (!user) {
  64. user = await prisma.user.create({
  65. data: {
  66. phone,
  67. nickname: `用户${phone.slice(-4)}`,
  68. avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${phone}`,
  69. memberLevel: 0,
  70. dailyUsage: 0,
  71. lastUsageDate: '',
  72. },
  73. });
  74. isNewUser = true;
  75. // 处理邀请关系
  76. if (inviteCode) {
  77. const shareRecord = await prisma.shareRecord.findFirst({
  78. where: { shareCode: inviteCode },
  79. });
  80. if (shareRecord) {
  81. await handleInvite(shareRecord.userId, user.id, inviteCode);
  82. }
  83. }
  84. }
  85. const token = generateToken(user.id.toString(), phone);
  86. return {
  87. token,
  88. user: {
  89. id: user.id.toString(),
  90. phone: user.phone!,
  91. nickname: user.nickname,
  92. avatar: user.avatar,
  93. memberLevel: user.memberLevel,
  94. isNewUser,
  95. },
  96. };
  97. }
  98. // 获取用户信息
  99. export async function getUserInfo(userId: string) {
  100. const user = await prisma.user.findUnique({ where: { id: parseInt(userId) } });
  101. if (!user) {
  102. throw new Error('用户不存在');
  103. }
  104. return {
  105. id: user.id.toString(),
  106. phone: user.phone,
  107. nickname: user.nickname,
  108. avatar: user.avatar,
  109. memberLevel: user.memberLevel,
  110. memberExpireAt: user.memberExpireAt,
  111. dailyUsage: user.dailyUsage,
  112. };
  113. }