cccc70338d
El prefijo venía del nombre de la app Django (`home`), jubilada y borrada hoy, así que ya no significaba nada: home_stripelog -> stripelog. La BD se sigue llamando django_wow por historia (renombrarla es otra operación). Comprobado antes de tocar nada: 0 colisiones con nombres existentes, 0 palabras reservadas (contrastado contra las 260 de MySQL), sin vistas ni triggers que dependieran de ellas. El RENAME va en una sola sentencia porque así es atómico, y MySQL reapunta solo las 4 claves ajenas entre estas tablas. 170 referencias actualizadas en 37 ficheros. Se dejó a propósito sql/drop_home_securitytoken_authuser_fk.sql sin tocar: es una migración ya aplicada y reescribirla sería falsear el historial. De paso salió un fallo previo: prices.ts consultaba `home_restoreitemprice`, una tabla que NO existe. El try/catch de priceFrom se comía el error y devolvía siempre el precio por defecto, así que el precio de restaurar objetos nunca fue configurable. Se deja documentado en el código; crear la tabla es otra decisión. Verificado tras aplicar: 0 tablas home_, las 51 siguen ahí, y el conteo exacto de filas cuadra con el volcado previo (store_item 3068, item_data 44873, stripelog 35, store_order 26, noticia 50). La web responde 200 en todas las rutas y la portada carga las noticias sin errores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
209 lines
5.9 KiB
TypeScript
209 lines
5.9 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
import type { SessionData } from './session'
|
|
import { getZoneName, getClassCss, getCharacterImage, getRaceName, getClassName } from './character-info'
|
|
import { PD_PER_UNIT } from './dpoints'
|
|
|
|
// Rango de la cuenta según los PD (14 niveles). Umbral = PD mínimos por nivel.
|
|
const RANK_FILES = [
|
|
'1_Newbie', '2_Rookie', '3_Apprentice', '4_Explorer', '5_Contributor', '6_Enthusiast',
|
|
'7_Collaborator', '8_Regular', '9_RisingStar', '10_Proficient', '11_Experienced',
|
|
'12_Mentor', '13_Veteran', '14_GrandMaster',
|
|
]
|
|
const RANK_MIN = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 5000, 10000, 10001]
|
|
|
|
/** Nivel de rango (1-14) e imagen según los PD (dp). */
|
|
export function getAccountRank(dp: number): { level: number; image: string } {
|
|
let idx = 0
|
|
for (let i = 0; i < RANK_MIN.length; i++) if (dp >= RANK_MIN[i]) idx = i
|
|
return { level: idx + 1, image: `/nw-themes/nw-ryu/nw-images/nw-ranks/${RANK_FILES[idx]}.svg` }
|
|
}
|
|
|
|
export interface Character {
|
|
name: string
|
|
level: number
|
|
gold: number
|
|
silver: number
|
|
copper: number
|
|
race: number
|
|
classId: number
|
|
className: string
|
|
raceName: string
|
|
classCss: string
|
|
zone: string
|
|
imageUrl: string
|
|
}
|
|
|
|
export interface AccountDashboard {
|
|
bnetEmail: string
|
|
gameAccount: string
|
|
regMail: string
|
|
email: string
|
|
joindate: string
|
|
lastIp: string
|
|
lastAttemptIp: string
|
|
dp: number
|
|
vp: number
|
|
donatedPD: number
|
|
battlepayCredits: number
|
|
securityTokenDate: string | null
|
|
isBanned: boolean
|
|
unbanDate: string | null
|
|
isRecruited: boolean
|
|
recruitedCount: number
|
|
characters: Character[]
|
|
}
|
|
|
|
/** Datos del panel de cuenta. Resiliente si las BD de AzerothCore no están pobladas. */
|
|
export async function getAccountDashboard(session: SessionData, locale = 'es'): Promise<AccountDashboard> {
|
|
const accountId = session.accountId ?? 0
|
|
|
|
let dp = 0
|
|
let vp = 0
|
|
try {
|
|
const [p] = await db(DB.default).query<RowDataPacket[]>(
|
|
'SELECT dp, vp FROM api_points WHERE accountID = ?',
|
|
[accountId],
|
|
)
|
|
if (p[0]) {
|
|
dp = p[0].dp
|
|
vp = p[0].vp
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Total donado (en PD) para el rango: suma de TODOS los pagos confirmados
|
|
// (Stripe mode test/live + SumUp mode sumup), convertidos a PD. Cuenta todas
|
|
// las pasarelas y donaciones mixtas, y NO baja al gastar PD (a diferencia del
|
|
// saldo). El rango usará el máximo entre esto y el saldo actual.
|
|
let donatedPD = 0
|
|
try {
|
|
const [d] = await db(DB.default).query<RowDataPacket[]>(
|
|
"SELECT COALESCE(SUM(amount), 0) AS total FROM stripelog WHERE account_id = ? AND fulfilled = 1 AND mode IN ('sumup', 'test', 'live')",
|
|
[accountId],
|
|
)
|
|
donatedPD = Math.round(Number(d[0]?.total ?? 0) * PD_PER_UNIT)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let battlepayCredits = 0
|
|
try {
|
|
const [c] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT battlePayCredits FROM battlenet_accounts WHERE id = ?',
|
|
[session.bnetId ?? 0],
|
|
)
|
|
if (c[0]?.battlePayCredits != null) battlepayCredits = c[0].battlePayCredits
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Datos de la tabla account (correos, fecha, IPs, reclutador).
|
|
let regMail = ''
|
|
let email = ''
|
|
let joindate = ''
|
|
let lastIp = ''
|
|
let lastAttemptIp = ''
|
|
let isRecruited = false
|
|
try {
|
|
const [a] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT reg_mail, email, last_ip, last_attempt_ip, joindate, recruiter FROM account WHERE id = ?',
|
|
[accountId],
|
|
)
|
|
if (a[0]) {
|
|
regMail = a[0].reg_mail || ''
|
|
email = a[0].email || ''
|
|
lastIp = a[0].last_ip || ''
|
|
lastAttemptIp = a[0].last_attempt_ip || ''
|
|
isRecruited = Boolean(a[0].recruiter)
|
|
if (a[0].joindate) joindate = new Date(a[0].joindate).toLocaleString('es-ES')
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Estado de baneo (account_banned activo).
|
|
let isBanned = false
|
|
let unbanDate: string | null = null
|
|
try {
|
|
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT unbandate FROM account_banned WHERE id = ? AND active = 1 LIMIT 1',
|
|
[accountId],
|
|
)
|
|
if (bn[0]) {
|
|
isBanned = true
|
|
unbanDate = new Date(Number(bn[0].unbandate) * 1000).toLocaleString('es-ES')
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Nº de amigos reclutados.
|
|
let recruitedCount = 0
|
|
try {
|
|
const [rc] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT COUNT(*) AS n FROM account WHERE recruiter = ?',
|
|
[accountId],
|
|
)
|
|
if (rc[0]) recruitedCount = Number(rc[0].n)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let securityTokenDate: string | null = null
|
|
try {
|
|
const [s] = await db(DB.default).query<RowDataPacket[]>(
|
|
'SELECT created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
[accountId],
|
|
)
|
|
if (s[0]) securityTokenDate = new Date(s[0].created_at).toISOString()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let characters: Character[] = []
|
|
try {
|
|
const [ch] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT name, level, money, race, class, zone, gender FROM characters WHERE account = ?',
|
|
[accountId],
|
|
)
|
|
characters = ch.map((r) => ({
|
|
name: r.name,
|
|
level: r.level,
|
|
gold: Math.floor(r.money / 10000),
|
|
silver: Math.floor((r.money % 10000) / 100),
|
|
copper: r.money % 100,
|
|
race: r.race,
|
|
classId: r.class,
|
|
className: getClassName(r.class, locale),
|
|
raceName: getRaceName(r.race, locale),
|
|
classCss: getClassCss(r.class),
|
|
zone: getZoneName(r.zone, locale),
|
|
imageUrl: getCharacterImage(r.class, r.race, r.gender),
|
|
}))
|
|
} catch {
|
|
/* la BD de personajes puede no estar poblada */
|
|
}
|
|
|
|
return {
|
|
bnetEmail: session.bnetEmail ?? '',
|
|
gameAccount: session.username ?? '',
|
|
regMail,
|
|
email,
|
|
joindate,
|
|
lastIp,
|
|
lastAttemptIp,
|
|
dp,
|
|
vp,
|
|
donatedPD,
|
|
battlepayCredits,
|
|
securityTokenDate,
|
|
isBanned,
|
|
unbanDate,
|
|
isRecruited,
|
|
recruitedCount,
|
|
characters,
|
|
}
|
|
}
|