wallet.service.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /**
  2. * 钱包服务 - 余额查询、扣费(原子事务)、充值、入账
  3. */
  4. import { Injectable, Logger } from '@nestjs/common';
  5. import { PrismaService } from '../prisma/prisma.service';
  6. import { InsufficientBalanceException } from './exceptions/insufficient-balance.exception';
  7. @Injectable()
  8. export class WalletService {
  9. private readonly logger = new Logger(WalletService.name);
  10. constructor(private readonly prisma: PrismaService) {}
  11. async getWallet(userId: string) {
  12. const user = await this.prisma.user.findUnique({
  13. where: { id: userId },
  14. select: { balanceCents: true, currency: true },
  15. });
  16. if (!user) {
  17. throw new Error('用户不存在');
  18. }
  19. const transactions = await this.prisma.balanceTransaction.findMany({
  20. where: { userId },
  21. orderBy: { createdAt: 'desc' },
  22. take: 20,
  23. select: {
  24. id: true,
  25. amountCents: true,
  26. type: true,
  27. refId: true,
  28. createdAt: true,
  29. },
  30. });
  31. return {
  32. balanceCents: Number(user.balanceCents),
  33. currency: user.currency,
  34. transactions: transactions.map((t) => ({
  35. ...t,
  36. amountCents: Number(t.amountCents),
  37. refId: t.refId ?? null,
  38. createdAt: t.createdAt.toISOString(),
  39. })),
  40. };
  41. }
  42. /**
  43. * 原子扣费 - 使用 UPDATE WHERE 保证余额充足
  44. * 返回值 { count: 0 } 表示余额不足,抛出 InsufficientBalanceException
  45. * 插入 BalanceTransaction type='usage'
  46. */
  47. async deduct(
  48. userId: string,
  49. amountCents: bigint,
  50. refId: string,
  51. _model?: string,
  52. ): Promise<{ newBalance: bigint }> {
  53. const result = await this.prisma.$transaction(async (tx) => {
  54. // 原子扣减:仅当 balanceCents >= amountCents 时更新
  55. const updateResult = await tx.user.updateMany({
  56. where: {
  57. id: userId,
  58. balanceCents: { gte: amountCents },
  59. },
  60. data: {
  61. balanceCents: { decrement: amountCents },
  62. },
  63. });
  64. if (updateResult.count === 0) {
  65. const user = await tx.user.findUnique({
  66. where: { id: userId },
  67. select: { balanceCents: true },
  68. });
  69. throw new InsufficientBalanceException(
  70. '余额不足,无法完成扣费',
  71. amountCents,
  72. user?.balanceCents ?? 0n,
  73. );
  74. }
  75. // 记录交易
  76. await tx.balanceTransaction.create({
  77. data: {
  78. userId,
  79. amountCents: -amountCents,
  80. type: 'usage',
  81. refId,
  82. },
  83. });
  84. // 读取新余额
  85. const updated = await tx.user.findUniqueOrThrow({
  86. where: { id: userId },
  87. select: { balanceCents: true },
  88. });
  89. return { newBalance: updated.balanceCents };
  90. });
  91. this.logger.debug(`Deducted user=${userId} amount=${amountCents} ref=${refId}`);
  92. return result;
  93. }
  94. /**
  95. * 入账(充值/退款)- 直接增加余额并记录交易
  96. */
  97. async credit(
  98. userId: string,
  99. amountCents: bigint,
  100. refId: string,
  101. type: string,
  102. ): Promise<{ newBalance: bigint }> {
  103. const result = await this.prisma.$transaction(async (tx) => {
  104. const updated = await tx.user.update({
  105. where: { id: userId },
  106. data: {
  107. balanceCents: { increment: amountCents },
  108. },
  109. select: { balanceCents: true },
  110. });
  111. await tx.balanceTransaction.create({
  112. data: {
  113. userId,
  114. amountCents,
  115. type,
  116. refId,
  117. },
  118. });
  119. return { newBalance: updated.balanceCents };
  120. });
  121. this.logger.debug(`Credited user=${userId} amount=${amountCents} ref=${refId} type=${type}`);
  122. return result;
  123. }
  124. }