쿠폰 자동 입력 사이트 작성
로그에서 `err_code=40011, msg=SAME TYPE EXCHANGE.`가 확인됩니다. **SAME TYPE EXCHANGE**는 WOS에서 **같은 종류(유형)의 보상을 이미 다른 쿠폰으로 받은 경우** 발생합니다. 예: `gogoWOS`와 `VpqG7dDK7` 둘 다 같은 종류의 아이템을 주는 쿠폰이라면, 하나를 이미 받았을 때 나머지는 이 에러가 납니다. - `40008 RECEIVED` → **이 쿠폰 자체를** 이미 수령 - `40011 SAME TYPE EXCHANGE` → **다른 쿠폰으로** 같은 유형 보상 이미 수령 두 가지 처리 방법이 있습니다: 1. **성공으로 처리** (40008처럼): 어차피 받을 수 없으니 다음에 재시도 안 함 2. **실패로 처리**: 다음날 다시 시도 (다음날 가능성 있는 경우) 어떻게 처리할까요?
This commit is contained in:
parent
107f1ffe84
commit
1628317b26
@ -1,7 +1,7 @@
|
||||
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 { Cron } from '@nestjs/schedule';
|
||||
import axios from 'axios';
|
||||
import * as crypto from 'crypto';
|
||||
import { WosUser } from '../entities/wos-user.entity';
|
||||
@ -10,6 +10,7 @@ import { WosCoupon } from '../entities/wos-coupon.entity';
|
||||
|
||||
const SECRET = 'tB87#kPtkxqOS2';
|
||||
const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api';
|
||||
const OCR_SERVER = 'http://127.0.0.1:5001';
|
||||
|
||||
const COMMON_HEADERS = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@ -36,18 +37,49 @@ export class WosService {
|
||||
private wosCouponRepo: Repository<WosCoupon>,
|
||||
) {}
|
||||
|
||||
private makeSign(fid: string, timestamp: number): string {
|
||||
const raw = `fid=${fid}&time=${timestamp}${SECRET}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
private now(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
private makePlayerSign(fid: string, timestamp: number): string {
|
||||
const raw = `fid=${fid}&time=${timestamp}${SECRET}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
private makeCaptchaSign(fid: string, timestamp: number): string {
|
||||
const raw = `fid=${fid}&init=0&time=${timestamp}${SECRET}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
private makeGiftSign(fid: string, timestamp: number, cdk: string, captchaCode: string): string {
|
||||
const raw = `captcha_code=${captchaCode}&cdk=${cdk}&fid=${fid}&time=${timestamp}${SECRET}`;
|
||||
return crypto.createHash('md5').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
private async solveCaptcha(fid: string): Promise<string> {
|
||||
const ts = this.now();
|
||||
const sign = this.makeCaptchaSign(fid, ts);
|
||||
const resp = await axios.post(
|
||||
`${WOS_API_BASE}/captcha`,
|
||||
new URLSearchParams({ fid, time: String(ts), sign, init: '0' }),
|
||||
{ headers: { ...COMMON_HEADERS } },
|
||||
);
|
||||
if (resp.data.code !== 0) throw new Error(`Captcha 요청 실패: ${resp.data.msg}`);
|
||||
|
||||
const imgB64: string = resp.data.data.img;
|
||||
const imgData = imgB64.includes(',') ? imgB64.split(',')[1] : imgB64;
|
||||
const imgBuffer = Buffer.from(imgData, 'base64');
|
||||
|
||||
const ocrResp = await axios.post(OCR_SERVER, imgBuffer, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
timeout: 5000,
|
||||
});
|
||||
return String(ocrResp.data).trim().toLowerCase();
|
||||
}
|
||||
|
||||
async getPlayer(fid: string) {
|
||||
const timestamp = this.now();
|
||||
const sign = this.makeSign(fid, timestamp);
|
||||
const sign = this.makePlayerSign(fid, timestamp);
|
||||
|
||||
const response = await axios.post(
|
||||
`${WOS_API_BASE}/player`,
|
||||
@ -77,42 +109,52 @@ export class WosService {
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
private async acquireSession(fid: string): Promise<string> {
|
||||
const timestamp = this.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'];
|
||||
if (setCookie && setCookie.length > 0) {
|
||||
return setCookie.map((c: string) => c.split(';')[0]).join('; ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private async redeemOne(
|
||||
fid: string,
|
||||
code: string,
|
||||
session: string,
|
||||
maxRetries = 5,
|
||||
): Promise<{ success: boolean; message: string; err_code?: number }> {
|
||||
const timestamp = this.now();
|
||||
const sign = this.makeSign(fid, timestamp);
|
||||
const ts1 = this.now();
|
||||
const playerSign = this.makePlayerSign(fid, ts1);
|
||||
await axios.post(
|
||||
`${WOS_API_BASE}/player`,
|
||||
new URLSearchParams({ fid, time: String(ts1), sign: playerSign }),
|
||||
{ headers: { ...COMMON_HEADERS } },
|
||||
);
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
let captchaCode: string;
|
||||
try {
|
||||
captchaCode = await this.solveCaptcha(fid);
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `캡차 오류: ${err.message}` };
|
||||
}
|
||||
|
||||
const ts2 = this.now();
|
||||
const giftSign = this.makeGiftSign(fid, ts2, code, captchaCode);
|
||||
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 } },
|
||||
new URLSearchParams({ fid, time: String(ts2), sign: giftSign, cdk: code, captcha_code: captchaCode }),
|
||||
{ headers: { ...COMMON_HEADERS } },
|
||||
);
|
||||
|
||||
const data = response.data;
|
||||
this.logger.log(`[${fid}] ${code} attempt=${attempt} captcha=${captchaCode} → code=${data.code} err=${data.err_code} msg=${data.msg}`);
|
||||
|
||||
if (data.code === 0) return { success: true, message: SUCCESS_MSG };
|
||||
if (data.err_code === 40103) {
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
continue;
|
||||
}
|
||||
return { success: false, message: `캡차 인식 실패 (${maxRetries}회 시도)`, err_code: data.err_code };
|
||||
}
|
||||
return { success: false, message: data.msg || `실패 (${data.err_code})`, err_code: data.err_code };
|
||||
}
|
||||
|
||||
return { success: false, message: '알 수 없는 오류' };
|
||||
}
|
||||
|
||||
async redeemAll(fid: string) {
|
||||
const activeCoupons = await this.wosCouponRepo.find({
|
||||
where: { is_active: true, is_expired: false },
|
||||
@ -125,7 +167,6 @@ export class WosService {
|
||||
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)) {
|
||||
@ -135,13 +176,7 @@ export class WosService {
|
||||
|
||||
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);
|
||||
}
|
||||
result = await this.redeemOne(fid, coupon.code);
|
||||
} catch (err: any) {
|
||||
result = { success: false, message: err.message || '오류 발생' };
|
||||
}
|
||||
@ -159,7 +194,7 @@ export class WosService {
|
||||
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));
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
}
|
||||
|
||||
return results;
|
||||
@ -224,8 +259,7 @@ export class WosService {
|
||||
if (alreadyDone) continue;
|
||||
|
||||
try {
|
||||
const session = await this.acquireSession(user.fid);
|
||||
const result = await this.redeemOne(user.fid, code, session);
|
||||
const result = await this.redeemOne(user.fid, code);
|
||||
|
||||
const finalResult =
|
||||
result.err_code === 40008
|
||||
|
||||
@ -46,11 +46,6 @@
|
||||
<CouponInput />
|
||||
</div>
|
||||
|
||||
<div class="card" v-if="store.player">
|
||||
<HistoryList />
|
||||
<p class="empty-history" v-if="store.history.length === 0">아직 입력 이력이 없습니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="card admin-card">
|
||||
<CouponManager />
|
||||
</div>
|
||||
@ -62,7 +57,6 @@ import { ref, onMounted } from 'vue';
|
||||
import { useWosStore } from '../stores/wos.store';
|
||||
import UserCard from '../components/UserCard.vue';
|
||||
import CouponInput from '../components/CouponInput.vue';
|
||||
import HistoryList from '../components/HistoryList.vue';
|
||||
import CouponManager from '../components/CouponManager.vue';
|
||||
|
||||
const store = useWosStore();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user