credit-service.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import { db } from '@/lib/db';
  2. import { users, userActivities } from '@/lib/schema';
  3. import { eq } from 'drizzle-orm';
  4. export interface CreditDeductionResult {
  5. success: boolean;
  6. message: string;
  7. user?: {
  8. id: string;
  9. credits: number;
  10. };
  11. credits?: {
  12. deducted: number;
  13. remaining: number;
  14. };
  15. }
  16. export interface ActivityRecord {
  17. id: string;
  18. type: string;
  19. description: string;
  20. creditAmount: number | null;
  21. metadata: string | null;
  22. createdAt: Date | null;
  23. }
  24. // 扣除用户积分
  25. export async function deductCredits(
  26. userId: string,
  27. amount: number,
  28. description: string,
  29. metadata?: any
  30. ): Promise<CreditDeductionResult> {
  31. try {
  32. // 首先获取用户当前积分
  33. const [user] = await db.select().from(users).where(eq(users.id, userId));
  34. if (!user) {
  35. return {
  36. success: false,
  37. message: '用户不存在'
  38. };
  39. }
  40. // 计算总积分(订阅积分 + 永久积分)
  41. const totalCredits = (user.subscriptionCredits || 0) + (user.credits || 0);
  42. // 检查积分是否足够
  43. if (totalCredits < amount) {
  44. return {
  45. success: false,
  46. message: '积分不足'
  47. };
  48. }
  49. // 优先使用订阅积分
  50. let remainingAmount = amount;
  51. let newSubscriptionCredits = user.subscriptionCredits || 0;
  52. let newPermanentCredits = user.credits || 0;
  53. // 先扣除订阅积分
  54. if (remainingAmount > 0 && newSubscriptionCredits > 0) {
  55. const deductFromSubscription = Math.min(remainingAmount, newSubscriptionCredits);
  56. newSubscriptionCredits -= deductFromSubscription;
  57. remainingAmount -= deductFromSubscription;
  58. }
  59. // 如果还有剩余需要扣除,则从永久积分中扣除
  60. if (remainingAmount > 0) {
  61. newPermanentCredits -= remainingAmount;
  62. }
  63. // 更新用户积分
  64. await db
  65. .update(users)
  66. .set({
  67. credits: newPermanentCredits,
  68. subscriptionCredits: newSubscriptionCredits,
  69. updatedAt: new Date()
  70. })
  71. .where(eq(users.id, userId));
  72. // 记录活动
  73. await db.insert(userActivities).values({
  74. userId: userId,
  75. type: 'credit_deduct',
  76. description: description,
  77. creditAmount: -amount,
  78. metadata: metadata ? JSON.stringify({
  79. ...metadata,
  80. deductedFromSubscription: (user.subscriptionCredits || 0) - newSubscriptionCredits,
  81. deductedFromPermanent: (user.credits || 0) - newPermanentCredits
  82. }) : null
  83. });
  84. return {
  85. success: true,
  86. message: '积分扣除成功',
  87. user: {
  88. id: userId,
  89. credits: newPermanentCredits + newSubscriptionCredits
  90. },
  91. credits: {
  92. deducted: amount,
  93. remaining: newPermanentCredits + newSubscriptionCredits
  94. }
  95. };
  96. } catch (error) {
  97. console.error('扣除积分失败:', error);
  98. return {
  99. success: false,
  100. message: '积分扣除失败'
  101. };
  102. }
  103. }
  104. // 增加用户积分
  105. export async function addCredits(
  106. userId: string,
  107. amount: number,
  108. description: string,
  109. metadata?: any,
  110. creditType: 'permanent' | 'subscription' = 'permanent'
  111. ): Promise<CreditDeductionResult> {
  112. try {
  113. // 首先获取用户当前积分
  114. const [user] = await db.select().from(users).where(eq(users.id, userId));
  115. if (!user) {
  116. return {
  117. success: false,
  118. message: '用户不存在'
  119. };
  120. }
  121. // 根据积分类型增加相应的积分
  122. let updateData: any = { updatedAt: new Date() };
  123. if (creditType === 'subscription') {
  124. // 添加订阅积分
  125. const newSubscriptionCredits = (user.subscriptionCredits || 0) + amount;
  126. updateData.subscriptionCredits = newSubscriptionCredits;
  127. } else {
  128. // 添加永久积分
  129. const newCredits = user.credits + amount;
  130. updateData.credits = newCredits;
  131. }
  132. await db
  133. .update(users)
  134. .set(updateData)
  135. .where(eq(users.id, userId));
  136. // 记录活动
  137. await db.insert(userActivities).values({
  138. userId: userId,
  139. type: 'credit_add',
  140. description: description,
  141. creditAmount: amount,
  142. metadata: metadata ? JSON.stringify({
  143. ...metadata,
  144. creditType: creditType
  145. }) : JSON.stringify({ creditType: creditType })
  146. });
  147. // 计算总积分
  148. const totalCredits = (creditType === 'subscription'
  149. ? (user.subscriptionCredits || 0) + amount + user.credits
  150. : user.credits + amount + (user.subscriptionCredits || 0)
  151. );
  152. return {
  153. success: true,
  154. message: '积分增加成功',
  155. user: {
  156. id: userId,
  157. credits: totalCredits
  158. },
  159. credits: {
  160. deducted: -amount, // 负数表示增加
  161. remaining: totalCredits
  162. }
  163. };
  164. } catch (error) {
  165. console.error('增加积分失败:', error);
  166. return {
  167. success: false,
  168. message: '积分增加失败'
  169. };
  170. }
  171. }
  172. // 记录用户活动(不涉及积分变化)
  173. export async function recordActivity(
  174. userId: string,
  175. type: string,
  176. description: string,
  177. metadata?: Record<string, any>
  178. ): Promise<boolean> {
  179. try {
  180. await db.insert(userActivities).values({
  181. userId: userId,
  182. type: type,
  183. description: description,
  184. creditAmount: null,
  185. metadata: metadata ? JSON.stringify(metadata) : null,
  186. });
  187. return true;
  188. } catch (error) {
  189. console.error('记录用户活动失败:', error);
  190. return false;
  191. }
  192. }
  193. // 获取用户活动记录
  194. export async function getUserActivities(
  195. userId: string,
  196. limit: number = 20,
  197. offset: number = 0
  198. ): Promise<ActivityRecord[]> {
  199. try {
  200. const activities = await db.query.userActivities.findMany({
  201. where: eq(userActivities.userId, userId),
  202. orderBy: (userActivities, { desc }) => [desc(userActivities.createdAt)],
  203. limit: limit,
  204. offset: offset,
  205. });
  206. return activities;
  207. } catch (error) {
  208. console.error('获取用户活动失败:', error);
  209. return [];
  210. }
  211. }
  212. // 清零用户订阅积分(订阅过期时调用)
  213. export async function clearSubscriptionCredits(
  214. userId: string,
  215. reason: string = 'credit_description.subscription_expired'
  216. ): Promise<CreditDeductionResult> {
  217. try {
  218. // 首先获取用户当前积分
  219. const [user] = await db.select().from(users).where(eq(users.id, userId));
  220. if (!user) {
  221. return {
  222. success: false,
  223. message: '用户不存在'
  224. };
  225. }
  226. const clearedAmount = user.subscriptionCredits || 0;
  227. if (clearedAmount === 0) {
  228. return {
  229. success: true,
  230. message: '订阅积分已为零,无需清零',
  231. user: {
  232. id: userId,
  233. credits: user.credits
  234. },
  235. credits: {
  236. deducted: 0,
  237. remaining: user.credits
  238. }
  239. };
  240. }
  241. // 清零订阅积分
  242. await db
  243. .update(users)
  244. .set({
  245. subscriptionCredits: 0,
  246. updatedAt: new Date()
  247. })
  248. .where(eq(users.id, userId));
  249. // 记录活动
  250. await db.insert(userActivities).values({
  251. userId: userId,
  252. type: 'subscription_expired',
  253. description: reason,
  254. creditAmount: -clearedAmount,
  255. metadata: JSON.stringify({
  256. clearedSubscriptionCredits: clearedAmount,
  257. remainingPermanentCredits: user.credits
  258. })
  259. });
  260. return {
  261. success: true,
  262. message: '订阅积分已清零',
  263. user: {
  264. id: userId,
  265. credits: user.credits // 只保留永久积分
  266. },
  267. credits: {
  268. deducted: clearedAmount,
  269. remaining: user.credits
  270. }
  271. };
  272. } catch (error) {
  273. console.error('清零订阅积分失败:', error);
  274. return {
  275. success: false,
  276. message: '清零订阅积分失败'
  277. };
  278. }
  279. }