쿠폰 자동 입력 사이트 작성
로그에서 `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 { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, In } from 'typeorm';
|
import { Repository, In } from 'typeorm';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { WosUser } from '../entities/wos-user.entity';
|
import { WosUser } from '../entities/wos-user.entity';
|
||||||
@ -10,6 +10,7 @@ import { WosCoupon } from '../entities/wos-coupon.entity';
|
|||||||
|
|
||||||
const SECRET = 'tB87#kPtkxqOS2';
|
const SECRET = 'tB87#kPtkxqOS2';
|
||||||
const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api';
|
const WOS_API_BASE = 'https://wos-giftcode-api.centurygame.com/api';
|
||||||
|
const OCR_SERVER = 'http://127.0.0.1:5001';
|
||||||
|
|
||||||
const COMMON_HEADERS = {
|
const COMMON_HEADERS = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
@ -36,18 +37,49 @@ export class WosService {
|
|||||||
private wosCouponRepo: Repository<WosCoupon>,
|
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 {
|
private now(): number {
|
||||||
return Date.now();
|
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) {
|
async getPlayer(fid: string) {
|
||||||
const timestamp = this.now();
|
const timestamp = this.now();
|
||||||
const sign = this.makeSign(fid, timestamp);
|
const sign = this.makePlayerSign(fid, timestamp);
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${WOS_API_BASE}/player`,
|
`${WOS_API_BASE}/player`,
|
||||||
@ -77,42 +109,52 @@ export class WosService {
|
|||||||
return playerInfo;
|
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(
|
private async redeemOne(
|
||||||
fid: string,
|
fid: string,
|
||||||
code: string,
|
code: string,
|
||||||
session: string,
|
maxRetries = 5,
|
||||||
): Promise<{ success: boolean; message: string; err_code?: number }> {
|
): Promise<{ success: boolean; message: string; err_code?: number }> {
|
||||||
const timestamp = this.now();
|
const ts1 = this.now();
|
||||||
const sign = this.makeSign(fid, timestamp);
|
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(
|
const response = await axios.post(
|
||||||
`${WOS_API_BASE}/gift_code`,
|
`${WOS_API_BASE}/gift_code`,
|
||||||
new URLSearchParams({ fid, time: String(timestamp), sign, cdk: code, validate: '1' }),
|
new URLSearchParams({ fid, time: String(ts2), sign: giftSign, cdk: code, captcha_code: captchaCode }),
|
||||||
{ headers: { ...COMMON_HEADERS, Cookie: session } },
|
{ headers: { ...COMMON_HEADERS } },
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = response.data;
|
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.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: data.msg || `실패 (${data.err_code})`, err_code: data.err_code };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { success: false, message: '알 수 없는 오류' };
|
||||||
|
}
|
||||||
|
|
||||||
async redeemAll(fid: string) {
|
async redeemAll(fid: string) {
|
||||||
const activeCoupons = await this.wosCouponRepo.find({
|
const activeCoupons = await this.wosCouponRepo.find({
|
||||||
where: { is_active: true, is_expired: false },
|
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 alreadySucceeded = new Set(existingLogs.map((l) => l.coupon_code));
|
||||||
|
|
||||||
const results: { name: string; code: string; status: string; message: string }[] = [];
|
const results: { name: string; code: string; status: string; message: string }[] = [];
|
||||||
let session = await this.acquireSession(fid);
|
|
||||||
|
|
||||||
for (const coupon of activeCoupons) {
|
for (const coupon of activeCoupons) {
|
||||||
if (alreadySucceeded.has(coupon.code)) {
|
if (alreadySucceeded.has(coupon.code)) {
|
||||||
@ -135,13 +176,7 @@ export class WosService {
|
|||||||
|
|
||||||
let result: { success: boolean; message: string; err_code?: number };
|
let result: { success: boolean; message: string; err_code?: number };
|
||||||
try {
|
try {
|
||||||
result = await this.redeemOne(fid, coupon.code, session);
|
result = await this.redeemOne(fid, coupon.code);
|
||||||
|
|
||||||
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) {
|
} catch (err: any) {
|
||||||
result = { success: false, message: err.message || '오류 발생' };
|
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 });
|
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 });
|
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;
|
return results;
|
||||||
@ -224,8 +259,7 @@ export class WosService {
|
|||||||
if (alreadyDone) continue;
|
if (alreadyDone) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await this.acquireSession(user.fid);
|
const result = await this.redeemOne(user.fid, code);
|
||||||
const result = await this.redeemOne(user.fid, code, session);
|
|
||||||
|
|
||||||
const finalResult =
|
const finalResult =
|
||||||
result.err_code === 40008
|
result.err_code === 40008
|
||||||
|
|||||||
@ -46,11 +46,6 @@
|
|||||||
<CouponInput />
|
<CouponInput />
|
||||||
</div>
|
</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">
|
<div class="card admin-card">
|
||||||
<CouponManager />
|
<CouponManager />
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +57,6 @@ import { ref, onMounted } from 'vue';
|
|||||||
import { useWosStore } from '../stores/wos.store';
|
import { useWosStore } from '../stores/wos.store';
|
||||||
import UserCard from '../components/UserCard.vue';
|
import UserCard from '../components/UserCard.vue';
|
||||||
import CouponInput from '../components/CouponInput.vue';
|
import CouponInput from '../components/CouponInput.vue';
|
||||||
import HistoryList from '../components/HistoryList.vue';
|
|
||||||
import CouponManager from '../components/CouponManager.vue';
|
import CouponManager from '../components/CouponManager.vue';
|
||||||
|
|
||||||
const store = useWosStore();
|
const store = useWosStore();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user