Android游戏防沉迷系统实现记录

根据国家新闻出版署的通知要求,游戏必须接入实名认证和防沉迷系统。这里记录一下Android端的实现方案。

系统架构设计

功能模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────┐
│ 防沉迷系统架构 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ 实名认证模块 │ │ 时长控制模块 │ │ 弹窗提示模块 │ │
│ │ │ │ │ │ │ │
│ │ • 身份证校验 │ │ • 在线计时 │ │ • 认证提示弹窗 │ │
│ │ • 人脸识别 │ │ • 时段限制 │ │ • 游戏时长提醒 │ │
│ │ • 数据存储 │ │ • 强制下线 │ │ • 强制退出确认 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 数据存储层 │ │
│ │ SQLite数据库 + 本地加密 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

防沉迷规则

根据国家规定,防沉迷系统要实现以下规则:

年龄段 游戏时长限制 可游戏时间段 消费限制
未满8周岁 禁止游戏 - 禁止充值
8-16周岁 1.5小时/日 8:00-22:00 单次≤50元,月累计≤200元
16-18周岁 2小时/日 8:00-22:00 单次≤100元,月累计≤400元
18周岁以上 无限制 全天 无限制

项目配置

build.gradle配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android {
compileSdkVersion 26
buildToolsVersion "26.0.2"

defaultConfig {
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
}

dependencies {
compile 'com.android.support:appcompat-v7:26.1.0'
}

AndroidManifest.xml权限声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.game">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:name=".BaseApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>

UI界面设计

实名认证提示弹窗布局 (dialog_auth_tip.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog_warning">

<TextView
android:id="@+id/dialog_auth_tip_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center"
android:padding="8dp"
android:textColor="#000000"
android:textSize="18sp"
android:text="实名认证" />

<TextView
android:id="@+id/dialog_auth_tip_message"
android:maxWidth="300dp"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="center"
android:padding="7dp"
android:textSize="14sp"
android:text="根据国家关于《防止未成年人沉迷网络游戏的通知》要求,玩家必须完成实名认证,否则将禁止进入游戏。" />

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#dfdfdf" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/dialog_auth_tip_button_negative"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_weight="1"
android:textColor="#000000"
android:text="取消" />

<Button
android:id="@+id/dialog_auth_tip_button_positive"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_weight="1"
android:textColor="#000000"
android:text="去认证" />

</LinearLayout>

</LinearLayout>

实名认证输入弹窗布局 (dialog_auth.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog_warning">

<TextView
android:id="@+id/dialog_auth_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center"
android:padding="8dp"
android:textColor="#000000"
android:textSize="18sp"
android:text="实名认证" />

<EditText
android:id="@+id/dialog_auth_username"
android:maxWidth="300dp"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_gravity="center"
android:inputType="textPersonName"
android:hint="请输入您的真实姓名"
android:textSize="14sp" />

<EditText
android:id="@+id/dialog_auth_userid"
android:maxWidth="300dp"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_gravity="center"
android:hint="请输入身份证号,如有字母请小写"
android:textSize="14sp"
android:inputType="textPersonName" />

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#dfdfdf" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/dialog_auth_button_negative"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_weight="1"
android:textColor="#000000"
android:text="取消" />

<Button
android:id="@+id/dialog_auth_button_positive"
android:layout_width="wrap_content"
android:layout_height="55dp"
android:layout_weight="1"
android:background="#33ccff"
android:textColor="#ffffff"
android:text="提交" />

</LinearLayout>

</LinearLayout>

防沉迷提示弹窗布局 (dialog_anti.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_dialog_warning">

<TextView
android:id="@+id/dialog_anti_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center"
android:padding="8dp"
android:textColor="#000000"
android:textSize="18sp"
android:text="防沉迷提示" />

<TextView
android:id="@+id/dialog_anti_message"
android:maxWidth="300dp"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="center"
android:padding="7dp"
android:textSize="14sp"
android:text="" />

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#dfdfdf" />

<Button
android:id="@+id/dialog_anti_button_negative"
android:layout_width="match_parent"
android:layout_height="55dp"
android:textColor="#000000"
android:text="我知道了" />

</LinearLayout>

弹窗背景样式 (bg_dialog_warning.xml)

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<corners android:radius="12dp" />
</shape>

Dialog主题样式 (styles.xml)

1
2
3
4
5
<resources>
<style name="WarningDialogTheme" parent="Theme.AppCompat.Dialog">
<item name="android:windowBackground">@drawable/bg_dialog_warning</item>
</style>
</resources>

核心服务类实现

防沉迷服务主类 (AuthService.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import android.app.ActivityManager;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Handler;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
* 防沉迷系统核心服务类
* 负责实名认证、游戏时长控制、未成年人保护等功能
*/
public class AuthService {

static Context mContext;
static File mDir;

// 数据库相关SQL语句
static String createSql = "create table user(id integer primary key autoincrement, name, id_card_number, dt, age integer, during integer)";
static String selectSql = "select id, name, id_card_number, dt, age, during from user";
static String insertSql = "insert into user(name, id_card_number, age, dt, during) values(?,?,?,?,?)";
static String updateSql = "update user set dt=?,during=? where id=?";

// 用户数据
static String dt = null; // 当前登录日期
static int id = 1; // 用户ID
static int age = 0; // 用户年龄
public static int during = 0; // 今日游戏时长(分钟)
static long times = 0; // 上次记录时间戳
static Handler mHandler; // 定时器Handler

/**
* 初始化防沉迷系统
* @param context 应用上下文
* @param dir 数据存储目录
*/
public static void init(Context context, File dir) {
mContext = context;
mDir = dir;
SQLiteDatabase DB = null;
Cursor cursor = null;
Date today = new Date();
times = today.getTime() / 1000 / 60; // 转换为分钟

try {
DB = SQLiteDatabase.openOrCreateDatabase(mDir + "/info.db", null);
try {
cursor = DB.rawQuery(selectSql, null);
} catch (Exception e) {
// 表不存在,创建表
DB.execSQL(createSql);
cursor = DB.rawQuery(selectSql, null);
}

if (cursor != null && cursor.getCount() > 0) {
// 已实名认证,读取用户信息
while (cursor.moveToNext()) {
String nameString = cursor.getString(cursor.getColumnIndex("name"));
String id_card_number = cursor.getString(cursor.getColumnIndex("id_card_number"));
dt = cursor.getString(cursor.getColumnIndex("dt"));
id = cursor.getInt(cursor.getColumnIndex("id"));
age = cursor.getInt(cursor.getColumnIndex("age"));
during = cursor.getInt(cursor.getColumnIndex("during"));

// 判断是否是同一天
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String dateTime = df.format(today);
if (dateTime.compareTo(dt) == 0) {
// 同一天,检查防沉迷
checkAnti();
} else {
// 新的一天,重置时长
dt = dateTime;
during = 0;
updateUser();
}
break;
}
} else {
// 未实名认证,显示认证提示
showAuthTipDialog();
}
} catch (Exception e) {
// 异常时显示认证提示
showAuthTipDialog();
} finally {
if (cursor != null) cursor.close();
if (DB != null) DB.close();
}

// 启动定时检查
scheduleAnti();
}

/**
* 显示实名认证提示弹窗
*/
private static void showAuthTipDialog() {
AuthTipDialog authTipDialog = new AuthTipDialog.Builder(mContext).create();
authTipDialog.show();
}

/**
* 启动防沉迷定时检查
* 每5分钟检查一次
*/
public static void scheduleAnti() {
mHandler = new Handler();
Runnable r = new Runnable() {
@Override
public void run() {
checkAnti();
// 每5分钟检查一次
mHandler.postDelayed(this, 300000);
}
};
mHandler.postDelayed(r, 300000);
}

/**
* 保存用户信息到数据库
*/
public static void saveUser(String id_card_number, String name) {
SQLiteDatabase DB = null;
try {
DB = SQLiteDatabase.openOrCreateDatabase(mDir + "/info.db", null);
// 根据身份证号计算年龄
Calendar cal = Calendar.getInstance();
age = cal.get(Calendar.YEAR) - new Integer(id_card_number.substring(6, 10));

SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
dt = df.format(new Date());

DB.execSQL(insertSql, new String[]{name, id_card_number, age + "", dt, "0"});
} catch (Exception e) {
e.printStackTrace();
} finally {
if (DB != null) DB.close();
}
}

/**
* 检查防沉迷状态
* 根据用户年龄和当前游戏时长判断是否触发限制
*/
public static void checkAnti() {
if (dt == null) return;

// 防沉迷规则:
// 未满13周岁:累计1小时强制下线
// 13-18周岁:累计2小时强制下线
// 所有未成年人:21:00-次日8:00禁止游戏

int duringMax = 60 * 24; // 默认无限制
boolean showTip = false;

if (age < 13) {
duringMax = 60; // 1小时
showTip = true;
} else if (age < 18) {
duringMax = 60 * 2; // 2小时
showTip = true;
}

Date today = new Date();
int h = today.getHours();

// 计算新增游戏时长
long d = today.getTime() / 1000 / 60 - times;
during += d;
times = today.getTime() / 1000 / 60;
updateUser();

if (during >= duringMax) {
// 游戏时长超限,强制退出
AntiDialog antiDialog = new AntiDialog.Builder(mContext).create(duringMax);
antiDialog.show();
} else if (duringMax == 60 && (h < 8 || h >= 21)) {
// 晚间时段限制(仅针对13周岁以下)
AntiDialog antiDialog = new AntiDialog.Builder(mContext).create(1);
antiDialog.show();
} else if (showTip) {
// 显示游戏时长提示
AntiDialog antiDialog = new AntiDialog.Builder(mContext).create(0);
antiDialog.show();
}
}

/**
* 更新用户游戏时长数据
*/
public static void updateUser() {
SQLiteDatabase DB = null;
try {
DB = SQLiteDatabase.openOrCreateDatabase(mDir + "/info.db", null);
DB.execSQL(updateSql, new String[]{dt, during + "", id + ""});
} catch (Exception e) {
e.printStackTrace();
} finally {
if (DB != null) DB.close();
}
}

/**
* 实名认证身份证与姓名匹配验证
* 使用第三方实名认证API(如阿里云、百度云等)
* @return 验证是否成功
*/
public static boolean idmatch(String id_card_number, String name) {
String access_token = getAuth();
String postIdMatchUrl = "https://aip.baidubce.com/rest/2.0/face/v3/person/idmatch?access_token=" + access_token;

try {
JSONObject jsonParams = new JSONObject();
jsonParams.put("id_card_number", id_card_number);
jsonParams.put("name", name);
String json = jsonParams.toString();

URL realUrl = new URL(postIdMatchUrl);
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");

OutputStream os = connection.getOutputStream();
os.write(json.getBytes("UTF-8"));
os.flush();

// 读取响应
InputStream inputStream;
int code = connection.getResponseCode();
if (code == 200) {
inputStream = connection.getInputStream();
} else {
inputStream = connection.getErrorStream();
return false;
}

BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String lines;
StringBuffer sb = new StringBuffer("");
while ((lines = reader.readLine()) != null) {
lines = URLDecoder.decode(lines, "UTF-8");
sb.append(lines);
}

JSONObject resultObject = new JSONObject(sb.toString());
connection.disconnect();

int error_code = resultObject.getInt("error_code");
if (error_code == 0) {
saveUser(id_card_number, name);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

/**
* 获取API访问Token
*/
private static String getAuth() {
// 替换为您的API Key和Secret Key
String clientId = "YOUR_API_KEY";
String clientSecret = "YOUR_SECRET_KEY";
return getAuth(clientId, clientSecret);
}

/**
* 通过API Key和Secret Key获取访问Token
*/
private static String getAuth(String ak, String sk) {
String authHost = "https://aip.baidubce.com/oauth/2.0/token?";
String getAccessTokenUrl = authHost
+ "grant_type=client_credentials"
+ "&client_id=" + ak
+ "&client_secret=" + sk;

try {
URL realUrl = new URL(getAccessTokenUrl);
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
connection.setRequestMethod("GET");
connection.connect();

InputStream inputStream;
int code = connection.getResponseCode();
if (code == 200) {
inputStream = connection.getInputStream();
} else {
inputStream = connection.getErrorStream();
}

BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
String result = "";
String line;
while ((line = in.readLine()) != null) {
result += line;
}

JSONObject jsonObject = new JSONObject(result);
return jsonObject.getString("access_token");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 完全退出应用
*/
public static void exitApp() {
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.AppTask> appTaskList = activityManager.getAppTasks();
for (ActivityManager.AppTask appTask : appTaskList) {
appTask.finishAndRemoveTask();
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
}

弹窗Dialog实现

实名认证提示弹窗 (AuthTipDialog.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import android.app.Dialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
* 实名认证提示弹窗
*/
public class AuthTipDialog extends Dialog {

private AuthTipDialog(Context context, int themeResId) {
super(context, themeResId);
}

public static class Builder {
private Context mContext;
private View mLayout;
private TextView mTitle;
private TextView mMessage;
private Button mPositiveButton;
private Button mNegativeButton;
private AuthTipDialog mDialog;

public Builder(Context context) {
mContext = context;
mDialog = new AuthTipDialog(context, R.style.WarningDialogTheme);
LayoutInflater inflater = LayoutInflater.from(context);
mLayout = inflater.inflate(R.layout.dialog_auth_tip, null);

mTitle = mLayout.findViewById(R.id.dialog_auth_tip_title);
mMessage = mLayout.findViewById(R.id.dialog_auth_tip_message);
mNegativeButton = mLayout.findViewById(R.id.dialog_auth_tip_button_negative);
mPositiveButton = mLayout.findViewById(R.id.dialog_auth_tip_button_positive);
}

public AuthTipDialog create() {
mDialog.setContentView(mLayout);

// 去认证按钮
mPositiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDialog.dismiss();
// 显示实名认证输入弹窗
AuthDialog authDialog = new AuthDialog.Builder(mContext).create();
authDialog.show();
}
});

// 取消按钮
mNegativeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDialog.dismiss();
AuthService.exitApp(); // 未认证则退出游戏
}
});

mDialog.setCancelable(false);
mDialog.setCanceledOnTouchOutside(false);
return mDialog;
}
}
}

实名认证输入弹窗 (AuthDialog.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

/**
* 实名认证输入弹窗
*/
public class AuthDialog extends Dialog {

private AuthDialog(Context context, int themeResId) {
super(context, themeResId);
}

public static class Builder {
private View mLayout;
private EditText mUsername;
private EditText mUserid;
private Button mPositiveButton;
private Button mNegativeButton;
private AuthDialog mDialog;
private Context mContext;

private String id_card_number;
private String name;

private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
mPositiveButton.setEnabled(true);
Toast.makeText(mContext, "实名认证失败", Toast.LENGTH_SHORT).show();
}
};

public Builder(Context context) {
mContext = context;
mDialog = new AuthDialog(mContext, R.style.WarningDialogTheme);
LayoutInflater inflater = LayoutInflater.from(mContext);
mLayout = inflater.inflate(R.layout.dialog_auth, null);

mUsername = mLayout.findViewById(R.id.dialog_auth_username);
mUserid = mLayout.findViewById(R.id.dialog_auth_userid);
mNegativeButton = mLayout.findViewById(R.id.dialog_auth_button_negative);
mPositiveButton = mLayout.findViewById(R.id.dialog_auth_button_positive);
}

public AuthDialog create() {
mDialog.setContentView(mLayout);

// 提交按钮
mPositiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
id_card_number = mUserid.getText().toString();
name = mUsername.getText().toString();

// 输入验证
if (name.length() == 0) {
Toast.makeText(mContext, "请输入姓名", Toast.LENGTH_SHORT).show();
return;
}
if (id_card_number.length() != 18) {
Toast.makeText(mContext, "请输入正确格式的身份证号", Toast.LENGTH_SHORT).show();
return;
}

mPositiveButton.setEnabled(false);

// 异步验证
new Thread(new Runnable() {
@Override
public void run() {
if (AuthService.idmatch(id_card_number, name)) {
mDialog.dismiss();
} else {
handler.sendMessage(new Message());
}
}
}).start();
}
});

// 取消按钮
mNegativeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDialog.dismiss();
AuthService.exitApp();
}
});

mDialog.setCancelable(false);
mDialog.setCanceledOnTouchOutside(false);
return mDialog;
}
}
}

防沉迷提示弹窗 (AntiDialog.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import android.app.Dialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
* 防沉迷提示弹窗
*/
public class AntiDialog extends Dialog {

static AntiDialog mDialog = null;

private AntiDialog(Context context, int themeResId) {
super(context, themeResId);
}

public static class Builder {
private View mLayout;
private TextView mTitle;
private TextView mMessage;
private Button mNegativeButton;
private int mAnti; // 防沉迷类型

public Builder(Context context) {
if (mDialog != null) mDialog.dismiss();
mDialog = new AntiDialog(context, R.style.WarningDialogTheme);
LayoutInflater inflater = LayoutInflater.from(context);
mLayout = inflater.inflate(R.layout.dialog_anti, null);

mTitle = mLayout.findViewById(R.id.dialog_anti_title);
mMessage = mLayout.findViewById(R.id.dialog_anti_message);
mNegativeButton = mLayout.findViewById(R.id.dialog_anti_button_negative);
}

public AntiDialog create(int anti) {
mAnti = anti;
String message;

if (anti == 1) {
// 晚间时段限制
message = "【健康系统】根据健康系统规则,您只能在8:00 - 21:00时间段进行游戏。请合理安排游戏时间,劳逸结合。";
} else if (anti > 1) {
// 游戏时长超限
message = "【健康系统】您今日累计游戏时间已经超过" + (anti / 60) + "个小时,根据健康系统规则,您今日将无法登录游戏。请合理安排游戏时间,劳逸结合。";
} else {
// 游戏时长提示
message = "【健康系统】您今日累计游戏时间已经有" + AuthService.during + "分钟。";
}

mMessage.setText(message);
mDialog.setContentView(mLayout);

mNegativeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mDialog.dismiss();
mDialog = null;
if (mAnti > 0) {
AuthService.exitApp(); // 触发限制则退出游戏
}
}
});

mDialog.setCancelable(false);
mDialog.setCanceledOnTouchOutside(false);
return mDialog;
}
}
}

使用说明

在MainActivity中初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 初始化防沉迷系统
AuthService.init(this, getFilesDir());
}

@Override
protected void onResume() {
super.onResume();
// 每次回到前台都检查防沉迷状态
AuthService.checkAnti();
}
}

实名认证API接入

本文示例使用百度云实名认证API,您也可以选择其他服务提供商:

服务提供商 接口地址 特点
百度云 aip.baidubce.com 免费额度充足
阿里云 aliyun.com 稳定性好
腾讯云 cloud.tencent.com 接口完善

接入步骤:

  1. 注册开发者账号
  2. 创建应用获取API Key和Secret Key
  3. 替换代码中的密钥
  4. 测试验证

总结一下

防沉迷系统实现的关键点:

模块 核心功能 实现要点
实名认证 身份证+姓名验证 使用第三方API,数据库存储
时长控制 游戏时长统计 定时检查,跨天重置
时段限制 晚间禁玩 根据年龄判断时段
强制退出 超限处理 弹窗提示,强制退出

注意事项:

  1. 实名认证信息必须加密存储
  2. 游戏时长数据需要防篡改
  3. 弹窗提示不能被跳过或遮挡
  4. 建议接入官方实名认证系统

参考:

  1. 国家新闻出版署通知:http://www.nppa.gov.cn/
  2. 百度云实名认证API:https://cloud.baidu.com/
  3. Android开发者文档:https://developer.android.com/