import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import { Cron, CronExpression } from '@nestjs/schedule'; import axios from 'axios'; import * as crypto from 'crypto'; import { WosUser } from '../entities/wos-user.entity'; import { CouponLog } from '../entities/coupon-log.entity'; import { WosCoupon } from '../entities/wos-coupon.entity'; const SECRET = 'tB87#kPtkxqOS2'; const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api'; const COMMON_HEADERS = { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Origin': 'https://wos-giftcode.centurygame.com', 'Referer': 'https://wos-giftcode.centurygame.com/', 'Accept': 'application/json, text/plain, */*', 'Accept-Language': 'ko-KR,ko;q=0.9,en;q=0.8', }; const SUCCESS_MSG = '성공'; const ALREADY_MSG = '이미 수령'; @Injectable() export class WosService { private readonly logger = new Logger(WosService.name); constructor( @InjectRepository(WosUser) private wosUserRepo: Repository, @InjectRepository(CouponLog) private couponLogRepo: Repository, @InjectRepository(WosCoupon) private wosCouponRepo: Repository, ) {} private makeSign(fid: string, timestamp: number): string { const raw = `fid=${fid}&time=${timestamp}${SECRET}`; return crypto.createHash('md5').update(raw).digest('hex'); } async getPlayer(fid: string) { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/player`, new URLSearchParams({ fid, time: String(timestamp), sign }), { headers: { ...COMMON_HEADERS } }, ); const data = response.data; if (data.code !== 0) { throw new HttpException( data.msg || `오류가 발생했습니다. (${data.err_code})`, HttpStatus.BAD_REQUEST, ); } const playerInfo = data.data; const existing = await this.wosUserRepo.findOne({ where: { fid } }); if (existing) { existing.nickname = playerInfo.nickname; existing.avatar_url = playerInfo.avatar_image; await this.wosUserRepo.save(existing); } else { await this.wosUserRepo.save({ fid, nickname: playerInfo.nickname, avatar_url: playerInfo.avatar_image }); } return playerInfo; } private async acquireSession(fid: string): Promise { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/captcha`, new URLSearchParams({ fid, time: String(timestamp), sign }), { headers: { ...COMMON_HEADERS }, withCredentials: true }, ); const setCookie = response.headers['set-cookie']; let sessionValue = ''; if (setCookie) { const found = setCookie.find((c: string) => c.toLowerCase().includes('session')); if (found) sessionValue = found.split(';')[0]; } return sessionValue; } private async redeemOne( fid: string, code: string, session: string, ): Promise<{ success: boolean; message: string; err_code?: number }> { const timestamp = Date.now(); const sign = this.makeSign(fid, timestamp); const response = await axios.post( `${WOS_API_BASE}/gift_code`, new URLSearchParams({ fid, time: String(timestamp), sign, cdk: code, validate: '1' }), { headers: { ...COMMON_HEADERS, Cookie: session } }, ); const data = response.data; if (data.code === 0) return { success: true, message: SUCCESS_MSG }; return { success: false, message: data.msg || `실패 (${data.err_code})`, err_code: data.err_code }; } async redeemAll(fid: string) { const activeCoupons = await this.wosCouponRepo.find({ where: { is_active: true, is_expired: false }, }); if (activeCoupons.length === 0) return []; const existingLogs = await this.couponLogRepo.find({ where: { fid, result_msg: In([SUCCESS_MSG, ALREADY_MSG]) }, }); const alreadySucceeded = new Set(existingLogs.map((l) => l.coupon_code)); const results: { name: string; code: string; status: string; message: string }[] = []; let session = await this.acquireSession(fid); for (const coupon of activeCoupons) { if (alreadySucceeded.has(coupon.code)) { results.push({ name: coupon.name, code: coupon.code, status: 'skipped', message: '이미 지급 완료' }); continue; } let result: { success: boolean; message: string; err_code?: number }; try { result = await this.redeemOne(fid, coupon.code, session); if (result.err_code === 40014 || result.err_code === 40004) { session = await this.acquireSession(fid); await new Promise((r) => setTimeout(r, 500)); result = await this.redeemOne(fid, coupon.code, session); } } catch (err: any) { result = { success: false, message: err.message || '오류 발생' }; } if (result.err_code === 40008) { result = { success: true, message: ALREADY_MSG, err_code: 40008 }; } if (result.err_code === 40007) { await this.wosCouponRepo.update(coupon.id, { is_expired: true, is_active: false }); result.message = '만료된 쿠폰'; } const status = result.success ? 'success' : 'error'; await this.couponLogRepo.save({ fid, coupon_code: coupon.code, result_msg: result.message }); results.push({ name: coupon.name, code: coupon.code, status, message: result.message }); await new Promise((r) => setTimeout(r, 1000)); } return results; } @Cron('0 0 9 * * *') async scheduledRedeemAll() { this.logger.log('자동 쿠폰 지급 시작...'); const users = await this.wosUserRepo.find(); for (const user of users) { try { await this.redeemAll(user.fid); this.logger.log(`[${user.fid}] ${user.nickname} 지급 완료`); } catch (err: any) { this.logger.error(`[${user.fid}] 지급 실패: ${err.message}`); } await new Promise((r) => setTimeout(r, 2000)); } this.logger.log('자동 쿠폰 지급 완료'); } async getHistory(fid: string) { return this.couponLogRepo.find({ where: { fid }, order: { executed_at: 'DESC' }, take: 100, }); } async listUsers() { return this.wosUserRepo.find({ order: { created_at: 'DESC' } }); } async listCoupons() { return this.wosCouponRepo.find({ order: { created_at: 'DESC' } }); } async addCoupon(name: string, code: string) { const exists = await this.wosCouponRepo.findOne({ where: { code } }); if (exists) { exists.name = name; exists.is_active = true; exists.is_expired = false; return this.wosCouponRepo.save(exists); } return this.wosCouponRepo.save({ name, code, is_active: true, is_expired: false }); } async toggleCoupon(id: number, is_active: boolean) { await this.wosCouponRepo.update(id, { is_active }); return this.wosCouponRepo.findOne({ where: { id } }); } async deleteCoupon(id: number) { await this.wosCouponRepo.delete(id); return { success: true }; } }