MatchMaster/assets/Script/coin_madness/GoldMinerService.ts

454 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Utils from '../module/Pay/Utils';
declare const wx: any;
/** Resident protocol client; contains no activity bundle assets. */
export default class GoldMinerService {
private static clients: { [key: string]: GoldMinerService } = {};
static enabled() { return !CC_PREVIEW && typeof wx !== 'undefined'; }
static testEnabled(): boolean {
if (!this.enabled()) return false;
const version = cc.fx.GameTool.getWechatGameVersion();
const normalize = value => String(value || '').replace(/\/+$/, '');
return ['开发版', '体验版'].indexOf(version) >= 0 && !!Utils.testHttpip
&& normalize(Utils.serverUrl()) === normalize(Utils.testHttpip)
&& normalize(Utils.serverUrl()) !== normalize(Utils.httpip);
}
static current(): GoldMinerService {
if (!this.enabled()) return null;
const user = cc.fx.GameConfig.GM_INFO;
if (!user.uid || !user.token) return null;
const key = Utils.serverUrl() + ':' + user.uid;
return this.clients[key] || (this.clients[key] = new GoldMinerService(String(user.uid), key));
}
info: any = null;
schedule: any = null;
private settlementCursor: string = null;
message = '';
refreshNeedsRetry = false;
get serverNow() { return Date.now() + this.offset; }
private offset = 0;
private tail: Promise<any> = Promise.resolve();
private refreshing: Promise<any> = null;
private displayReady: Promise<any> = null;
private ledger: any;
private polling = 0;
private constructor(private uid: string, private scope: string) {
const raw = cc.sys.localStorage.getItem('goldMiner:v1:' + scope);
this.ledger = raw ? JSON.parse(raw) : { orders: {}, grants: {}, events: {}, notices: {}, sequence: 0 };
cc.game.on(cc.game.EVENT_HIDE, () => this.stopPolling());
}
private active() { return String(cc.fx.GameConfig.GM_INFO.uid) === this.uid && Utils.serverUrl() + ':' + this.uid === this.scope; }
private persist() { cc.sys.localStorage.setItem('goldMiner:v1:' + this.scope, JSON.stringify(this.ledger)); }
private id(prefix: string) { return prefix + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 12); }
private serial<T>(work: () => Promise<T>): Promise<T> {
const result = this.tail.then(work);
this.tail = result.catch(() => undefined);
return result;
}
private async post(path: string, body: any, authenticated = true): Promise<any> {
if (!this.active()) throw new Error('账号已切换,请重新打开活动');
const startedAt = Date.now(), url = Utils.serverUrl() + path;
let response: any;
const request = authenticated ? { ...body, uid: this.uid } : body;
this.iosPaymentLog('request', { url, request });
try { response = await new Promise(resolve => Utils.POSTJSON(path, request, resolve, authenticated)); }
catch (error) {
this.iosPaymentLog('request exception', { url, code: error.code, errCode: error.errCode, message: error.message });
throw error;
}
this.iosPaymentLog('response', { url, action: body && body.action, durationMs: Date.now() - startedAt,
code: response && response.code, errorCode: response && response.errorCode, response });
if (!this.active()) throw new Error('账号已切换,请重新打开活动');
if (!response || response.code !== 1) {
const error: any = new Error(response && (response.msg || response.message || response.errorCode) || '网络请求失败,请重试');
error.code = response && response.errorCode;
// The deployed query endpoint uses this response for unconfirmed payment.
if (!error.code && (path === 'wx/getPayInfo' || path === 'wx/iosgetPayInfo')
&& response && response.code === 0 && response.data && response.data.pay_state === 1
&& response.msg === '充值未成功') error.code = 'PAYMENT_PENDING';
if (error.code === 'PAYMENT_PENDING') error.paymentPending = true;
throw error;
}
return response.data;
}
testAvailable() { return GoldMinerService.testEnabled() && this.active(); }
private iosPaymentLog(event: string, detail: any) {
try {
if (typeof wx === 'undefined' || wx.getSystemInfoSync().platform !== 'ios') return;
const entry = '[GoldMiner iOS] ' + event + ' ' + JSON.stringify(this.testSafe({ time: new Date().toISOString(), ...detail }), null, 2);
console.log(entry);
} catch (_) { /* Diagnostics must never interrupt payment. */ }
}
private testSafe(value: any): any {
if (Array.isArray(value)) return value.map(item => this.testSafe(item));
if (value && typeof value === 'object') {
const result = {};
Object.keys(value).forEach(key => result[key] = /token|signature|paySig|signData|sessionFrom|session_key/i.test(key) ? '[已隐藏]' : this.testSafe(value[key]));
return result;
}
return value;
}
private activity(action: string, body: any = {}) { return this.post('goldMiner/index', { ...body, action }); }
private async readSchedule() {
const data = await this.post('activityConfig/list', {}, false);
if (!data || data.schemaVersion !== 1 || !Array.isArray(data.activities) || !Number.isFinite(data.serverNow)) throw new Error('活动日程数据异常');
this.schedule = data;
this.offset = data.serverNow - Date.now();
return data;
}
private async readInfo() {
const info = await this.activity('info');
if (!info || !Array.isArray(info.tasks) || !Number.isFinite(info.serverTime)
|| ['locked', 'unavailable', 'purchasable', 'purchase_disabled', 'unlocked'].indexOf(info.status) < 0) throw new Error('活动数据异常,请稍后重试');
this.info = info; this.offset = info.serverTime - Date.now();
return info;
}
/** Adapt the server snapshot to the existing prefab's view model. */
view(granted = 0): any {
const info = this.info;
if (!info) return null;
const stages = info.tasks.map(task => ({ taskId: task.taskId, wins: task.targetWins,
coins: task.itemsSnapshot.filter(item => item.type === 'coin').reduce((sum, item) => sum + item.count, 0),
claimStatus: task.claimStatus }));
const claimed = info.tasks.map(task => task.claimStatus === 'claimed');
const next = claimed.indexOf(false);
return { serverNow: Date.now() + this.offset, granted, status: info.status, state: {
config: { periodId: info.periodId, startsAt: info.startsAt, endsAt: info.endsAt,
priceCents: info.priceFen, stages },
totalWins: info.progressWins || 0, paidAt: info.status === 'unlocked' ? 1 : 0,
claimed, stage: next < 0 ? stages.length : next, settled: false,
} };
}
get settlementNotice() { return Object.keys(this.ledger.notices).reduce((sum, id) => sum + this.ledger.notices[id], 0); }
dismissNotice() { this.ledger.notices = {}; this.persist(); }
get hasBadge() {
return this.settlementNotice > 0 || !!(this.info && this.info.tasks.some(t =>
t.claimStatus !== 'claimed' && (t.claimStatus === 'claimable' || t.claimStatus === 'issuing'
|| t.claimStatus === 'pending_unlock' || this.info.progressWins >= t.targetWins)));
}
stopPolling() { this.polling++; }
/** First validated snapshot; recovery continues on the existing serialized queue. */
readForDisplay(): Promise<any> {
this.refresh();
return this.displayReady;
}
refresh(): Promise<any> {
if (this.refreshing) return this.refreshing;
let show: (data: any) => void, fail: (error: any) => void;
this.displayReady = new Promise((resolve, reject) => { show = resolve; fail = reject; });
// Homepage refreshes need not consume the early snapshot.
this.displayReady.catch(() => undefined);
this.refreshing = this.serial(async () => {
this.message = '';
this.refreshNeedsRetry = false;
await this.readInfo();
show(this.view());
try { await this.readSchedule(); } catch (error) { this.message = this.friendly(error); }
for (const period of Object.keys(this.ledger.orders)) {
const order = this.ledger.orders[period];
if (order.outTradeNo && !order.done && order.paymentState !== 'cancelled') {
try { await this.queryOrder(order); } catch (error) { this.refreshNeedsRetry = true; this.message = this.friendly(error); }
}
}
for (const id of Object.keys(this.ledger.grants)) {
const grant = this.ledger.grants[id];
if (grant.phase !== 'confirmed' && !grant.settlementId && this.info.periodId === grant.periodId
&& Date.now() + this.offset < this.info.endsAt) {
try { await this.deliver(grant.periodId, grant.taskId); } catch (error) { this.message = this.friendly(error); }
}
}
// Recover issuing grants even if the page was closed immediately after claim.
for (const task of this.info.tasks) {
if (task.claimStatus === 'issuing') {
try { await this.deliver(this.info.periodId, task.taskId); } catch (error) { this.message = this.friendly(error); }
}
}
try { await this.settle(); } catch (error) { this.refreshNeedsRetry = true; this.message = this.friendly(error); }
await this.readInfo();
return this.view();
});
this.refreshing.then(() => this.refreshing = null, error => { fail(error); this.refreshing = null; });
return this.refreshing;
}
/** Retry recovery without reloading the activity schedule or replaying ordinary claims. */
retryRecovery(): Promise<any> {
return this.serial(async () => {
this.refreshNeedsRetry = false;
this.message = '';
let changed = false;
for (const period of Object.keys(this.ledger.orders)) {
const order = this.ledger.orders[period];
if (order.outTradeNo && !order.done && order.paymentState !== 'cancelled') {
try { await this.queryOrder(order); changed = true; }
catch (error) { this.refreshNeedsRetry = true; this.message = this.friendly(error); }
}
}
const notice = this.settlementNotice;
try { await this.settle(); }
catch (error) { this.refreshNeedsRetry = true; this.message = this.friendly(error); }
if (changed || notice !== this.settlementNotice) await this.readInfo();
return this.view();
});
}
async request(action: string, body: any = {}, onSaved?: (amount: number) => void): Promise<any> {
let purchaseNotified = false;
try {
if (action === 'read') return { code: 1, data: await this.refresh(), msg: this.message };
return await this.serial(async () => {
let granted = 0;
if (action === 'purchase') {
await this.purchase(body.periodId);
const order = this.ledger.orders[body.periodId];
const confirmed = !!(order && order.done) || !!(this.info && this.info.periodId === body.periodId && this.info.status === 'unlocked');
wx.showToast({ title: confirmed ? '购买成功' : '支付结果确认中', icon: confirmed ? 'success' : 'none', duration: 2000 });
purchaseNotified = true;
}
else if (action === 'claim') {
const task = this.info && this.info.tasks[body.stage];
if (!task || body.periodId !== this.info.periodId) throw new Error('活动已更新,请刷新');
granted = await this.deliver(body.periodId, task.taskId, undefined, undefined, onSaved);
} else throw new Error('不支持的活动操作');
await this.readInfo();
return { code: 1, data: this.view(granted), msg: this.message };
});
} catch (error) {
this.message = this.friendly(error);
this.iosPaymentLog('operation failed', { action, code: error.code, errCode: error.errCode, message: error.message });
if (action === 'purchase' && error.paymentPending) this.refreshNeedsRetry = true;
if (action === 'purchase' && !purchaseNotified && this.active()) {
wx.showToast({ title: error.code === 'PAYMENT_CANCELLED' ? '已取消支付' : error.paymentPending ? '支付结果确认中' : '购买失败', icon: 'none', duration: 2000 });
}
// Invalidate stale claim buttons after a rejected operation.
try { await this.readInfo(); } catch (_) { /* Preserve the error for the player. */ }
return { code: 0, data: this.view(), msg: this.message };
}
}
private friendly(error: any) {
const messages = { PAYMENT_PENDING: '支付结果确认中,请稍后刷新',
NOT_PAID: '请先解锁活动', PERIOD_ENDED: '本期活动已结束', PERIOD_SETTLEMENT_REQUIRED: '本期已结束,奖励将通过补发到账', PURCHASE_DISABLED: '本期暂不可购买',
PAYMENT_CHANNEL_CONFLICT: '请使用本期订单原有的支付渠道', UNAUTHORIZED: '登录已失效,请重新进入游戏',
TARGET_NOT_REACHED: '通关目标尚未达成', ORDER_CLOSED: '订单已关闭,请联系客服处理',
MANUAL_REVIEW: '订单需要核对,请联系客服,勿重复付款',
PAYMENT_CANCELLED: '已取消支付,再次点击解锁将重新下单' };
return messages[error.code] || error.message || '活动请求失败,请稍后重试';
}
private async purchase(periodId: string, fallbackToCustomerService = false) {
await this.readInfo();
if (this.info.periodId !== periodId || this.info.status !== 'purchasable') throw new Error('当前不可购买,请刷新活动');
if (!Number.isSafeInteger(this.info.priceFen) || this.info.priceFen <= 0) throw new Error('活动价格异常,请刷新活动');
// Each explicit purchase starts a fresh order request, regardless of the previous attempt.
const ios = wx.getSystemInfoSync().platform === 'ios';
const order = { createRequestId: this.id('buy'),
channel: ios && (fallbackToCustomerService || !cc.fx.GameConfig.GM_INFO.iosCanPay) ? 'legacy_ios' : 'native', ios } as any;
this.iosPaymentLog('purchase route', { periodId, ...order, iosCanPay: cc.fx.GameConfig.GM_INFO.iosCanPay, fallbackToCustomerService });
// Keep the native order recoverable until the server accepts the channel switch.
if (!fallbackToCustomerService) { this.ledger.orders[periodId] = order; this.persist(); }
const body = { itemid: this.info.productId, createRequestId: order.createRequestId,
itemCount: 1, itemPrice: this.info.priceFen };
let payment;
try {
payment = order.channel === 'legacy_ios'
? {}
: await this.post(order.ios ? 'wx/iosorderPaySig' : 'wx/orderPaySig', body);
} catch (error) {
if (error.code === 'ALREADY_UNLOCKED') { await this.readInfo(); return; }
if (fallbackToCustomerService && error.code === 'PAYMENT_CHANNEL_CONFLICT') {
throw new Error('当前 iOS 直付不可用,但服务端不允许将本期订单切换为客服支付,请联系客服处理');
}
throw error;
}
if (!payment || (order.channel !== 'legacy_ios' && !payment.outTradeNo)) throw new Error('订单数据异常');
this.ledger.orders[periodId] = order;
order.outTradeNo = payment.outTradeNo; this.persist();
const required = order.channel === 'legacy_ios' ? [] : ['signData', 'signature', 'paySig'];
if (required.some(key => typeof payment[key] !== 'string' || !payment[key].trim())) {
order.paymentState = 'failed'; this.persist();
throw new Error('下单响应缺少有效支付参数:' + required.filter(key => typeof payment[key] !== 'string' || !payment[key].trim()).join(', '));
}
const ticket = ++this.polling;
order.paymentState = 'pending'; this.persist();
try { await new Promise<void>((resolve, reject) => {
let done = false;
const api = order.channel === 'legacy_ios' ? 'openCustomerServiceConversation' : 'requestMidasPaymentGameItem';
this.iosPaymentLog('wx call', { api, outTradeNo: order.outTradeNo });
const finish = (ok: boolean, detail: any = {}) => {
if (done) return; done = true; clearTimeout(timer);
this.iosPaymentLog('wx callback', { api, outTradeNo: order.outTradeNo, success: ok, errCode: detail.errCode, response: detail });
if (ok) resolve();
else {
const cancelled = /cancel/i.test(detail.errMsg || '');
order.paymentState = cancelled ? 'cancelled' : 'failed'; this.persist();
const error: any = new Error('微信支付未拉起或未完成:' + (detail.errMsg || detail.message || '未知错误')
+ (detail.errCode != null ? '(' + detail.errCode + ')' : ''));
error.errCode = detail.errCode;
if (Number(detail.errCode) === 2860 || /PRODUCT ID NOT EXIST OR NOT PUBLISH/i.test(detail.errMsg || '')) {
error.message = '微信支付商品不存在或未发布(2860)。请核对微信支付后台的商品 ID、发布状态及支付环境。';
if (this.testAvailable()) {
error.message += '\n活动商品 ID:' + String(body.itemid);
try {
const signed = JSON.parse(payment.signData);
error.message += '\n签名商品 ID:' + String(signed.productId)
+ '\n支付环境 env:' + String(signed.env)
+ '\n支付应用 offerId:' + String(signed.offerId)
+ '\n分区 zoneId:' + String(signed.zoneId);
} catch (_) { /* Keep the original payment payload untouched. */ }
}
}
if (cancelled) error.code = 'PAYMENT_CANCELLED';
reject(error);
}
};
const timer = setTimeout(() => {
if (done) return; done = true;
this.iosPaymentLog('wx timeout', { api, outTradeNo: order.outTradeNo, timeoutMs: 120000 });
const error: any = new Error('支付结果确认中,请稍后刷新'); error.paymentPending = true; reject(error);
}, 120000);
const fail = (error: any) => finish(false, error || {});
try { if (order.channel === 'legacy_ios') {
Utils.GoKEFu({ price: this.info.priceFen, payment_name: this.info.productId, payment_count: 1 },
(result, detail) => result === 'success' ? finish(true, { result }) : fail(detail || { errMsg: '客服会话打开失败' }),
outTradeNo => {
order.outTradeNo = outTradeNo; this.persist();
this.iosPaymentLog('customer order created', { outTradeNo, productId: this.info.productId, priceFen: this.info.priceFen });
});
} else {
wx.requestMidasPaymentGameItem({ signData: payment.signData, signature: payment.signature, paySig: payment.paySig,
success: res => finish(true, res), fail });
} } catch (error) { fail(error); }
}); } catch (error) {
if (ios && order.channel === 'native' && Number(error.errCode) === 16
&& error.code !== 'PAYMENT_CANCELLED') {
this.iosPaymentLog('fallback to legacy_ios', { errCode: error.errCode, message: error.message, outTradeNo: order.outTradeNo });
return this.purchase(periodId, true);
}
throw error;
}
for (let attempt = 0; attempt < 4; attempt++) {
if (ticket !== this.polling) break;
try { await this.queryOrder(order); return; }
catch (error) {
if (error.code !== 'PAYMENT_PENDING' && error.code !== 'NETWORK_UNKNOWN') { error.paymentPending = true; throw error; }
}
if (attempt < 3) await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1500));
}
this.refreshNeedsRetry = true;
this.message = '支付结果确认中,回到游戏后会继续查询';
}
private async queryOrder(order: any) {
const data = await this.post(order.ios ? 'wx/iosgetPayInfo' : 'wx/getPayInfo', { outTradeNo: order.outTradeNo });
if (!data || data.pay_state !== 2 || data.rewardDelivery !== 'goldMiner.claim' || !data.goldMiner) {
const error: any = new Error('支付权益尚未确认');
error.paymentPending = true;
// Only the documented PAYMENT_PENDING response is eligible for rapid retries.
throw error;
}
order.done = true; order.route = data.goldMiner.fulfillmentRoute; order.targetPeriodId = data.goldMiner.targetPeriodId;
this.persist();
this.message = order.route === 'carry_next' ? '购买资格将在 ' + order.targetPeriodId.replace('goldMiner:', '') + ' 开始的一期生效' : '活动权益已确认';
}
private async deliver(periodId: string, taskId: string, settlementId?: string, reward?: any, onSaved?: (amount: number) => void): Promise<number> {
const key = periodId + ':' + taskId;
let record = this.ledger.grants[key];
const known = !!record;
if (!record) {
record = this.ledger.grants[key] = { periodId, taskId, requestId: this.id('claim'), phase: 'requested' };
this.persist();
}
const data = reward ? { ...reward, claimStatus: 'issuing', requiresClientDelivery: true }
: await this.activity('claim', { periodId, taskId, requestId: record.requestId });
if (data.claimStatus === 'claimed' || data.requiresClientDelivery === false) {
record.phase = 'confirmed'; this.persist(); return 0;
}
if (!data.grantId || !Array.isArray(data.items) || data.claimStatus !== 'issuing' || data.requiresClientDelivery !== true) throw new Error('奖励授权数据异常');
// A previously issuing grant without a local journal may have been paid on another installation.
if (!known && this.info && periodId === this.info.periodId
&& this.info.tasks.some(t => t.taskId === taskId && t.claimStatus === 'issuing')) record.phase = 'uncertain';
if (record.grantId && record.grantId !== data.grantId) throw new Error('奖励凭证不一致,请联系客服');
record.grantId = data.grantId; record.items = data.items;
if (settlementId && record.phase !== 'confirmed') record.settlementId = settlementId;
this.persist();
if (record.phase === 'saving' || record.phase === 'uncertain') throw new Error('奖励到账结果待核对,请联系客服,勿重复领取');
const amount = data.items.reduce((sum, item) => {
if (item.type !== 'coin' || !Number.isSafeInteger(item.count) || item.count <= 0) throw new Error('奖励类型暂不支持,请更新游戏或联系客服');
return sum + item.count;
}, 0);
let granted = 0;
if (record.phase !== 'saved' && record.phase !== 'confirmed') {
await Utils.withCoinSave(async () => {
if (!this.active()) throw new Error('账号已切换');
const balance = Math.floor(cc.fx.GameConfig.GM_INFO.coin) + amount;
if (!Number.isSafeInteger(balance) || balance < amount) throw new Error('金币余额异常');
record.phase = 'saving'; record.balance = balance;
if (settlementId) record.settlementId = settlementId;
this.persist();
cc.fx.GameConfig.GM_INFO.coin = balance;
cc.fx.StorageMessage.setStorage('coin', { coin: balance, timestamp: Date.now() });
const saved = await this.post('userCoin', { action: 'save', coinAmount: balance });
if (!saved || saved.coinAmount !== balance) throw new Error('金币保存结果待核对,请联系客服');
record.phase = 'saved'; record.credited = true; record.amount = amount; this.persist(); granted = amount;
// Presentation must not delay or prevent delivery confirmation.
if (onSaved) { try { onSaved(amount); } catch (error) { cc.warn('奖励动画启动失败', error); } }
});
}
if (settlementId) {
const confirmation = await this.activity('confirm_settlement_delivery', { settlementId, grantIds: [record.grantId] });
if (confirmation.settlementId !== settlementId || !Array.isArray(confirmation.confirmedGrantIds)
|| confirmation.confirmedGrantIds.indexOf(record.grantId) < 0) throw new Error('补发确认结果异常');
} else await this.activity('confirm_delivery', { periodId, taskId, grantId: record.grantId });
record.phase = 'confirmed'; this.persist();
return granted;
}
private async settle() {
let cursor: string = this.settlementCursor;
const seen: string[] = [];
do {
const data = await this.activity('settlements', { limit: 100, ...(cursor ? { afterId: cursor } : {}) });
if (data.status === 'settling') { this.refreshNeedsRetry = true; this.settlementCursor = cursor; this.message = '活动奖励结算中,请稍后刷新'; return; }
for (const settlement of data.items) {
if (!settlement.settlementId || !Array.isArray(settlement.rewards)) throw new Error('补发数据异常');
for (const reward of settlement.rewards) {
await this.deliver(settlement.periodId, reward.taskId, settlement.settlementId, reward);
const record = this.ledger.grants[settlement.periodId + ':' + reward.taskId];
if (record.credited && record.settlementId === settlement.settlementId && !record.notified) {
this.ledger.notices[settlement.settlementId] = (this.ledger.notices[settlement.settlementId] || 0) + record.amount;
record.notified = true; this.persist();
}
}
}
cursor = data.nextCursor;
if (cursor && seen.indexOf(cursor) >= 0) throw new Error('补发分页异常');
if (cursor) seen.push(cursor);
} while (cursor);
this.settlementCursor = null;
}
/** Attach once to the existing victory save. Never replay the non-idempotent legacy save on login. */
victory(level: number, endless: boolean): any {
if (!this.active()) return null;
const now = Date.now() + this.offset;
// Use server-issued IDs verbatim, including temporary periods on any weekday.
const scheduled = this.schedule && this.schedule.activities.find(a => a.activityId === 'goldMiner'
&& a.startsAt <= now && now < a.endsAt);
const periodId = scheduled ? scheduled.periodId : this.info && this.info.startsAt <= now
&& now < this.info.endsAt ? this.info.periodId : null;
if (!periodId) return null;
const event: any = { periodId, eventId: this.id('win'), outcome: 'win', mode: endless ? 'endless' : 'main' };
if (endless) {
// Millisecond time plus random suffix reduces cross-device collisions; persisted monotonic floor.
this.ledger.sequence = Math.max(this.ledger.sequence + 1, Date.now() * 1000 + Math.floor(Math.random() * 1000));
event.endlessSequence = this.ledger.sequence;
} else event.clearedMainLevel = level;
this.ledger.events[event.eventId] = { event, level, state: 'sent', sentAt: Date.now() }; this.persist();
return event;
}
victoryResult(event: any, response: any) {
if (!event || !this.active()) return;
const result = response && response.data && response.data.goldMiner;
const record = this.ledger.events[event.eventId];
if (result && result.code === 1) {
delete this.ledger.events[event.eventId];
if (this.info && this.info.periodId === event.periodId) this.info.progressWins = result.progressWins;
} else if (record) record.state = result && result.errorCode || 'unknown';
this.persist();
}
}