El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Web

Cómo integrar el Cash Shop (WCoin) al sitio del servidor de MU

Conecta el Cash Shop del juego al sitio de tu servidor de MU Online gestionando el saldo de WCoin, WCoinP y Goblin Point con transacciones atómicas en SQL, panel de recarga y entrega segura de créditos.

GA Gabriel · Actualizado el 2 jul 2025 · ⏱ 26 min de lectura
Respuesta rápida

El Cash Shop es donde el servidor de MU Online transforma el engagement en ingresos, y el sitio es la puerta de entrada de ese flujo. Cuando un jugador recarga créditos en el panel web, espera abrir la tienda dentro del juego y ver el saldo ahí. Hacer que ese puente parezca instantáneo y, sobre todo

El Cash Shop es donde el servidor de MU Online transforma el engagement en ingresos, y el sitio es la puerta de entrada de ese flujo. Cuando un jugador recarga créditos en el panel web, espera abrir la tienda dentro del juego y ver el saldo ahí. Hacer que ese puente parezca instantáneo y, sobre todo, a prueba de fallas es lo que separa a un sistema de cash confiable de uno que genera quejas y perjuicios. En este tutorial vas a construir la integración entre el sitio y el Cash Shop cuidando lo que realmente importa: dónde vive el saldo en la base, cómo acreditarlo con seguridad transaccional y cómo evitar créditos duplicados o perdidos. Los nombres de columnas y tablas varían según la versión del emulador, así que trata cada fragmento de código como un ejemplo a adaptar a tu schema.

Antes de tocar cualquier cosa, es importante entender que el Cash Shop del cliente MU no habla con el sitio directamente. Lee un saldo numérico que está en la tabla de cuentas de la base. El sitio, por lo tanto, no "conversa" con la tienda: solo altera ese número. Toda la integración se resume en manipular ese saldo de forma correcta y en registrar cada movimiento. Esta claridad mental evita que busques APIs que no existen y te enfoques en la parte que de verdad importa, que es la base de datos.

Requisitos previos

Si el servidor todavía no está en línea, sigue primero la guía de cómo crear un servidor de MU Online. Con el servidor listo, necesitas:

  • SQL Server con la base MuOnline y acceso a la tabla de cuentas (MEMB_INFO o equivalente de tu versión).
  • PHP 7.4 o superior con la extensión sqlsrv/pdo_sqlsrv habilitada.
  • Sistema de login del sitio ya funcionando, con la sesión del jugador autenticada.
  • Usuario SQL específico del sitio con permiso de lectura y escritura solo en las columnas de saldo y en las tablas de log, nunca con db_owner.
  • Conocimiento de las columnas de moneda de tu versión: WCoinC, WCoinP, GoblinPoint o los nombres equivalentes.
  • Un gateway de pago ya elegido (el crédito solo debe liberarse tras la confirmación del pago).

Confirma antes que nada qué columnas de moneda existen en tu base. Una consulta rápida evita dolores de cabeza:

SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MEMB_INFO'
  AND COLUMN_NAME IN ('WCoinC', 'WCoinP', 'GoblinPoint', 'GameIDC');

Si las columnas no aparecen con esos nombres, búscalas en la documentación de tu emulador. Alterar la columna equivocada hace que el saldo se sume en un lugar que la tienda del juego no lee.

Dónde vive el saldo de moneda

Las tres monedas más comunes del Cash Shop están, en la mayoría de las versiones, en la propia tabla de cuentas. Entender el papel de cada una orienta lo que el sitio debe acreditar.

MonedaColumna típicaUso comúnOrigen en el sitio
WCoin / WCoinCWCoinCMoneda principal comprableRecarga pagada
WCoinPWCoinPMoneda premium o de bonusBonus, promociones
Goblin PointGoblinPointMoneda de eventos y tiendas alternativasRecompensas, eventos

El Cash Shop dentro del juego lee esos campos cuando el jugador abre la tienda. Esto significa que el trabajo del sitio es, esencialmente, ejecutar un UPDATE seguro en esas columnas y registrar la operación. Toda la complejidad está en hacer ese UPDATE de forma que nunca acredite de más ni de menos, incluso frente a fallas de red, clics dobles y webhooks repetidos.

Modelado de las tablas de control

Nunca acredites directo sin registrar. Crea dos tablas de apoyo: una para el historial de transacciones y otra que sirve de traba de idempotencia. En muchos casos una única tabla bien modelada cumple los dos papeles.

CREATE TABLE CashShop_Transacao (
    Id            BIGINT IDENTITY(1,1) PRIMARY KEY,
    ContaId       VARCHAR(10)  NOT NULL,
    Moeda         VARCHAR(20)  NOT NULL,   -- 'WCoinC', 'WCoinP', 'GoblinPoint'
    Quantidade    INT          NOT NULL,
    SaldoAntes    INT          NOT NULL,
    SaldoDepois   INT          NOT NULL,
    Origem        VARCHAR(40)  NOT NULL,   -- 'mercadopago', 'admin', 'bonus'
    GatewayRef    VARCHAR(80)  NULL,       -- ID del pago (idempotencia)
    CriadoEm      DATETIME     NOT NULL DEFAULT GETDATE()
);
GO

-- El índice único garantiza que el mismo pago nunca acredite dos veces
CREATE UNIQUE INDEX UX_CashShop_GatewayRef
ON CashShop_Transacao (GatewayRef)
WHERE GatewayRef IS NOT NULL;
GO

El índice único filtrado es la pieza central de la seguridad. Vuelve físicamente imposible grabar dos transacciones con el mismo GatewayRef. Si un webhook llega duplicado, la segunda inserción falla en la base, y tu código trata ese error como "ya procesado". No necesitas confiar solo en la lógica de la aplicación; la base impone la regla.

Guardar SaldoAntes y SaldoDepois parece redundante, pero es oro para la auditoría. Cuando un jugador abre una disputa diciendo que no recibió, tienes el valor exacto antes y después de cada operación, con sello de tiempo. Esto resuelve quejas en segundos.

El procedimiento de crédito con transacción

El corazón del sistema es la operación de acreditar. Necesita ser atómica: leer el saldo, sumar, grabar el nuevo saldo y registrar la transacción, todo dentro de una única transacción SQL. Una stored procedure encapsula esta lógica y la vuelve reutilizable.

CREATE PROCEDURE dbo.CreditarWCoin
    @ContaId    VARCHAR(10),
    @Quantidade INT,
    @Origem     VARCHAR(40),
    @GatewayRef VARCHAR(80) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;      -- cualquier error revierte la transacción

    BEGIN TRAN;

    -- Traba la fila de la cuenta para una lectura consistente
    DECLARE @saldoAntes INT;
    SELECT @saldoAntes = WCoinC
    FROM MEMB_INFO WITH (UPDLOCK, ROWLOCK)
    WHERE memb___id = @ContaId;

    IF @saldoAntes IS NULL
    BEGIN
        ROLLBACK TRAN;
        THROW 50001, 'Conta inexistente', 1;
    END

    -- Idempotencia: si ya fue procesado, aborta silenciosamente
    IF @GatewayRef IS NOT NULL AND EXISTS (
        SELECT 1 FROM CashShop_Transacao WHERE GatewayRef = @GatewayRef)
    BEGIN
        ROLLBACK TRAN;
        RETURN;   -- ya acreditado antes
    END

    UPDATE MEMB_INFO
    SET WCoinC = WCoinC + @Quantidade
    WHERE memb___id = @ContaId;

    INSERT INTO CashShop_Transacao
        (ContaId, Moeda, Quantidade, SaldoAntes, SaldoDepois, Origem, GatewayRef)
    VALUES
        (@ContaId, 'WCoinC', @Quantidade, @saldoAntes,
         @saldoAntes + @Quantidade, @Origem, @GatewayRef);

    COMMIT TRAN;
END
GO

Tres detalles hacen que esta procedure sea segura. El XACT_ABORT ON garantiza que cualquier error revierte todo automáticamente. El WITH (UPDLOCK, ROWLOCK) en la lectura del saldo traba la fila de la cuenta para que dos ejecuciones simultáneas no lean el mismo saldo y se sobrescriban una a la otra, lo que causaría pérdida de crédito. Y el chequeo de GatewayRef da la segunda capa de idempotencia, complementando el índice único. Con esto, incluso dos webhooks llegando en el mismo milisegundo resultan en un único crédito.

Capa PHP que llama al crédito

En PHP, la llamada queda limpia porque toda la complejidad está en la procedure. El papel de PHP es validar quién está pidiendo y pasar los parámetros con binding.

<?php
// lib/cashshop.php
require_once __DIR__ . '/../config/db.php';

function creditarWCoin(string $contaId, int $qtd, string $origem, ?string $ref = null): bool {
    if ($qtd <= 0) return false;

    $conn = getDB();
    $sql = "{CALL dbo.CreditarWCoin(?, ?, ?, ?)}";
    $params = [
        [$contaId, SQLSRV_PARAM_IN],
        [$qtd,     SQLSRV_PARAM_IN],
        [$origem,  SQLSRV_PARAM_IN],
        [$ref,     SQLSRV_PARAM_IN],
    ];

    $stmt = sqlsrv_query($conn, $sql, $params);
    if ($stmt === false) {
        error_log('Erro ao creditar WCoin: ' . print_r(sqlsrv_errors(), true));
        return false;
    }
    return true;
}

El uso de parámetros vinculados (?) es innegociable. Nunca armes la llamada concatenando el $contaId o la cantidad directamente en la cadena, porque eso abre la puerta a la inyección de SQL. Con el binding, el driver trata cada valor como dato, no como comando, y el ataque simplemente no funciona.

Panel de recarga en el sitio

El panel que ve el jugador organiza los paquetes de créditos y lleva al pago. El crédito en sí solo se libera después de que el gateway confirma, nunca en el clic del botón.

<?php
// recarga.php
session_start();
require_once __DIR__ . '/../lib/cashshop.php';

if (empty($_SESSION['conta_mu'])) {
    header('Location: /login');
    exit;
}

$pacotes = [
    'p1' => ['wcoin' => 1000,  'preco' => 'R$ 10,00'],
    'p2' => ['wcoin' => 2500,  'preco' => 'R$ 20,00', 'bonus' => 300],
    'p3' => ['wcoin' => 6000,  'preco' => 'R$ 40,00', 'bonus' => 1000],
];

// Consultar el saldo actual para mostrarlo
$conn = getDB();
$stmt = sqlsrv_query($conn,
    "SELECT WCoinC, WCoinP FROM MEMB_INFO WHERE memb___id = ?",
    [[$_SESSION['conta_mu'], SQLSRV_PARAM_IN]]);
$saldo = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
?>
<h1>Recarga de WCoin</h1>
<p>Saldo actual: <strong><?= (int)$saldo['WCoinC'] ?> WCoin</strong></p>

<div class="pacotes">
<?php foreach ($pacotes as $id => $p): ?>
  <div class="pacote">
    <h3><?= number_format($p['wcoin'], 0, ',', '.') ?> WCoin</h3>
    <?php if (!empty($p['bonus'])): ?>
      <span class="bonus">+<?= $p['bonus'] ?> de bonus</span>
    <?php endif; ?>
    <p class="preco"><?= $p['preco'] ?></p>
    <a href="/pagamento?pacote=<?= $id ?>" class="btn">Comprar</a>
  </div>
<?php endforeach; ?>
</div>

El botón lleva al flujo de pago, no al crédito. Esta separación es una regla de seguridad fundamental: el crédito sucede exclusivamente en el callback del gateway, cuando tienes certeza de que el dinero entró. Acreditar en el clic permitiría a cualquiera ganar WCoin gratis con solo llamar la URL.

Entrega del crédito tras la confirmación del pago

Cuando el gateway confirma el pago, llama a tu endpoint de callback. Es ahí donde sucede el crédito, usando el ID del pago como clave de idempotencia.

<?php
// callback-pagamento.php  (llamado por el gateway, no por el jugador)
require_once __DIR__ . '/../lib/cashshop.php';

// 1) Validar la autenticidad de la notificación (firma/token del gateway)
//    La forma varía según el gateway; NUNCA acredites sin validar.

// 2) Recuperar los datos confirmados
$pagamentoId = $notificacao['id'];        // ID único del gateway
$statusPago  = $notificacao['status'] === 'approved';
$contaId     = $notificacao['metadata']['conta'];
$wcoin       = (int)$notificacao['metadata']['wcoin'];

// 3) Acreditar solo si fue aprobado, usando el ID como referencia
if ($statusPago) {
    $ok = creditarWCoin($contaId, $wcoin, 'gateway', $pagamentoId);
    // Si el webhook se repite, el índice único bloquea el segundo crédito
    http_response_code($ok ? 200 : 500);
} else {
    http_response_code(200); // reconoce la notificación aun sin acreditar
}

Nota que el $pagamentoId se vuelve el GatewayRef de la transacción. Si el gateway reenvía la notificación, algo común y esperado, el segundo intento choca con el índice único y no acredita de nuevo. Esa es la protección que impide que un reintento de red se vuelva WCoin duplicado.

Cuándo ve el jugador el saldo en el juego

Un punto que genera muchas dudas de soporte: acredité en la base, ¿por qué el jugador no lo ve? La respuesta depende de cómo el Cash Shop lee el saldo, y eso varía según la versión. En general hay dos comportamientos:

  1. Lectura directa de la base cada vez que se abre la tienda: el jugador solo necesita cerrar y reabrir el Cash Shop. Es el caso más común y el más cómodo.
  2. Saldo en cache en el GameServer: el servidor mantiene el valor en memoria y solo lo recarga al loguear. Aquí el jugador necesita reloguear para ver el crédito.

Deja esto claro en la pantalla de éxito de la recarga, algo como "tu WCoin fue acreditado; reabre la tienda o reloguea para verlo". Un aviso simple elimina la mitad de los tickets de soporte. Si tu emulador ofrece un comando o puerto de recarga en tiempo real en el GameServer, integrar por ahí entrega la mejor experiencia, pero no es obligatorio para un sistema funcional.

Seguridad y buenas prácticas

El sistema de cash maneja dinero real, así que merece un rigor extra. Además del binding de parámetros y la idempotencia ya cubiertos, aplica estos principios:

  • Menor privilegio en la base: el usuario SQL del sitio solo necesita EXECUTE en la procedure de crédito y SELECT en las columnas de saldo, nada más.
  • Validación de la notificación del gateway: valida la firma o el token de toda notificación de pago; sin eso, cualquiera puede forjar un callback y ganar créditos.
  • Log completo e inmutable: nunca permitas DELETE ni UPDATE en la tabla de transacciones al usuario del sitio; es un libro mayor.
  • Límites de sanidad: rechaza cantidades absurdas de WCoin por transacción; un valor muy por encima del paquete mayor es señal de manipulación.
  • HTTPS obligatorio: todo el flujo de recarga y callback tiene que viajar bajo TLS.

Errores comunes y soluciones

SíntomaCausa probableSolución
El crédito no aparece en el juegoEl Cash Shop lee el saldo en cachePide al jugador que reloguee; documéntalo en la pantalla
El jugador recibió WCoin duplicadoWebhook duplicado sin idempotenciaAgrega el índice único en GatewayRef y el chequeo en la procedure
El saldo se sumó en la columna equivocadaColumna de moneda incorrectaConfirma el nombre real de la columna en el schema antes de acreditar
Crédito liberado sin pagoCrédito en el clic en vez del callbackMueve el crédito al callback validado del gateway
Error de deadlock bajo concurrenciaOrden de trabas inconsistenteUsa UPDLOCK, ROWLOCK y mantén la transacción corta
sqlsrv_query devuelve false al acreditarPermiso insuficienteConcede EXECUTE en la procedure al usuario del sitio
Alguien forjó una notificaciónCallback sin validación de firmaValida el token/firma de toda notificación antes de acreditar

Lista de verificación de lanzamiento

  • Columnas de moneda (WCoinC, WCoinP, GoblinPoint) confirmadas en el schema real
  • Tabla CashShop_Transacao creada con SaldoAntes y SaldoDepois
  • Índice único en GatewayRef funcionando
  • Stored procedure CreditarWCoin con XACT_ABORT y UPDLOCK
  • Usuario SQL del sitio con solo el EXECUTE y SELECT necesarios
  • PHP usando parámetros vinculados en toda llamada
  • Crédito sucediendo solo en el callback validado del gateway
  • Validación de firma/token de la notificación de pago implementada
  • Pantalla de éxito avisando sobre reabrir la tienda o reloguear
  • Límites de sanidad por transacción configurados
  • Todo el flujo bajo HTTPS
  • Prueba de webhook duplicado confirmando un crédito único
  • Prueba de cuenta inexistente devolviendo un error tratado

Con esta base, el Cash Shop de tu sitio acredita moneda de forma confiable, audita cada centavo y resiste las fallas de red que inevitablemente suceden. La lección que atraviesa todo es la misma: el crédito es dinero, así que trata cada operación como una transacción bancaria, con atomicidad, idempotencia y registro completo.

Preguntas frecuentes

¿Cuál es la diferencia entre WCoin, WCoinP y Goblin Point?

WCoin (también llamado WCoinC) es la moneda de créditos comprable, WCoinP es la moneda premium o de bonus, y Goblin Point es una moneda alternativa usada en algunos eventos y tiendas. El nombre y la columna exacta varían según la versión, pero las tres están en la tabla de cuentas y las lee el Cash Shop dentro del juego.

¿El crédito agregado en el sitio aparece al instante en el juego?

Depende de dónde el Cash Shop lea el saldo. Si lo lee directo de la tabla de cuentas cada vez que se abre la tienda, el jugador solo necesita reabrir el Cash Shop. Algunos servidores mantienen el saldo en cache en el GameServer, y en ese caso el jugador necesita reloguear para actualizar.

¿Por qué necesito una transacción SQL para agregar créditos?

Porque agregar crédito implica leer el saldo, sumar y grabar, además de registrar el log de la compra. Si el proceso falla a la mitad sin transacción, el jugador puede recibir crédito sin registro o pagar sin recibir. La transacción garantiza que o todo sucede, o nada sucede.

¿Cómo evito que alguien reciba crédito dos veces por el mismo pago?

Usa una clave de idempotencia única por transacción, normalmente el ID del pago del gateway, grabada en una tabla de control con índice único. Antes de acreditar, verifica si ese ID ya fue procesado; si ya lo fue, ignóralo. Esto neutraliza los webhooks duplicados y las recargas de página.

¿Necesito una columna nueva en la tabla de cuentas para el WCoin?

En la mayoría de las versiones la columna ya existe, con un nombre como WCoinC, WCoinP o GameIDC según el emulador. Confirma el schema antes de crear cualquier cosa; crear una columna duplicada puede hacer que el Cash Shop del juego lea el saldo equivocado.

GA
Editor de guías y builds

Gabriel cubre gameplay, builds de clases, PvP y progresión. Prueba cada estrategia en un servidor antes de publicar.

Sigue leyendo

Artículos relacionados