first commit
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* ComplianceManager.js
|
||||
* Manages underage protection, probability disclosure, and anti-cheat measures.
|
||||
* Ensures compliance with Chinese gaming regulations and WeChat platform rules.
|
||||
*/
|
||||
|
||||
class ComplianceManager {
|
||||
constructor() {
|
||||
this._isMinor = false;
|
||||
this._monthlySpending = 0;
|
||||
this._dailyAdCount = 0;
|
||||
this._trackingDate = '';
|
||||
|
||||
this._load();
|
||||
this._checkMinorStatus();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Persistence
|
||||
// ============================================================
|
||||
|
||||
/** @private */
|
||||
_getTodayKey() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_getMonthKey() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_load() {
|
||||
try {
|
||||
if (GameGlobal && GameGlobal.storageManager) {
|
||||
const data = GameGlobal.storageManager.get('compliance', null);
|
||||
if (data) {
|
||||
this._isMinor = data.isMinor || false;
|
||||
|
||||
// Monthly spending
|
||||
const currentMonth = this._getMonthKey();
|
||||
if (data.spendingMonth === currentMonth) {
|
||||
this._monthlySpending = data.monthlySpending || 0;
|
||||
}
|
||||
|
||||
// Daily ad count
|
||||
const today = this._getTodayKey();
|
||||
if (data.adDate === today) {
|
||||
this._dailyAdCount = data.dailyAdCount || 0;
|
||||
}
|
||||
this._trackingDate = today;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ComplianceManager] Failed to load:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_save() {
|
||||
try {
|
||||
if (GameGlobal && GameGlobal.storageManager) {
|
||||
GameGlobal.storageManager.set('compliance', {
|
||||
isMinor: this._isMinor,
|
||||
spendingMonth: this._getMonthKey(),
|
||||
monthlySpending: this._monthlySpending,
|
||||
adDate: this._getTodayKey(),
|
||||
dailyAdCount: this._dailyAdCount,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Minor Status Detection
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Check if the user is a minor via WeChat platform API.
|
||||
* @private
|
||||
*/
|
||||
_checkMinorStatus() {
|
||||
// WeChat provides user age info through specific APIs
|
||||
// In production, this would call wx.getUserInfo or a server-side check
|
||||
// For now, default to non-minor
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && typeof wx.getSetting === 'function') {
|
||||
// Placeholder: in production, check real-name verification status
|
||||
console.log('[ComplianceManager] Minor status check: defaulting to adult');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Public API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Whether the current user is identified as a minor.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isMinor() {
|
||||
return this._isMinor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minor status (called after real-name verification).
|
||||
* @param {boolean} isMinor
|
||||
*/
|
||||
setMinorStatus(isMinor) {
|
||||
this._isMinor = isMinor;
|
||||
this._save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a purchase is allowed for the current user.
|
||||
* Minors: monthly limit ¥400, single purchase > ¥50 requires confirmation.
|
||||
* @param {number} amountFen - Purchase amount in fen (分).
|
||||
* @returns {{ allowed: boolean, needsConfirmation: boolean, reason?: string }}
|
||||
*/
|
||||
checkPurchaseRestriction(amountFen) {
|
||||
if (!this._isMinor) {
|
||||
return { allowed: true, needsConfirmation: false };
|
||||
}
|
||||
|
||||
const amountYuan = amountFen / 100;
|
||||
|
||||
// Monthly limit: ¥400
|
||||
const newTotal = this._monthlySpending + amountFen;
|
||||
if (newTotal > 40000) { // 400 yuan in fen
|
||||
return {
|
||||
allowed: false,
|
||||
needsConfirmation: false,
|
||||
reason: 'monthly_limit',
|
||||
};
|
||||
}
|
||||
|
||||
// Single purchase > ¥50 needs confirmation
|
||||
if (amountYuan > 50) {
|
||||
return {
|
||||
allowed: true,
|
||||
needsConfirmation: true,
|
||||
reason: 'large_purchase',
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true, needsConfirmation: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a successful purchase amount.
|
||||
* @param {number} amountFen
|
||||
*/
|
||||
recordPurchase(amountFen) {
|
||||
this._monthlySpending += amountFen;
|
||||
this._save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ad can be shown to the current user.
|
||||
* Minors: max 5 ads per day.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canShowAd() {
|
||||
if (!this._isMinor) return true;
|
||||
|
||||
const today = this._getTodayKey();
|
||||
if (this._trackingDate !== today) {
|
||||
this._trackingDate = today;
|
||||
this._dailyAdCount = 0;
|
||||
}
|
||||
|
||||
return this._dailyAdCount < 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an ad shown to the user.
|
||||
*/
|
||||
recordAdShown() {
|
||||
if (!this._isMinor) return;
|
||||
|
||||
const today = this._getTodayKey();
|
||||
if (this._trackingDate !== today) {
|
||||
this._trackingDate = today;
|
||||
this._dailyAdCount = 0;
|
||||
}
|
||||
|
||||
this._dailyAdCount++;
|
||||
this._save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate game session data for anti-cheat.
|
||||
* Checks for impossible stats (e.g., too many kills in too short time).
|
||||
* @param {object} stats - { kills, timeElapsed, score }
|
||||
* @returns {{ valid: boolean, flags: string[] }}
|
||||
*/
|
||||
validateGameSession(stats) {
|
||||
const flags = [];
|
||||
|
||||
if (!stats) return { valid: true, flags };
|
||||
|
||||
// Check impossible kill rate (>10 kills per minute)
|
||||
if (stats.kills && stats.timeElapsed) {
|
||||
const killsPerMinute = stats.kills / (stats.timeElapsed / 60);
|
||||
if (killsPerMinute > 10) {
|
||||
flags.push('suspicious_kill_rate');
|
||||
}
|
||||
}
|
||||
|
||||
// Check impossible score
|
||||
if (stats.score && stats.score > 100000) {
|
||||
flags.push('suspicious_score');
|
||||
}
|
||||
|
||||
// Check suspicious ad reward frequency
|
||||
// (anti-cheat for ad reward manipulation)
|
||||
return {
|
||||
valid: flags.length === 0,
|
||||
flags,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ComplianceManager;
|
||||
Reference in New Issue
Block a user