网络
OkHttp
配合后端上传或拉取资源
OKHTTP 是 Android 流行的网络请求库,性能好且功能强大,聊天室项目主要使用 OKHTTP 来提交和拉取 JSON 数据
public static final String url = "https://api.example.com/example";
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(
json.toString(),
MediaType.get("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url(url)
.post(requestBody) // .get() 表示拉取JSON
.build();
Response response = client.newCall(request).execute();
String result = response.body().string();
// 得到JSON数据
WebSocket
建立网络通信连接
简单的实现方法是 OkHttp 结合 WebSocketListener,首先要重写 WebSocketListener 的 4 种重写方法
通常还要加入 start 开始连接,sendMessage 发送消息和 close 主动关闭的方法,这 3 种方法往往接受 OkHttpClient 和 WebSocket 实例
public class MyWebSocketListener extends WebSocketListener {
private Activity activity;
private OkHttpClient client;
private WebSocket webSocket;
public void start(Activity activity, String url) {
this.activity = activity;
this.viewModel = viewModel;
client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
webSocket = client.newWebSocket(request, this);
}
public void sendMessage(String message) { if(webSocket != null) webSocket.send(message); }
public void close() { if(webSocket != null) webSocket.close(1000, "Closing"); }
@Override
public void onOpen(WebSocket webSocket, Response response){
super.onOpen(webSocket, response);
// 连接成功的回调
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response){
super.onFailure(webSocket, t, response);
// 连接失败的回调
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 回调处理接收到的JSON数据
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
// 连接关闭的回调
}
}
UI
RecyclerView
消息列表
首先准备数据模型类 Item 和 item 条目的布局文件 item.xml
,定义继承自 RecyclerView.Adapter<VH>
的类,重写构造方法,onCreateViewHolder,onBindViewHolder 和 getItemCount,VH 是同样自定义继承自 RecyclerView.ViewHolder
的类,通常可以写为内部类
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder> {
// 用于绑定和管理条目布局控件的ViewHolder内部类
public static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
ImageView imageView;
public MyViewHolder(@NonNull View itemView){
super(itemView);
textView = itemView.findViewById(R.id.text_view);
imageView = itemView.findViewById(R.id.image_view);
}
}
private final List<Item> itemList; // 数据模型集合
public MyRecyclerViewAdapter(List<Item> itemList){ this.itemList = itemList; }
// 创建ViewHolder并加载item布局
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
return new Holder(layoutInflater.inflate(R.layout.item, parent, false));
}
// LayoutInflater 是用于将布局xml文件转换成View对象的工具
// inflate方法用于实际转换并将View放入指定的布局对象parent内,false表示不立即放入而是等待RecylcerView处理
// 绑定数据到ViewHolder管理的控件
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position){
Item item = itemList.get(position);
// 准备数据
holder.textView.setText(item.getText());
holder.imageView.setImageResource(item.getImage());
}
}
RecyclerView recyclerView = findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
MyRecyclerViewAdapter adapter = new MyRecyclerViewAdapter(itemList /* 数据源 */);
recyclerView.setAdapter(adapter);
适应性布局
通过重写 getItemViewType 方法获得不同 viewType 标识符以适应性加载不同条目布局
@Override
public int getItemViewType(int position) {
Item item = itemList.get(position);
return item.getType();
// Type 可以是 public static final int 常量
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if(viewType == TYPE_A) {
view = layoutInflater.inflate(R.layout.item_A);
} else if(){
// ...
}
return new MyViewHolder(view);
}
DiffUtil
Android 提供的工具类,用于计算两个列表之间的差异,生成增删改和移动的操作序列,且通知 RecyclerView 更新 UI
DiffUtil 比 notifyDataSetChanged 更高效,只刷新差异的部分
1. DiffUtil.Callback 子类
public class MessageDiffCallback extends DiffUtil.Callback {
private final List<item> oldList;
private final List<item> newList;
public MessageDiffCallback(List<item> oldList, List<item> newList){
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize(){ return oldList.size(); }
@Override
public int getNewListSize(){ return newList.size(); }
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return oldList.get(oldItemPosition).getId() ==
newList.get(newItemPosition).getId();
} // 根据特征值(这里是id)判断是否为同一条目
@Override
public boolean areContentsTheSame(int oldItemPosition. int newItemPosition){
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
}
2. 计算差异 更新 Adapter
List<ChatMessageItem> oldList = adapter.getMessages();
List<ChatMessageItem> newList = viewModel.getMessages().getValue();
DiffUtil.Callback callback = new MessageDiffCallback(oldList, newList);
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
adapter.setMessages(newList); // 先更新Adapter内部数据
diffResult.dispatchUpdatesTo(adapter); // 更新UI的核心语句
ViewPager2 & BottomNavigationView
滑动切换页面配合底部导航栏
首先在主布局 xml 文件定义 ViewPager2 和 BottomBavigationView,创建 menu 文件并放入若干 item
定义继承 FragmentStateAdapter 类的 ViewPagerAdapter
public class ViewPagerAdapter extends FragmentStateAdapter {
public ViewPagerAdapter(FragmentActivity fragmentActivity){ super(fragmentActivity) }
@NonNull
@Override
public Fragment createFragement(int position){
switch (position) {
case 0:
return new FragmentA();
case 1:
return new FragmentB();
// ...
default:
return new FragmentDefault();
}
}
@Override
pubilc int getItemCount(){ return 2; /* position(max) + 1 */ }
}
在主页面设置 ViewPager2 和 BottomBavigationView 的逻辑
public class MainActivity extends AppCompatActivity {
private ViewPager2 viewPager;
private BottomNavigationView bottomNavigationView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = findViewById(R.id.viewPager);
bottomNavigationView = findViewById(R.id.bottomNavigationView);
// 设置 Adapter
viewPager.setAdapter(new ViewPagerAdapter(this));
// BottomNavigationView 点击切换页面
bottomNavigationView.setOnItemSelectedListener(item -> {
switch (item.getItemId()) {
case R.id.nav_home:
viewPager.setCurrentItem(0, false);
return true;
case R.id.nav_dashboard:
viewPager.setCurrentItem(1, false);
return true;
case R.id.nav_notifications:
viewPager.setCurrentItem(2, false);
return true;
}
return false;
});
// ViewPager2 页面切换时更新 BottomNavigationView 选中状态
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
switch (position) {
case 0:
bottomNavigationView.setSelectedItemId(R.id.nav_home);
break;
case 1:
bottomNavigationView.setSelectedItemId(R.id.nav_dashboard);
break;
case 2:
bottomNavigationView.setSelectedItemId(R.id.nav_notifications);
break;
}
}
});
}
}
重要方法有:
底部导航栏的 setOnItemSelectedListener
和 setSelectedItemId
,前者用于监听导航栏菜单项点击逻辑,返回 MenuItem 对象;后者设置导航栏当前 item 更新为正常的选中状态
ViewPager2 的 registerOnPageChangeCallback
和 setCurrentItem
,前者用于监听用户滑动或调用 setCurrentItem 的逻辑;后者直接切换到指定索引页面
真正发挥切换页面作用的是 setCurrentItem
ViewBinding
使用方式
在 app build.gradle 的 android 配置块加入
buildFeatures { viewBinding true }
ViewBinding 会根据每个 xml 布局文件生成对应的绑定类,生成规则为去掉下划线且将首字母大写 + Binding,例如 activity_main.xml 的绑定类为 ActivityMainBinding
ViewBinding 绑定类拥有 2 种实例化方式,分别对应活动布局和子布局(如 Fragment 或 View)
// 以 ActivityMainBinding 为例 public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); /* 完成的工作有 1. 实例化绑定类对象 2. 加载 activity_main.xml 布局文件 3. 创建布局里所有View的对象 所有嵌套的View都能被创建 4. 将View对象与绑定类里的成员变量关联起来 */ setContentView(binding.getRoot()); // 告诉Activity使用绑定类的根布局作为界面根布局 } }
// 以 FragmentMainBinding 为例,构造方法和其他重写方法略 public class MainFragment extends Fragment { private ActivityMainBinding binding; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { binding = FragmentInformationBinding.inflate(inflater, container, false); // 与单参构造方法类似,因为碎片等布局在活动内部,所以要传入父容器实例 return binding.getRoot(); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } }
完成初始化操作后,即可引用绑定类实例的成员变量来直接操作控件
LayoutInflater
用于将 xml 布局加载成 View 对象
1. setContentView
Activity 的 setContentView 在底层会调用 LayoutInflater 加载 xml 文件成 View 对象
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 几乎等同于
View view = getLayoutInflater().inflater(R.layout.activity_main, null);
setContentView(view);
2. ViewBinding 初始化
ViewBinding 使用 LayoutInflater 来加载布局且返回绑定类的实例
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
3. RecyclerView Adapter onCreateViewHolder
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
View view = LayoutInflater.from(parent.getContent()).inflate(R.layout.item_layout, parent, false);
return new MyViewHolder(view);
} // item 需要随用户滑动动态加载显示
4. 动态引入布局
LinearLayout container = findViewById(R.id.main);
View butotnView = getLayoutInflater().inflate(R.layout.button_layout, container, false);
container.addView(buttonView);
希望动态加载的控件不一定是额外的 xml layout 文件,只要是存在的 ViewGroup 父容器即可,布局文件中提前写空的 layout 可以明确的确定控件位置,方便做 UI 排版
数据
单例模式
设置用户自己的个人信息类
public class Instance {
private static final Instance instance = new Instance();
private Data data;
private Instance(){}
// 私有化构造方法,保证全局只有instance唯一实例
public static Instance getInstance(){ return instance; }
public Data getData(){ return data; }
public void setData(Data data){ this.data = data; }
}
// 饿汉式,在类初始化后就加载实例,而不是在getInstance方法被调用时
ViewModel
存储消息集合
ViewModel 类似单例模式,不随活动碎片销毁而回收,但在绑定宿主调用 finish 方法或不再加入返回栈后回收
LiveData 是生命周期感知型数据容器,可以让实例 Observe 观察 LiveData 的数据变化,执行更新逻辑
MutableLiveData 继承自 LiveData,是可写的,提供了主线程进行的 setValue 方法和回调的 postValue 方法
一个好的 ViewModel 继承类应该有私有的 MutableLiveData,提供一系列操作容器的方法,最终调用 setValue 或 postValue 方法来回调 Observe 的更新逻辑
其他观察 ViewModel 的实例应该只有 LiveData 只读容器的引用
SharedPreferences
登录缓存
SharedPreferences 用于保存一些简单的键值对数据(比如设置、用户偏好、登录状态等)
// Activity/Fragment 中获取 SharedPreferences
String document_name = "";
SharedPreferences sp = getSharedPreferences(document_name, MODE_PRIVATE);
// MODE_PRIVATE 表示文件私有
// 全局获取 通过上下文实例 Context 调用 getSharedPreferences 获得
SharedPreferences.Editor editor = sp.edit();
editor.putString("username", "Young");
editor.putInt("age", 20);
editor.putBoolean("isLogin", true);
editor.apply();
// apply 异步提交保存 commit 同步提交保存 返回 boolean 表示是否成功
// 删除调用 remove 方法 参数传递 put 的键名 最后也要保存
String name = sp.getString("username", "defaultName");
// 读取数据不需要 Editor 实例 第二参数是键不存在时的默认返回值
SQLiteOpenHelper & Cursor
本地存储消息
继承自 SQLiteOpenHelper 的自定义类可以用于创建数据库和提供可读写的数据库对象 SQLiteDatabase
基本使用
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "database.db";
private static final int DB_VERSION = 1;
public MyDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String createTable = "CREATE TABLE chair (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name TEXT, " +
"age INTEGER)";
db.execSQL(createTable);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS chair");
// DROP TABLE IF EXISTS chair 库中存在 chair 表则删除 否则什么都不做
onCreate(db);
}
}
CURD
DatabaseHelper dbHelper = new DatabaseHelper(context);
SQLiteDatabase db = dbHelper.getWritableDatabase(); // 获取可写流
// 增
ContentValues values = new ContentValues();
values.put(key1, value1);
values.put(key2, value2);
db.insert(table, null, values);
// 改
ContentValues updateValues = new ContentValues();
updateValues.put(key, value);
db.update(table, updateValues, "name=?", new String[]{"4Forsee"});
/*
int update(
String table, // 表名
ContentValues values, // 要更新的列及新值
String whereClause, // WHERE 条件(可以用 ? 占位)
String[] whereArgs // 占位符对应的值
)
*/
// 删
db.delete(table, "name=?", new String[]{"4Forsee"});
Cursor 查
Cursor 是 SQLite 查询结果的游标对象,指向一行数据,可以按列读取数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("SELECT * FROM chair WHERE age > ?", new String[]{"24"});
// rawQuery 原生使用SQL语句查询 query方法参数繁杂
if (cursor.moveToFirst()) { // 移到第一行
do {
String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
Log.d("TAG", "name=" + name);
} while (cursor.moveToNext());
}
//
cursor.close();
db.close();
活动间通信
ActivityResult
用于打开相册
Activity1 期望启动 Activity2 且回调 Activity2 的返回值在 < 30 API 前要配对使用 startActivityForResult(Intent intent, int requestCode)
和 onActivityResult(int requestCode, int resultCode, Intent data)
,前者调用后者重写,requestCode 是唯一整数,用于在回调区分来源
当启动活动 Activity2 调用 setResult(int resultCode, Intent intent)
且 finish 结束后,回调 Activity1 的 onActivityResult 方法,在方法中根据不同的 requestCode 请求码和 resultCode 返回码设计具体逻辑
这样写有 requestCode,resultCode 和 Intent 的解析码都要处理,代码耦合且不直观,所以有 ActivityResult 的出现,内部管理 requestCode,使用方法如下:
private ActivityResultLauncher<Intent> launcher;
在 Activity 中设置 ActivityResultLauncher 成员变量在 Activity 的 onCreate 方法中注册回调
launcher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK) { Intent data = result.getData(); if (data != null) { String value = data.getStringExtra("result_key"); // 处理返回的数据 } } } );
Intent intent = new Intent(this, Activity2.class); launcher.launch(intent);
Activity2 的回调逻辑和原来一样,都是调用 setResult 方法且结束后回调
注册回调结构
launcher =
registerForActivityResult(ActivityResultContract<I, O> Contract, ActivityResultCallback<O> Callback);
ActivityResultContract 是安卓已经写好的任务合同,定义了发送接收数据的类型和任务执行的具体细节
ActivityResultCallback 是回调逻辑,result 的数据类型相当于 Contract 的接收数据类型
未完成
- 点击按钮显示聊天室所有成员
- 后端代码迁移到新服务器
- 尝试开通SMTP做邮箱验证码登录和找回密码
- 让下线的用户也能接收到消息
- 发送多媒体文件
- 实时更新用户信息
- 长按消息出现删除按钮
服务器搭建
在阿里云平台的 轻量应用服务器 或 云服务器 ECS 购买服务器,也可以尝试拿到试用服务器,通常是 1 个月
创建系统时选择 Linux Ubuntu 且安装 Docker,不同的 Linux 发行版本有不同的软件包管理器(yum 或 apt)
创建成功后直接进入或设置密码进入服务器内部,在命令行输入下列内容:
sudo apt update # 安装 Docker sudo apt install -y docker.io sudo systemctl enable --now docker sudo docker --version # 安装 Node.js sudo apt install -y nodejs npm # 安装 PM2 sudo npm install -g pm2 pm2 --version # 安装 Nano 编辑器 sudo apt install -y nano nano --version # 拉取v8.0的 MySQL 镜像 docker pull mysql:8.0 # 运行 MySQL 容器 docker run --name 容器名称 -e MYSQL_ROOT_PASSWORD=密码 -p 3306:3306 -d mysql:8.0
成功执行后,服务器就拥有简易的后端环境,可以继续安装 express,ws,mysql2,multer 等模块
表和存储路径
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(50) | NO | UNI | NULL | |
| password | varchar(100) | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+ (users)
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| avatar | varchar(255) | YES | | NULL | |
| nickname | varchar(50) | YES | | | |
| bio | text | YES | | NULL | |
+------------+--------------+------+-----+---------+----------------+ (information)
服务器头像存储路径 /home/admin/public/avatar/
默认头像 0.png
本地头像存储路径 /data/data/com.example.chatroom/files/avatar
Nodejs 后端代码
const express = require('express');
const mysql = require('mysql2/promise');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const http = require('http');
const { WebSocketServer, WebSocket } = require('ws');
const app = express();
app.use(express.json());
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// MySQL 配置
const dbConfig = {
host: 'localhost',
port: 3306,
user: 'root',
password: '123456',
database: 'chatdb'
};
// 头像存储目录
const avatarDir = '/home/admin/public/avatar/';
if (!fs.existsSync(avatarDir)) {
fs.mkdirSync(avatarDir, { recursive: true });
}
// Multer 配置:接收 multipart/form-data 的 avatar 文件
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, avatarDir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const name = Date.now() + '-' + Math.round(Math.random() * 1e9) + ext;
cb(null, name);
}
});
const upload = multer({ storage });
const getActiveClientCount = () => {
let count = 0;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
count++;
}
});
return count;
};
wss.on('connection', (ws) => {
console.log('有客户端连接,当前客户端数量:', getActiveClientCount());
ws.on('message', (msg) => {
let obj;
try {
obj = JSON.parse(msg);
} catch (e) {
return;
}
// 广播给所有客户端
wss.clients.forEach(client => {
console.log('发送成功');
if (client.readyState === ws.OPEN) {
client.send(JSON.stringify({
...obj,
isSelf: client === ws ? 1 : 0
}));
}
});
});
ws.on('close', (code, reason) => {
console.log('客户端断开连接,当前客户端数量:', getActiveClientCount());
});
});
/**
* 上传头像并更新数据库
* POST /upload
* fields:
* - id: 用户 ID (text)
* - avatar: 头像文件 (file)
*/
app.post('/upload', upload.single('avatar'), async (req, res) => {
const userId = req.body.id;
if (!userId) {
return res.status(400).json({ success: false, message: '缺少用户 ID' });
}
if (!req.file) {
return res.status(400).json({ success: false, message: '没有上传文件' });
}
const absolutePath = path.join(avatarDir, req.file.filename);
let connection;
try {
connection = await mysql.createConnection(dbConfig);
const [result] = await connection.execute(
'UPDATE information SET avatar = ? WHERE id = ?',
[absolutePath, userId]
);
if (result.affectedRows === 0) {
return res.json({ success: false, message: '未找到该用户信息,更新失败' });
}
return res.json({
success: true,
message: '头像上传并更新成功',
avatarPath: absolutePath
});
} catch (err) {
console.error(err);
return res.status(500).json({ success: false, message: '服务器内部错误' });
} finally {
if (connection) await connection.end();
}
});
/**
* 更新用户个人信息(不含头像)
* POST /update
* JSON body:
* { id, nickname, bio }
*/
app.post('/update', async (req, res) => {
const { id, nickname, bio } = req.body;
if (!id || nickname === undefined || bio === undefined) {
return res.status(400).json({ success: false, message: '参数不完整' });
}
let connection;
try {
connection = await mysql.createConnection(dbConfig);
const [result] = await connection.execute(
'UPDATE information SET nickname = ?, bio = ? WHERE id = ?',
[nickname, bio, id]
);
if (result.affectedRows === 0) {
return res.json({ success: false, message: '未找到该用户信息,更新失败' });
}
return res.json({ success: true, message: '信息更新成功' });
} catch (err) {
console.error(err);
return res.status(500).json({ success: false, message: '服务器错误' });
} finally {
if (connection) await connection.end();
}
});
/**
* 登录接口
* POST /login
* JSON body:
* { username, password }
*/
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, message: '用户名或密码不能为空' });
}
let connection;
try {
connection = await mysql.createConnection(dbConfig);
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
if (rows.length === 0) {
const [insertResult] = await connection.execute(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, password]
);
await connection.execute(
'INSERT INTO information (id, avatar, nickname, bio) VALUES (?, ?, ?, ?)',
[insertResult.insertId, '/home/admin/public/avatar/0.png', username, '']
);
return res.json({
success: true,
message: '新用户已创建并登录',
user: {
id: insertResult.insertId,
username,
avatar: '/home/admin/public/avatar/0.png',
nickname: username,
bio: ''
}
});
} else {
if (rows[0].password !== password) {
return res.status(401).json({ success: false, message: '密码错误' });
}
const userId = rows[0].id;
const [infoRows] = await connection.execute(
'SELECT avatar, nickname, bio FROM information WHERE id = ?',
[userId]
);
return res.json({
success: true,
message: '登录成功',
user: {
id: userId,
username,
avatar: infoRows[0]?.avatar || '',
nickname: infoRows[0]?.nickname || '',
bio: infoRows[0]?.bio || ''
}
});
}
} catch (err) {
console.error(err);
return res.status(500).json({ success: false, message: '服务器错误' });
} finally {
if (connection) await connection.end();
}
});
/**
* 获取所有头像
* GET /avatars
* 返回:{ success: true, data: [avatar1, avatar2, ...] }
*/
app.get('/avatars', async (req, res) => {
try {
const connection = await mysql.createConnection(dbConfig);
const [results] = await connection.execute('SELECT avatar FROM information');
await connection.end();
const avatars = results.map(row => row.avatar);
res.json({ success: true, data: avatars });
} catch (err) {
return res.status(500).json({ success: false, message: '服务器错误' });
}
});
server.listen(3000, '0.0.0.0', () => {
console.log('服务器启动,监听端口 3000');
});