v2版本
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
791
IM_API_Collection.postman_collection.json
Normal file
791
IM_API_Collection.postman_collection.json
Normal file
@@ -0,0 +1,791 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "nl-im-api-collection",
|
||||
"name": "IM系统 API 接口集合",
|
||||
"description": "IM系统后端API接口测试集合,包含认证、用户、联系人、房间、消息、附件等所有接口",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://localhost:12080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "user_id",
|
||||
"value": "10001",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "认证模块",
|
||||
"item": [
|
||||
{
|
||||
"name": "用户登录",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"account\": \"user1@example.com\",\n \"password\": \"12345678\",\n \"remember\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/login",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" var jsonData = pm.response.json();",
|
||||
" pm.collectionVariables.set(\"token\", jsonData.token);",
|
||||
" pm.collectionVariables.set(\"user_id\", jsonData.user.id);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户注册",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"newuser@example.com\",\n \"phone\": \"13800138000\",\n \"password\": \"12345678\",\n \"confirm_password\": \"12345678\",\n \"code\": \"123456\",\n \"agree_terms\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/register",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "register"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "发送邮箱验证码",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"target\": \"test@example.com\",\n \"type\": \"email\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/send-email-code",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "send-email-code"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "发送短信验证码",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"target\": \"13800138000\",\n \"type\": \"sms\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/send-sms-code",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "send-sms-code"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "检查Token有效性",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/check-token",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "check-token"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "用户管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "获取当前用户信息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/user/my-info",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "user", "my-info"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取用户列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/user/list?page=1&page_size=20",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "user", "list"],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "page_size",
|
||||
"value": "20"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "创建用户",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"admin@example.com\",\n \"phone\": \"13900139000\",\n \"password\": \"12345678\",\n \"name\": \"管理员\",\n \"avatar\": \"A\",\n \"desc\": \"系统管理员\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/user/create",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "user", "create"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "更新用户信息",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"{{user_id}}\",\n \"updates\": {\n \"name\": \"新名称\",\n \"desc\": \"新签名\",\n \"region\": \"北京\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/user/update",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "user", "update"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "联系人管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "获取联系人列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "搜索用户",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/search?keyword=10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "search"],
|
||||
"query": [
|
||||
{
|
||||
"key": "keyword",
|
||||
"value": "10002"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "添加好友",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"to_user_id\": \"10002\",\n \"message\": \"你好,我想加你为好友\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/add-friend",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "add-friend"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取好友申请列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/friend-requests",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "friend-requests"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "接受好友申请",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"request_id\": 1\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/accept-request",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "accept-request"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "拒绝好友申请",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"request_id\": 1\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/reject-request",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "reject-request"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取分组列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/groups",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "groups"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "创建分组",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"group_name\": \"家人\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/groups",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "groups"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "更新分组",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"group_name\": \"朋友\",\n \"sort_order\": 1\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/groups/update/1",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "groups", "update", "1"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "删除分组",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/groups/delete/1",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "groups", "delete", "1"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取好友详情",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "10002"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "更新好友信息",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"remark_name\": \"备注名称\",\n \"group_id\": 1,\n \"is_top\": true,\n \"is_muted\": false\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/update/10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "update", "10002"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "删除好友",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/contacts/delete/10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "contacts", "delete", "10002"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "房间管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "创建房间",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"room_type\": \"p2p\",\n \"members\": [\"{{user_id}}\", \"10002\"]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/rooms",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "rooms"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取房间信息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/rooms/10001_10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "rooms", "10001_10002"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "消息管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "发送消息",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "X-User-ID",
|
||||
"value": "{{user_id}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"receiver_user_id\": \"10002\",\n \"room_id\": \"10001_10002\",\n \"message_type\": 0,\n \"content\": \"这是一条测试消息\",\n \"duration\": 0\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/send",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "send"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取历史消息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/messages?room_id=10001_10002&page=1&page_size=50",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "messages"],
|
||||
"query": [
|
||||
{
|
||||
"key": "room_id",
|
||||
"value": "10001_10002"
|
||||
},
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "page_size",
|
||||
"value": "50"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "附件管理",
|
||||
"item": [
|
||||
{
|
||||
"name": "上传附件",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "file",
|
||||
"type": "file",
|
||||
"src": []
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "image",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/attachments/upload",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "attachments", "upload"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取附件信息",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/attachments/1",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "attachments", "1"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取附件列表",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/attachments?type=image&page=1&page_size=20",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "attachments"],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "image"
|
||||
},
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "page_size",
|
||||
"value": "20"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "删除附件",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{token}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/attachments/delete/1",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "attachments", "delete", "1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "系统接口",
|
||||
"item": [
|
||||
{
|
||||
"name": "健康检查",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/health",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "health"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "获取ICE服务器配置",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/ice-servers?user_id={{user_id}}",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "ice-servers"],
|
||||
"query": [
|
||||
{
|
||||
"key": "user_id",
|
||||
"value": "{{user_id}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "检查用户在线状态",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/check-user-online?user_id=10002",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "check-user-online"],
|
||||
"query": [
|
||||
{
|
||||
"key": "user_id",
|
||||
"value": "10002"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/**
|
||||
* package main
|
||||
*
|
||||
* IM系统后端服务主程序
|
||||
*
|
||||
* 功能概述:
|
||||
* 1. 初始化配置、数据库、Redis等基础服务
|
||||
* 2. 初始化业务服务(认证、用户、联系人、房间、聊天、附件等)
|
||||
* 3. 启动WebSocket服务器和TURN服务器
|
||||
* 4. 注册HTTP API路由和中间件
|
||||
* 5. 启动HTTP服务器
|
||||
*
|
||||
* 技术栈:
|
||||
* - Gin: HTTP Web框架
|
||||
* - GORM: ORM数据库操作
|
||||
* - Redis: 缓存和消息队列
|
||||
* - WebSocket: 实时通信
|
||||
* - JWT: 身份认证
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -8,9 +27,11 @@ import (
|
||||
|
||||
"xk-websocket-v2/internal/api"
|
||||
"xk-websocket-v2/internal/manager"
|
||||
"xk-websocket-v2/internal/middleware"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/turnserver"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
"xk-websocket-v2/internal/ws"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -21,7 +42,17 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ... (initConfig, initDB, initRedis 保持不变) ...
|
||||
/**
|
||||
* initConfig
|
||||
*
|
||||
* 功能:初始化配置文件读取器
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 设置配置文件名为 "config"
|
||||
* 2. 设置配置文件类型为 YAML
|
||||
* 3. 添加配置文件搜索路径(configs目录和当前目录)
|
||||
* 4. 读取配置文件,如果失败则终止程序
|
||||
*/
|
||||
func initConfig() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
@@ -32,72 +63,227 @@ func initConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initDB
|
||||
*
|
||||
* 功能:初始化MySQL数据库连接并执行数据库迁移
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 从配置文件中读取数据库连接字符串(DSN)
|
||||
* 2. 使用GORM连接MySQL数据库
|
||||
* 3. 如果连接失败,终止程序
|
||||
* 4. 执行自动数据库迁移,创建所有表结构
|
||||
* 5. 为所有表添加中文注释
|
||||
* 6. 返回数据库连接实例
|
||||
*
|
||||
* @returns *gorm.DB 数据库连接实例
|
||||
*/
|
||||
func initDB() *gorm.DB {
|
||||
// 步骤1: 从配置文件读取数据库连接字符串
|
||||
dsn := viper.GetString("database.dsn")
|
||||
|
||||
// 步骤2: 使用GORM连接MySQL数据库
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 数据库连接失败: %v", err)
|
||||
}
|
||||
db.AutoMigrate(&model.ChatMessage{})
|
||||
|
||||
// 步骤3: 执行自动数据库迁移,创建所有表结构
|
||||
err = db.AutoMigrate(
|
||||
&model.ChatMessage{},
|
||||
&model.User{},
|
||||
&model.UserContact{},
|
||||
&model.ContactGroup{},
|
||||
&model.ChatRoom{},
|
||||
&model.FriendRequest{},
|
||||
&model.VerificationCode{},
|
||||
&model.Attachment{},
|
||||
&model.ApiRequestLog{},
|
||||
&model.LoginLog{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤4: 为所有表添加中文注释,提高数据库可读性
|
||||
tableComments := []struct {
|
||||
table string
|
||||
comment string
|
||||
}{
|
||||
{"chat_messages", "聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态"},
|
||||
{"users", "用户基本信息表"},
|
||||
{"user_contacts", "用户联系人表,存储好友关系、分组、备注等信息"},
|
||||
{"contact_groups", "联系人分组表"},
|
||||
{"chat_rooms", "聊天房间表,支持点对点和群聊"},
|
||||
{"friend_requests", "好友申请表"},
|
||||
{"verification_codes", "验证码表"},
|
||||
{"attachments", "附件表,记录上传的文件信息"},
|
||||
{"api_request_logs", "接口请求日志表"},
|
||||
{"login_logs", "登录日志表"},
|
||||
}
|
||||
|
||||
for _, tc := range tableComments {
|
||||
sql := fmt.Sprintf("ALTER TABLE `%s` COMMENT = '%s'", tc.table, tc.comment)
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
log.Printf("⚠️ 添加表注释失败 %s: %v", tc.table, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("✅ 数据库迁移完成")
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* initRedis
|
||||
*
|
||||
* 功能:初始化Redis连接
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 从配置文件读取Redis连接信息(地址、密码、数据库编号)
|
||||
* 2. 创建Redis客户端实例
|
||||
* 3. 执行Ping操作测试连接
|
||||
* 4. 如果连接失败,终止程序
|
||||
* 5. 返回Redis客户端实例
|
||||
*
|
||||
* @returns *redis.Client Redis客户端实例
|
||||
*/
|
||||
func initRedis() *redis.Client {
|
||||
// 步骤1: 从配置文件读取Redis连接信息
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: viper.GetString("redis.addr"),
|
||||
Password: viper.GetString("redis.password"),
|
||||
DB: viper.GetInt("redis.db"),
|
||||
Addr: viper.GetString("redis.addr"), // Redis服务器地址
|
||||
Password: viper.GetString("redis.password"), // Redis密码
|
||||
DB: viper.GetInt("redis.db"), // Redis数据库编号
|
||||
})
|
||||
|
||||
// 步骤2: 执行Ping操作测试连接是否正常
|
||||
if _, err := rdb.Ping(rdb.Context()).Result(); err != nil {
|
||||
log.Fatalf("❌ Redis 连接失败: %v", err)
|
||||
}
|
||||
|
||||
return rdb
|
||||
}
|
||||
|
||||
/**
|
||||
* main
|
||||
*
|
||||
* 功能:程序主入口,初始化所有服务并启动HTTP服务器
|
||||
*
|
||||
* 执行流程:
|
||||
* 1. 初始化配置、数据库、Redis
|
||||
* 2. 启动WebSocket工作池
|
||||
* 3. 初始化雪花ID生成器
|
||||
* 4. 初始化所有业务服务
|
||||
* 5. 启动TURN服务器(用于WebRTC)
|
||||
* 6. 创建Gin路由引擎
|
||||
* 7. 注册中间件(响应时间、请求日志、CORS)
|
||||
* 8. 注册WebSocket路由
|
||||
* 9. 注册HTTP API路由
|
||||
* 10. 启动HTTP服务器
|
||||
*/
|
||||
func main() {
|
||||
initConfig()
|
||||
db := initDB()
|
||||
rdb := initRedis()
|
||||
// 步骤1: 初始化基础服务
|
||||
initConfig() // 读取配置文件
|
||||
db := initDB() // 连接MySQL数据库
|
||||
rdb := initRedis() // 连接Redis
|
||||
|
||||
// 步骤2: 启动WebSocket工作池,用于处理WebSocket消息
|
||||
ws.StartWorkerPool()
|
||||
defer ws.StopWorkerPool()
|
||||
service.InitChatService(db, rdb)
|
||||
defer ws.StopWorkerPool() // 程序退出时关闭工作池
|
||||
|
||||
// 步骤3: 初始化雪花ID生成器(用于生成全局唯一ID)
|
||||
// 从配置文件读取数据中心ID和机器ID,如果未配置则使用默认值
|
||||
datacenterID := viper.GetInt64("snowflake.datacenter_id")
|
||||
if datacenterID == 0 {
|
||||
datacenterID = 1 // 默认数据中心ID为1
|
||||
}
|
||||
machineID := viper.GetInt64("snowflake.machine_id")
|
||||
if machineID == 0 {
|
||||
machineID = 1 // 默认机器ID为1
|
||||
}
|
||||
if err := utils.InitSnowflake(datacenterID, machineID); err != nil {
|
||||
log.Fatalf("❌ 初始化雪花ID生成器失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤4: 初始化所有业务服务
|
||||
service.InitChatService(db, rdb) // 聊天服务(消息处理、WebSocket分发)
|
||||
service.InitAuthService(db, rdb) // 认证服务(登录、注册、验证码)
|
||||
service.InitUserService(db) // 用户服务(用户信息管理)
|
||||
service.InitContactService(db) // 联系人服务(好友管理、分组管理)
|
||||
service.InitRoomService(db) // 房间服务(聊天房间管理)
|
||||
service.InitAttachmentService(db) // 附件服务(文件上传、管理)
|
||||
service.InitLoginLogService(db) // 登录日志服务(记录登录历史)
|
||||
|
||||
// 步骤5: 启动TURN服务器(用于WebRTC音视频通话)
|
||||
go turnserver.Start()
|
||||
|
||||
// 步骤6: 创建Gin路由引擎
|
||||
r := gin.Default()
|
||||
|
||||
// 允许跨域 (重要:为了前端本地开发)
|
||||
// 步骤7: 注册中间件(按顺序执行)
|
||||
// 响应时间统计中间件(必须在最前面,用于记录请求开始时间)
|
||||
r.Use(middleware.ResponseTimeMiddleware())
|
||||
|
||||
// 接口请求日志中间件(记录所有API请求信息)
|
||||
requestLogMiddleware := middleware.NewRequestLogMiddleware(db)
|
||||
r.Use(requestLogMiddleware.Handler())
|
||||
|
||||
// CORS跨域中间件(允许前端跨域访问,重要:为了前端本地开发)
|
||||
r.Use(func(c *gin.Context) {
|
||||
// 设置允许的源(*表示允许所有源)
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
// 设置允许的HTTP方法
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
// 设置允许的请求头
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-ID")
|
||||
// 处理OPTIONS预检请求
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
c.AbortWithStatus(204) // 返回204 No Content
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// 静态文件服务(提供附件访问功能)
|
||||
// 访问路径:/uploads/xxx -> ./uploads/xxx
|
||||
r.Static("/uploads", "./uploads")
|
||||
|
||||
// 步骤8: 注册WebSocket路由
|
||||
// 路径:GET /ws?user_id=xxx
|
||||
r.GET("/ws", func(c *gin.Context) {
|
||||
// 步骤1: 创建WebSocket升级器,允许所有来源连接
|
||||
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
|
||||
|
||||
// 步骤2: 将HTTP连接升级为WebSocket连接
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
return // 升级失败,直接返回
|
||||
}
|
||||
|
||||
// 步骤3: 获取用户ID(从查询参数中获取)
|
||||
userID := c.Query("user_id")
|
||||
|
||||
// 步骤4: 生成唯一的客户端ID(节点ID + 时间戳)
|
||||
clientID := fmt.Sprintf("%s-%d", viper.GetString("app.node_id"), time.Now().UnixNano())
|
||||
|
||||
// 步骤5: 创建客户端对象,包含连接和发送队列
|
||||
client := &manager.Client{ID: clientID, Conn: conn, SendQueue: make(chan []byte, 256)}
|
||||
|
||||
// 步骤6: 注册客户端到管理器
|
||||
manager.Manager.Register(client)
|
||||
|
||||
// 步骤7: 如果提供了用户ID,绑定用户到客户端
|
||||
if userID != "" {
|
||||
service.ChatSvc.BindUser(client, userID)
|
||||
}
|
||||
|
||||
// 步骤8: 发送客户端ID给前端
|
||||
client.SendQueue <- []byte(fmt.Sprintf(`{"clientId": "%s"}`, clientID))
|
||||
|
||||
// 步骤9: 循环读取WebSocket消息
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
// 连接断开,注销客户端
|
||||
manager.Manager.Unregister(client)
|
||||
break
|
||||
}
|
||||
@@ -107,22 +293,66 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
// 步骤9: 注册HTTP API路由
|
||||
apiGroup := r.Group("/api")
|
||||
{
|
||||
// 公开接口(不需要认证,任何人都可以访问)
|
||||
apiGroup.POST("/login", api.LoginHandler)
|
||||
apiGroup.POST("/register", api.RegisterHandler)
|
||||
apiGroup.POST("/send-email-code", api.SendEmailCodeHandler)
|
||||
apiGroup.POST("/send-sms-code", api.SendSmsCodeHandler)
|
||||
apiGroup.GET("/check-token", api.CheckTokenHandler)
|
||||
apiGroup.GET("/health", api.HealthHandler)
|
||||
apiGroup.GET("/ice-servers", api.ICEHandler)
|
||||
|
||||
// 消息相关(可选认证,兼容旧代码)
|
||||
apiGroup.POST("/send", api.SendHandler)
|
||||
apiGroup.POST("/send-to-user", api.SendToUserHandler)
|
||||
apiGroup.POST("/bind", api.BindHandler)
|
||||
apiGroup.GET("/check-user-online", api.CheckUserOnlineHandler)
|
||||
apiGroup.GET("/messages", api.HistoryHandler)
|
||||
apiGroup.GET("/messages/sync", api.SyncMessagesHandler)
|
||||
apiGroup.GET("/health", api.HealthHandler)
|
||||
apiGroup.GET("/ice-servers", api.ICEHandler)
|
||||
|
||||
// 新增:联系人列表
|
||||
apiGroup.GET("/contacts", api.ContactListHandler)
|
||||
// 需要认证的接口组(必须携带有效的JWT Token)
|
||||
authGroup := apiGroup.Group("")
|
||||
authGroup.Use(middleware.JWTAuthMiddleware()) // 使用JWT认证中间件
|
||||
{
|
||||
// 用户管理
|
||||
authGroup.GET("/user/my-info", api.GetMyInfoHandler)
|
||||
authGroup.GET("/user/list", api.GetUserListHandler)
|
||||
authGroup.POST("/user/create", api.CreateUserHandler)
|
||||
authGroup.POST("/user/update", api.UpdateUserHandler)
|
||||
authGroup.POST("/user/delete", api.DeleteUserHandler)
|
||||
|
||||
// 联系人管理
|
||||
authGroup.GET("/contacts", api.ContactListHandler)
|
||||
authGroup.GET("/contacts/search", api.SearchUsersHandler)
|
||||
authGroup.POST("/contacts/add-friend", api.AddFriendHandler)
|
||||
authGroup.GET("/contacts/friend-requests", api.GetFriendRequestsHandler)
|
||||
authGroup.POST("/contacts/accept-request", api.AcceptFriendRequestHandler)
|
||||
authGroup.POST("/contacts/reject-request", api.RejectFriendRequestHandler)
|
||||
authGroup.GET("/contacts/groups", api.GetGroupsHandler)
|
||||
authGroup.POST("/contacts/groups", api.CreateGroupHandler)
|
||||
authGroup.POST("/contacts/groups/update/:id", api.UpdateGroupHandler)
|
||||
authGroup.POST("/contacts/groups/delete/:id", api.DeleteGroupHandler)
|
||||
authGroup.GET("/contacts/:id", api.GetContactDetailHandler)
|
||||
authGroup.POST("/contacts/update/:id", api.UpdateContactHandler)
|
||||
authGroup.POST("/contacts/delete/:id", api.DeleteContactHandler)
|
||||
|
||||
// 房间管理
|
||||
authGroup.POST("/rooms", api.CreateRoomHandler)
|
||||
authGroup.GET("/rooms/:id", api.GetRoomHandler)
|
||||
|
||||
// 附件管理
|
||||
authGroup.POST("/attachments/upload", api.UploadAttachmentHandler)
|
||||
authGroup.GET("/attachments", api.GetAttachmentsHandler)
|
||||
authGroup.GET("/attachments/:id", api.GetAttachmentHandler)
|
||||
authGroup.POST("/attachments/delete/:id", api.DeleteAttachmentHandler)
|
||||
}
|
||||
}
|
||||
|
||||
port := viper.GetString("app.port")
|
||||
// 步骤10: 启动HTTP服务器
|
||||
port := viper.GetString("app.port") // 从配置文件读取端口号
|
||||
log.Printf("🚀 服务启动在端口: %s", port)
|
||||
r.Run(":" + port)
|
||||
r.Run(":" + port) // 启动服务器并监听指定端口
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ app:
|
||||
database:
|
||||
# 数据库连接字符串 (DSN)
|
||||
# 格式: user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
dsn: "root:root@tcp(127.0.0.1:3306)/nl_im?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
# dsn: "root:root@tcp(127.0.0.1:3306)/nl_im?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
dsn: "root:mysql_PKC65h@tcp(101.43.12.11:3306)/nl_im_plus?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
# 连接池最大空闲连接数
|
||||
max_idle_conns: 10
|
||||
# 连接池最大打开连接数
|
||||
@@ -26,11 +27,18 @@ database:
|
||||
# Redis 配置 (用于缓存和集群消息广播)
|
||||
# ==========================================
|
||||
redis:
|
||||
addr: "127.0.0.1:6379"
|
||||
password: "redis_pBjaRs"
|
||||
addr: "101.43.12.11:6379"
|
||||
password: "redis_xeePNa"
|
||||
# 数据库索引 (0-15)
|
||||
db: 0
|
||||
|
||||
# ==========================================
|
||||
# JWT 配置
|
||||
# ==========================================
|
||||
jwt:
|
||||
# JWT签名密钥(生产环境应使用强随机密钥)
|
||||
secret: "xk-websocket-jwt-secret-key-2025-change-in-production"
|
||||
|
||||
# ==========================================
|
||||
# TURN/STUN 服务器配置 (WebRTC 中继)
|
||||
# ==========================================
|
||||
@@ -38,7 +46,7 @@ turn:
|
||||
# 是否启用内置 TURN 服务
|
||||
enabled: true
|
||||
# 服务器公网 IP (重要:客户端必须能访问该IP)
|
||||
public_ip: "127.0.0.1"
|
||||
public_ip: "101.43.12.11"
|
||||
# TURN 服务监听端口 (UDP & TCP)
|
||||
listen_port: 3478
|
||||
# 认证领域名称
|
||||
|
||||
3
go.mod
3
go.mod
@@ -5,10 +5,12 @@ go 1.24.1
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/panjf2000/ants/v2 v2.11.3
|
||||
github.com/pion/turn/v2 v2.1.6
|
||||
github.com/spf13/viper v1.21.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
@@ -59,7 +61,6 @@ require (
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -43,6 +43,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
||||
153
internal/api/attachment_handler.go
Normal file
153
internal/api/attachment_handler.go
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:附件管理相关API处理器
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* UploadAttachmentHandler
|
||||
* 功能:上传附件
|
||||
* 路径:POST /api/attachments/upload
|
||||
*/
|
||||
func UploadAttachmentHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
// 获取文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "文件上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
fileType := c.PostForm("type")
|
||||
if fileType == "" {
|
||||
// 根据文件扩展名推断类型
|
||||
ext := file.Filename[len(file.Filename)-4:]
|
||||
if ext == ".jpg" || ext == ".png" || ext == ".gif" || ext == "webp" || ext == "jpeg" {
|
||||
fileType = "image"
|
||||
} else {
|
||||
fileType = "video"
|
||||
}
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if fileType != "image" && fileType != "video" {
|
||||
utils.BadRequest(c, "文件类型必须是image或video")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if fileType == "image" && file.Size > model.MaxImageSize {
|
||||
utils.BadRequest(c, "图片大小不能超过10MB")
|
||||
return
|
||||
}
|
||||
if fileType == "video" && file.Size > model.MaxVideoSize {
|
||||
utils.BadRequest(c, "视频大小不能超过500MB")
|
||||
return
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "打开文件失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 上传文件
|
||||
attachment, err := service.AttachmentSvc.UploadFile(
|
||||
userID.(string),
|
||||
file.Filename,
|
||||
fileType,
|
||||
file.Size,
|
||||
src,
|
||||
)
|
||||
if err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, attachment, "上传成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAttachmentHandler
|
||||
* 功能:获取附件信息
|
||||
* 路径:GET /api/attachments/:id
|
||||
*/
|
||||
func GetAttachmentHandler(c *gin.Context) {
|
||||
attachmentID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
|
||||
attachment, err := service.AttachmentSvc.GetAttachment(uint(attachmentID))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "附件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, attachment, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteAttachmentHandler
|
||||
* 功能:删除附件(仅上传者可删除)
|
||||
* 路径:DELETE /api/attachments/:id
|
||||
*/
|
||||
func DeleteAttachmentHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
attachmentID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
|
||||
if err := service.AttachmentSvc.DeleteAttachment(uint(attachmentID), userID.(string)); err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "附件已删除")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAttachmentsHandler
|
||||
* 功能:获取附件列表
|
||||
* 路径:GET /api/attachments
|
||||
*/
|
||||
func GetAttachmentsHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
fileType := c.Query("type")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
attachments, total, err := service.AttachmentSvc.GetUserAttachments(userID.(string), fileType, page, pageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if attachments == nil {
|
||||
attachments = []model.Attachment{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"data": attachments,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": pageSize,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
225
internal/api/auth_handler.go
Normal file
225
internal/api/auth_handler.go
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:认证相关API处理器
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* LoginHandler
|
||||
* 功能:用户登录
|
||||
* 路径:POST /api/login
|
||||
*/
|
||||
func LoginHandler(c *gin.Context) {
|
||||
var req model.LoginReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
ip := getClientIP(c)
|
||||
|
||||
// 调用认证服务登录
|
||||
user, err := service.AuthSvc.Login(req.Account, req.Password)
|
||||
if err != nil {
|
||||
// 记录登录失败日志
|
||||
service.LoginLogSvc.LogLogin(req.Account, "0", ip, false)
|
||||
utils.Unauthorized(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
token, err := utils.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
// 记录登录失败日志
|
||||
service.LoginLogSvc.LogLogin(req.Account, "0", ip, false)
|
||||
utils.InternalError(c, "生成Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
// 记录登录成功日志
|
||||
service.LoginLogSvc.LogLogin(req.Account, user.ID, ip, true)
|
||||
|
||||
utils.SuccessWithData(c, model.LoginResponse{
|
||||
Token: token,
|
||||
User: *user,
|
||||
}, "登录成功")
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端IP
|
||||
func getClientIP(c *gin.Context) string {
|
||||
// 优先从X-Forwarded-For获取
|
||||
ip := c.GetHeader("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从X-Real-IP获取
|
||||
ip = c.GetHeader("X-Real-IP")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从RemoteAddr获取
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterHandler
|
||||
* 功能:用户注册
|
||||
* 路径:POST /api/register
|
||||
*/
|
||||
func RegisterHandler(c *gin.Context) {
|
||||
var req model.RegisterReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果提供了验证码,验证验证码
|
||||
if req.Code != "" {
|
||||
// 根据邮箱或手机号确定类型
|
||||
codeType := "email"
|
||||
if len(req.Phone) > 0 {
|
||||
codeType = "sms"
|
||||
}
|
||||
target := req.Email
|
||||
if codeType == "sms" {
|
||||
target = req.Phone
|
||||
}
|
||||
|
||||
valid, err := service.AuthSvc.VerifyCode(target, req.Code, codeType)
|
||||
if err != nil || !valid {
|
||||
utils.BadRequest(c, "验证码无效或已过期")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 调用认证服务注册
|
||||
user, err := service.AuthSvc.Register(&req)
|
||||
if err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
token, err := utils.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "生成Token失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, model.RegisterResponse{
|
||||
Token: token,
|
||||
User: *user,
|
||||
}, "注册成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* LogoutHandler
|
||||
* 功能:用户登出(可选,前端清除token)
|
||||
* 路径:POST /api/logout
|
||||
*/
|
||||
func LogoutHandler(c *gin.Context) {
|
||||
// 登出主要是前端清除token,后端可以记录日志或清除session
|
||||
utils.Success(c, "登出成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckTokenHandler
|
||||
* 功能:检查Token有效性
|
||||
* 路径:GET /api/check-token
|
||||
*/
|
||||
func CheckTokenHandler(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
utils.Unauthorized(c, "缺少Token")
|
||||
return
|
||||
}
|
||||
|
||||
// 移除 "Bearer " 前缀(如果存在)
|
||||
if len(token) > 7 && token[:7] == "Bearer " {
|
||||
token = token[7:]
|
||||
}
|
||||
|
||||
userID, err := utils.ValidateToken(token)
|
||||
if err != nil {
|
||||
utils.Unauthorized(c, "Token无效或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"status": "valid",
|
||||
"user_id": userID,
|
||||
}, "Token有效")
|
||||
}
|
||||
|
||||
/**
|
||||
* SendEmailCodeHandler
|
||||
* 功能:发送邮箱验证码
|
||||
* 路径:POST /api/send-email-code
|
||||
*/
|
||||
func SendEmailCodeHandler(c *gin.Context) {
|
||||
var req model.SendCodeReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type != "email" {
|
||||
utils.BadRequest(c, "类型必须是email")
|
||||
return
|
||||
}
|
||||
|
||||
code, err := service.AuthSvc.SendEmailCode(req.Target)
|
||||
if err != nil {
|
||||
utils.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 开发环境返回验证码,生产环境不应返回
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"code": code, // 仅开发环境,生产环境应移除
|
||||
}, "验证码已发送")
|
||||
}
|
||||
|
||||
/**
|
||||
* SendSmsCodeHandler
|
||||
* 功能:发送短信验证码
|
||||
* 路径:POST /api/send-sms-code
|
||||
*/
|
||||
func SendSmsCodeHandler(c *gin.Context) {
|
||||
var req model.SendCodeReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type != "sms" {
|
||||
utils.BadRequest(c, "类型必须是sms")
|
||||
return
|
||||
}
|
||||
|
||||
code, err := service.AuthSvc.SendSmsCode(req.Target)
|
||||
if err != nil {
|
||||
utils.InternalError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 开发环境返回验证码,生产环境不应返回
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"code": code, // 仅开发环境,生产环境应移除
|
||||
}, "验证码已发送")
|
||||
}
|
||||
|
||||
362
internal/api/contact_handler.go
Normal file
362
internal/api/contact_handler.go
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:联系人管理相关API处理器
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* ContactListHandler
|
||||
* 功能:获取联系人列表(已存在,需改为从数据库查询)
|
||||
* 路径:GET /api/contacts
|
||||
*/
|
||||
func ContactListHandler(c *gin.Context) {
|
||||
// 从Context获取用户ID(由JWT中间件注入)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
// 如果没有认证,返回空列表(兼容旧代码)
|
||||
utils.SuccessWithData(c, []interface{}{}, "获取成功")
|
||||
return
|
||||
}
|
||||
|
||||
contacts, err := service.ContactSvc.GetContactsWithUserInfo(userID.(string))
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if contacts == nil {
|
||||
contacts = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, contacts, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchUsersHandler
|
||||
* 功能:搜索用户
|
||||
* 路径:GET /api/contacts/search
|
||||
*/
|
||||
func SearchUsersHandler(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
if keyword == "" {
|
||||
utils.BadRequest(c, "搜索关键词不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
users, err := service.ContactSvc.SearchUsers(keyword, limit)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "搜索失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if users == nil {
|
||||
users = []model.User{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, users, "搜索成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* AddFriendHandler
|
||||
* 功能:添加好友(发送申请)
|
||||
* 路径:POST /api/contacts/add-friend
|
||||
*/
|
||||
func AddFriendHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
ToUserID string `json:"to_user_id" binding:"required"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.AddFriend(userID.(string), req.ToUserID, req.Message); err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "好友申请已发送")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFriendRequestsHandler
|
||||
* 功能:获取好友申请列表
|
||||
* 路径:GET /api/contacts/friend-requests
|
||||
*/
|
||||
func GetFriendRequestsHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
requests, err := service.ContactSvc.GetFriendRequests(userID.(string))
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if requests == nil {
|
||||
requests = []model.FriendRequest{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, requests, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* AcceptFriendRequestHandler
|
||||
* 功能:接受好友申请
|
||||
* 路径:POST /api/contacts/accept-request
|
||||
*/
|
||||
func AcceptFriendRequestHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
RequestID uint `json:"request_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.AcceptFriendRequest(req.RequestID, userID.(string)); err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "已接受好友申请")
|
||||
}
|
||||
|
||||
/**
|
||||
* RejectFriendRequestHandler
|
||||
* 功能:拒绝好友申请
|
||||
* 路径:POST /api/contacts/reject-request
|
||||
*/
|
||||
func RejectFriendRequestHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
RequestID uint `json:"request_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.RejectFriendRequest(req.RequestID, userID.(string)); err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "已拒绝好友申请")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetGroupsHandler
|
||||
* 功能:获取分组列表
|
||||
* 路径:GET /api/contacts/groups
|
||||
*/
|
||||
func GetGroupsHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
groups, err := service.ContactSvc.GetGroups(userID.(string))
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if groups == nil {
|
||||
groups = []model.ContactGroup{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, groups, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateGroupHandler
|
||||
* 功能:创建分组
|
||||
* 路径:POST /api/contacts/groups
|
||||
*/
|
||||
func CreateGroupHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
GroupName string `json:"group_name" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
group, err := service.ContactSvc.CreateGroup(userID.(string), req.GroupName)
|
||||
if err != nil {
|
||||
utils.BadRequest(c, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, group, "创建成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateGroupHandler
|
||||
* 功能:更新分组
|
||||
* 路径:PUT /api/contacts/groups/:id
|
||||
*/
|
||||
func UpdateGroupHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
groupID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
|
||||
var req struct {
|
||||
GroupName string `json:"group_name"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.GroupName != "" {
|
||||
updates["group_name"] = req.GroupName
|
||||
}
|
||||
if req.SortOrder > 0 {
|
||||
updates["sort_order"] = req.SortOrder
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.UpdateGroup(uint(groupID), userID.(string), updates); err != nil {
|
||||
utils.BadRequest(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "更新成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteGroupHandler
|
||||
* 功能:删除分组
|
||||
* 路径:DELETE /api/contacts/groups/:id
|
||||
*/
|
||||
func DeleteGroupHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
groupID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
|
||||
if err := service.ContactSvc.DeleteGroup(uint(groupID), userID.(string)); err != nil {
|
||||
utils.BadRequest(c, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "删除成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetContactDetailHandler
|
||||
* 功能:获取好友详情
|
||||
* 路径:GET /api/contacts/:id
|
||||
*/
|
||||
func GetContactDetailHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
contactID := c.Param("id")
|
||||
|
||||
contact, err := service.ContactSvc.GetContactDetail(userID.(string), contactID)
|
||||
if err != nil {
|
||||
utils.NotFound(c, "好友不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取联系人用户信息
|
||||
user, err := service.UserSvc.GetUserByID(contactID)
|
||||
if err != nil {
|
||||
utils.NotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"contact": contact,
|
||||
"user": user,
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, result, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateContactHandler
|
||||
* 功能:更新好友信息
|
||||
* 路径:PUT /api/contacts/:id
|
||||
*/
|
||||
func UpdateContactHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
contactID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
RemarkName string `json:"remark_name"`
|
||||
GroupID uint `json:"group_id"`
|
||||
IsTop *bool `json:"is_top"`
|
||||
IsMuted *bool `json:"is_muted"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.RemarkName != "" {
|
||||
updates["remark_name"] = req.RemarkName
|
||||
}
|
||||
if req.GroupID > 0 {
|
||||
updates["group_id"] = req.GroupID
|
||||
}
|
||||
if req.IsTop != nil {
|
||||
updates["is_top"] = *req.IsTop
|
||||
}
|
||||
if req.IsMuted != nil {
|
||||
updates["is_muted"] = *req.IsMuted
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.UpdateContact(userID.(string), contactID, updates); err != nil {
|
||||
utils.BadRequest(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "更新成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteContactHandler
|
||||
* 功能:删除好友
|
||||
* 路径:DELETE /api/contacts/:id
|
||||
*/
|
||||
func DeleteContactHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
contactID := c.Param("id")
|
||||
|
||||
if err := service.ContactSvc.DeleteContact(userID.(string), contactID); err != nil {
|
||||
utils.BadRequest(c, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "已删除好友")
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/manager"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/turnserver"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
@@ -33,7 +34,7 @@ func SendHandler(c *gin.Context) {
|
||||
var req model.SendMessageReq
|
||||
// 1. 绑定并校验 JSON 参数
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的JSON参数"})
|
||||
utils.BadRequest(c, "无效的JSON参数")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ func SendHandler(c *gin.Context) {
|
||||
service.ChatSvc.HandleUserMessage(mockClient, &req)
|
||||
|
||||
// 6. 返回成功响应
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
utils.Success(c, "消息已发送")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,12 +90,12 @@ func SendToUserHandler(c *gin.Context) {
|
||||
func BindHandler(c *gin.Context) {
|
||||
var req model.BindReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
// 调用服务层进行绑定
|
||||
service.ChatSvc.BindUserByClientID(req.ClientID, req.UserID)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
utils.Success(c, "绑定成功")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +107,7 @@ func CheckUserOnlineHandler(c *gin.Context) {
|
||||
userID := c.Query("user_id")
|
||||
// 调用服务层查询 Redis
|
||||
isOnline := service.ChatSvc.IsUserOnline(userID)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success", "result": isOnline})
|
||||
utils.SuccessWithData(c, gin.H{"is_online": isOnline}, "查询成功")
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -120,18 +121,52 @@ func CheckUserOnlineHandler(c *gin.Context) {
|
||||
*/
|
||||
func HistoryHandler(c *gin.Context) {
|
||||
roomID := c.Query("room_id")
|
||||
var msgs []model.ChatMessage
|
||||
|
||||
// 简单查询最近 50 条
|
||||
// 生产环境应添加 page, page_size 参数
|
||||
result := service.ChatSvc.DB.Where("room_id = ?", roomID).
|
||||
Order("created_at desc").Limit(50).Find(&msgs)
|
||||
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"})
|
||||
if roomID == "" {
|
||||
utils.BadRequest(c, "room_id参数必填")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": msgs})
|
||||
|
||||
// 分页参数
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 50
|
||||
}
|
||||
|
||||
var msgs []model.ChatMessage
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
service.ChatSvc.DB.Model(&model.ChatMessage{}).Where("room_id = ?", roomID).Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
result := service.ChatSvc.DB.Where("room_id = ?", roomID).
|
||||
Order("created_at desc").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&msgs)
|
||||
|
||||
if result.Error != nil {
|
||||
utils.InternalError(c, "db error")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if msgs == nil {
|
||||
msgs = []model.ChatMessage{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"data": msgs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": pageSize,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,28 +178,6 @@ func SyncMessagesHandler(c *gin.Context) {
|
||||
HistoryHandler(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* ContactListHandler
|
||||
* 功能:获取联系人列表 (模拟数据)。
|
||||
* 路径:GET /api/contacts
|
||||
* 说明:用于前端展示登录后的好友列表。
|
||||
*/
|
||||
func ContactListHandler(c *gin.Context) {
|
||||
// 模拟 10 个用户数据
|
||||
users := []model.UserContact{
|
||||
{ID: "1001", Name: "张三 (我)", Avatar: "张", Desc: "Golang 专家"},
|
||||
{ID: "1002", Name: "李琦 (妻)", Avatar: "李", Desc: "在线"},
|
||||
{ID: "1003", Name: "王医生", Avatar: "医", Desc: "主任医师"},
|
||||
{ID: "1004", Name: "客服小蜜", Avatar: "客", Desc: "全天在线"},
|
||||
{ID: "1005", Name: "技术支持", Avatar: "技", Desc: "请重启试试"},
|
||||
{ID: "1006", Name: "财务小赵", Avatar: "财", Desc: "报销单请提交"},
|
||||
{ID: "1007", Name: "运维阿强", Avatar: "运", Desc: "服务器维护中"},
|
||||
{ID: "1008", Name: "测试小丽", Avatar: "测", Desc: "Bug 太多了"},
|
||||
{ID: "1009", Name: "HR", Avatar: "人", Desc: "本月考勤异常"},
|
||||
{ID: "1010", Name: "老板", Avatar: "老", Desc: "今晚开会"},
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": users})
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 系统与 WebRTC 接口
|
||||
@@ -176,11 +189,11 @@ func ContactListHandler(c *gin.Context) {
|
||||
* 路径:GET /api/health
|
||||
*/
|
||||
func HealthHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"status": "ok",
|
||||
"node": viper.GetString("app.node_id"),
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}, "服务正常")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,5 +218,5 @@ func ICEHandler(c *gin.Context) {
|
||||
Username: username,
|
||||
Credential: credential,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": []model.ICEServerConfig{cfg}})
|
||||
utils.SuccessWithData(c, []model.ICEServerConfig{cfg}, "获取成功")
|
||||
}
|
||||
|
||||
71
internal/api/room_handler.go
Normal file
71
internal/api/room_handler.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:房间管理相关API处理器
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* CreateRoomHandler
|
||||
* 功能:创建房间
|
||||
* 路径:POST /api/rooms
|
||||
*/
|
||||
func CreateRoomHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var req struct {
|
||||
RoomType string `json:"room_type" binding:"required,oneof=p2p group"`
|
||||
Members []string `json:"members" binding:"required"`
|
||||
RoomName string `json:"room_name"`
|
||||
RoomAvatar string `json:"room_avatar"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
room, err := service.RoomSvc.CreateRoom(req.RoomType, req.Members, userID.(string))
|
||||
if err != nil {
|
||||
utils.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 如果提供了房间名称和头像,更新
|
||||
if req.RoomName != "" || req.RoomAvatar != "" {
|
||||
updates := make(map[string]interface{})
|
||||
if req.RoomName != "" {
|
||||
updates["room_name"] = req.RoomName
|
||||
}
|
||||
if req.RoomAvatar != "" {
|
||||
updates["room_avatar"] = req.RoomAvatar
|
||||
}
|
||||
service.RoomSvc.DB.Model(room).Updates(updates)
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, room, "创建成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetRoomHandler
|
||||
* 功能:获取房间信息
|
||||
* 路径:GET /api/rooms/:id
|
||||
*/
|
||||
func GetRoomHandler(c *gin.Context) {
|
||||
roomID := c.Param("id")
|
||||
|
||||
room, err := service.RoomSvc.GetRoom(roomID)
|
||||
if err != nil {
|
||||
utils.NotFound(c, "房间不存在")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, room, "获取成功")
|
||||
}
|
||||
|
||||
155
internal/api/user_handler.go
Normal file
155
internal/api/user_handler.go
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:用户管理相关API处理器
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* GetMyInfoHandler
|
||||
* 功能:获取当前用户信息
|
||||
* 路径:GET /api/user/my-info
|
||||
* 需要:JWT认证
|
||||
*/
|
||||
func GetMyInfoHandler(c *gin.Context) {
|
||||
// 从Context获取用户ID(由JWT中间件注入)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
utils.Unauthorized(c, "未认证")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := service.UserSvc.GetUserByID(userID.(string))
|
||||
if err != nil {
|
||||
utils.NotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, user, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserListHandler
|
||||
* 功能:获取用户列表
|
||||
* 路径:GET /api/user/list
|
||||
*/
|
||||
func GetUserListHandler(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
users, total, err := service.UserSvc.GetUserList(page, pageSize)
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 确保返回空数组而不是null
|
||||
if users == nil {
|
||||
users = []model.User{}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"data": users,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": pageSize,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateUserHandler
|
||||
* 功能:创建用户(管理员)
|
||||
* 路径:POST /api/user/create
|
||||
*/
|
||||
func CreateUserHandler(c *gin.Context) {
|
||||
var user model.User
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成用户ID
|
||||
user.ID = generateUserID()
|
||||
|
||||
if err := service.UserSvc.CreateUser(&user); err != nil {
|
||||
utils.BadRequest(c, "创建用户失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = "" // 清除密码
|
||||
utils.SuccessWithData(c, user, "创建成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateUserHandler
|
||||
* 功能:更新用户信息
|
||||
* 路径:POST /api/user/update
|
||||
*/
|
||||
func UpdateUserHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
Updates map[string]interface{} `json:"updates" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.UserSvc.UpdateUser(req.ID, req.Updates); err != nil {
|
||||
utils.BadRequest(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "更新成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteUserHandler
|
||||
* 功能:删除用户
|
||||
* 路径:POST /api/user/delete
|
||||
*/
|
||||
func DeleteUserHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.UserSvc.DeleteUser(req.ID); err != nil {
|
||||
utils.BadRequest(c, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.Success(c, "删除成功")
|
||||
}
|
||||
|
||||
// 辅助函数:生成用户ID
|
||||
func generateUserID() string {
|
||||
// 使用时间戳+随机数生成用户ID
|
||||
timestamp := time.Now().UnixNano()
|
||||
random := rand.Intn(1000000)
|
||||
return fmt.Sprintf("user_%d_%d", timestamp, random)
|
||||
}
|
||||
|
||||
112
internal/middleware/auth.go
Normal file
112
internal/middleware/auth.go
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* package middleware
|
||||
* 作用:JWT认证中间件
|
||||
*/
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* JWTAuthMiddleware
|
||||
*
|
||||
* 功能:JWT认证中间件
|
||||
*
|
||||
* 作用:
|
||||
* 1. 从HTTP请求头或查询参数中提取JWT Token
|
||||
* 2. 验证Token的有效性和过期时间
|
||||
* 3. 从Token中解析出用户ID
|
||||
* 4. 将用户ID注入到Gin Context中,供后续处理器使用
|
||||
* 5. 如果Token无效或缺失,返回401未授权错误
|
||||
*
|
||||
* 使用场景:
|
||||
* - 需要用户登录才能访问的API接口
|
||||
* - 需要在处理器中获取当前用户信息的接口
|
||||
*
|
||||
* @returns gin.HandlerFunc 中间件处理函数
|
||||
*/
|
||||
func JWTAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 步骤1: 从HTTP请求头中获取Token(标准方式)
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
// 步骤2: 如果请求头中没有,尝试从查询参数获取(兼容旧代码)
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
// 步骤3: 如果仍然没有Token,返回401未授权错误
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证Token"})
|
||||
c.Abort() // 终止请求处理
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤4: 移除 "Bearer " 前缀(如果存在)
|
||||
// 标准JWT Token格式:Bearer <token>
|
||||
if len(token) > 7 && token[:7] == "Bearer " {
|
||||
token = token[7:]
|
||||
}
|
||||
|
||||
// 步骤5: 验证Token的有效性和过期时间
|
||||
userID, err := utils.ValidateToken(token)
|
||||
if err != nil {
|
||||
// Token无效或已过期,返回401错误
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token无效或已过期: " + err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤6: 将解析出的用户ID注入到Context中
|
||||
// 后续处理器可以通过 c.Get("user_id") 获取当前用户ID
|
||||
c.Set("user_id", userID)
|
||||
c.Next() // 继续执行下一个中间件或处理器
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OptionalJWTAuthMiddleware
|
||||
*
|
||||
* 功能:可选的JWT认证中间件(不强制要求认证)
|
||||
*
|
||||
* 作用:
|
||||
* 1. 如果请求中提供了Token,则验证Token并注入用户ID
|
||||
* 2. 如果没有提供Token,则继续执行,不返回错误
|
||||
* 3. 适用于既支持登录用户访问,也支持匿名用户访问的接口
|
||||
*
|
||||
* 使用场景:
|
||||
* - 公开接口,但登录用户可以获取更多信息
|
||||
* - 兼容旧代码,不强制要求认证
|
||||
*
|
||||
* @returns gin.HandlerFunc 中间件处理函数
|
||||
*/
|
||||
func OptionalJWTAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 步骤1: 尝试从请求头或查询参数获取Token
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
|
||||
// 步骤2: 如果提供了Token,则验证并注入用户ID
|
||||
if token != "" {
|
||||
// 移除 "Bearer " 前缀
|
||||
if len(token) > 7 && token[:7] == "Bearer " {
|
||||
token = token[7:]
|
||||
}
|
||||
|
||||
// 验证Token,如果有效则注入用户ID
|
||||
// 如果Token无效,不返回错误,继续执行(允许匿名访问)
|
||||
if userID, err := utils.ValidateToken(token); err == nil {
|
||||
c.Set("user_id", userID)
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤3: 继续执行,无论是否有Token
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
173
internal/middleware/request_log.go
Normal file
173
internal/middleware/request_log.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* package middleware
|
||||
* 作用:接口请求日志中间件
|
||||
* 说明:记录所有API请求信息,异步写入数据库
|
||||
*/
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RequestLogMiddleware 请求日志中间件
|
||||
type RequestLogMiddleware struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// NewRequestLogMiddleware 创建请求日志中间件
|
||||
func NewRequestLogMiddleware(db *gorm.DB) *RequestLogMiddleware {
|
||||
return &RequestLogMiddleware{DB: db}
|
||||
}
|
||||
|
||||
// Handler 中间件处理函数
|
||||
func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 跳过OPTIONS请求
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
ip := getClientIP(c)
|
||||
|
||||
// 本地IP不记录
|
||||
if utils.GetIPLocation(ip) == "本地" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
var requestBody []byte
|
||||
if c.Request.Body != nil {
|
||||
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
}
|
||||
|
||||
// 获取用户ID(从Context中获取,未登录为"0")
|
||||
userID := "0"
|
||||
if uid, exists := c.Get("user_id"); exists {
|
||||
if uidStr, ok := uid.(string); ok {
|
||||
userID = uidStr
|
||||
}
|
||||
}
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 创建响应写入器
|
||||
writer := &responseWriter{
|
||||
ResponseWriter: c.Writer,
|
||||
body: &bytes.Buffer{},
|
||||
}
|
||||
c.Writer = writer
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算请求时间
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 异步记录日志(避免影响性能)
|
||||
go m.logRequest(c, ip, userID, requestBody, writer.body.Bytes(), writer.status, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// logRequest 记录请求日志
|
||||
func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, requestBody, responseBody []byte, httpStatus int, duration time.Duration) {
|
||||
// 获取IP归属地
|
||||
location := utils.GetIPLocation(ip)
|
||||
|
||||
// 获取响应code(从响应体中解析)
|
||||
responseCode := 0
|
||||
if len(responseBody) > 0 {
|
||||
// 尝试解析响应体获取code
|
||||
var resp model.ApiResponse
|
||||
if err := json.Unmarshal(responseBody, &resp); err == nil {
|
||||
responseCode = resp.Code
|
||||
}
|
||||
}
|
||||
|
||||
// 限制请求参数长度(避免存储过大)
|
||||
requestParams := string(requestBody)
|
||||
if len(requestParams) > 5000 {
|
||||
requestParams = requestParams[:5000] + "...(truncated)"
|
||||
}
|
||||
|
||||
// 限制返回参数长度
|
||||
responseParams := string(responseBody)
|
||||
if len(responseParams) > 5000 {
|
||||
responseParams = responseParams[:5000] + "...(truncated)"
|
||||
}
|
||||
|
||||
// 创建日志记录
|
||||
log := model.ApiRequestLog{
|
||||
Route: c.FullPath(),
|
||||
IP: ip,
|
||||
IPLocation: location,
|
||||
UserID: userID,
|
||||
Method: c.Request.Method,
|
||||
RequestParams: requestParams,
|
||||
ResponseParams: responseParams,
|
||||
ResponseCode: responseCode,
|
||||
HTTPStatus: httpStatus,
|
||||
RequestTime: time.Now(),
|
||||
}
|
||||
|
||||
// 异步写入数据库
|
||||
m.DB.Create(&log)
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端IP
|
||||
func getClientIP(c *gin.Context) string {
|
||||
// 优先从X-Forwarded-For获取
|
||||
ip := c.GetHeader("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
ips := strings.Split(ip, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 从X-Real-IP获取
|
||||
ip = c.GetHeader("X-Real-IP")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从RemoteAddr获取
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
// responseWriter 响应写入器(用于捕获响应内容)
|
||||
type responseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteString(s string) (int, error) {
|
||||
w.body.WriteString(s)
|
||||
return w.ResponseWriter.WriteString(s)
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(statusCode int) {
|
||||
w.status = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
26
internal/middleware/response.go
Normal file
26
internal/middleware/response.go
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* package middleware
|
||||
* 作用:响应时间统计中间件
|
||||
* 说明:记录请求开始时间,用于计算响应时间
|
||||
*/
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* ResponseTimeMiddleware
|
||||
* 作用:记录请求开始时间,用于后续计算响应时间
|
||||
*/
|
||||
func ResponseTimeMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 记录请求开始时间
|
||||
c.Set("request_start_time", time.Now())
|
||||
// 继续处理请求
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
28
internal/model/response.go
Normal file
28
internal/model/response.go
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* package model
|
||||
* 作用:定义统一的API响应格式
|
||||
*/
|
||||
package model
|
||||
|
||||
/**
|
||||
* InterfaceInfo
|
||||
* 作用:接口信息,包含响应时间和服务器标识
|
||||
*/
|
||||
type InterfaceInfo struct {
|
||||
ResultTime string `json:"result_time"` // 响应时间,格式:XX ms
|
||||
Ecs string `json:"ecs"` // 服务器标识
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiResponse
|
||||
* 作用:统一的API响应结构体
|
||||
* 说明:所有API响应都使用此格式,HTTP状态码统一返回200,错误通过code字段标识
|
||||
*/
|
||||
type ApiResponse struct {
|
||||
Code int `json:"code"` // 业务状态码:0=成功,非0=失败
|
||||
Message string `json:"message"` // 响应消息
|
||||
Result interface{} `json:"result"` // 响应数据
|
||||
Type string `json:"type"` // 响应类型:"success" 或 "error"
|
||||
InterfaceInfo InterfaceInfo `json:"interface_info"` // 接口信息
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ type ChatMessage struct {
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (ChatMessage) TableName() string {
|
||||
return "chat_messages"
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 交互数据传输对象 (DTO - Data Transfer Object)
|
||||
// ==========================================
|
||||
@@ -125,19 +130,281 @@ type ICEServerConfig struct {
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
/**
|
||||
* User
|
||||
* 对应数据库表:users
|
||||
* 作用:用户基本信息表
|
||||
*/
|
||||
type User struct {
|
||||
// 用户唯一ID
|
||||
ID string `gorm:"primaryKey;type:varchar(100);comment:用户唯一ID" json:"id"`
|
||||
// 邮箱
|
||||
Email string `gorm:"type:varchar(255);uniqueIndex;comment:邮箱" json:"email"`
|
||||
// 手机号
|
||||
Phone string `gorm:"type:varchar(20);uniqueIndex;comment:手机号" json:"phone"`
|
||||
// 密码(加密后)
|
||||
Password string `gorm:"type:varchar(255);comment:密码(加密后)" json:"-"`
|
||||
// 用户名称
|
||||
Name string `gorm:"type:varchar(100);comment:用户名称" json:"name"`
|
||||
// 用户头像URL或字符
|
||||
Avatar string `gorm:"type:varchar(500);comment:用户头像" json:"avatar"`
|
||||
// 用户描述或签名
|
||||
Desc string `gorm:"type:varchar(500);comment:用户描述或签名" json:"desc"`
|
||||
// 地区
|
||||
Region string `gorm:"type:varchar(100);comment:地区" json:"region"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
// 更新时间
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
/**
|
||||
* UserContact
|
||||
* 作用:模拟联系人结构
|
||||
* 对应数据库表:user_contacts
|
||||
* 作用:用户联系人表,存储好友关系、分组、备注等信息
|
||||
*/
|
||||
type UserContact struct {
|
||||
// 用户唯一ID
|
||||
ID string `json:"id"`
|
||||
// 用户名称
|
||||
Name string `json:"name"`
|
||||
// 用户头像URL或字符
|
||||
Avatar string `json:"avatar"`
|
||||
// 用户描述或签名
|
||||
Desc string `json:"desc"`
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 用户ID - 当前用户
|
||||
UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"`
|
||||
// 联系人ID - 好友的用户ID
|
||||
ContactID string `gorm:"type:varchar(100);index;comment:联系人ID" json:"contact_id"`
|
||||
// 备注名称 - 用户自定义的好友备注
|
||||
RemarkName string `gorm:"type:varchar(100);comment:备注名称" json:"remark_name"`
|
||||
// 分组ID - 好友所属分组
|
||||
GroupID uint `gorm:"type:int;index;comment:分组ID" json:"group_id"`
|
||||
// 是否置顶
|
||||
IsTop bool `gorm:"type:tinyint(1);default:0;comment:是否置顶" json:"is_top"`
|
||||
// 是否免打扰
|
||||
IsMuted bool `gorm:"type:tinyint(1);default:0;comment:是否免打扰" json:"is_muted"`
|
||||
// 最后聊天时间
|
||||
LastChatTime *time.Time `gorm:"type:datetime;comment:最后聊天时间" json:"last_chat_time"`
|
||||
// 最后一条消息
|
||||
LastMessage string `gorm:"type:text;comment:最后一条消息" json:"last_message"`
|
||||
// 未读消息数
|
||||
UnreadCount int `gorm:"type:int;default:0;comment:未读消息数" json:"unread_count"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
// 更新时间
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (UserContact) TableName() string {
|
||||
return "user_contacts"
|
||||
}
|
||||
|
||||
/**
|
||||
* ContactGroup
|
||||
* 对应数据库表:contact_groups
|
||||
* 作用:联系人分组表
|
||||
*/
|
||||
type ContactGroup struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 用户ID - 分组所属用户
|
||||
UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"`
|
||||
// 分组名称
|
||||
GroupName string `gorm:"type:varchar(100);comment:分组名称" json:"group_name"`
|
||||
// 排序顺序
|
||||
SortOrder int `gorm:"type:int;default:0;comment:排序顺序" json:"sort_order"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (ContactGroup) TableName() string {
|
||||
return "contact_groups"
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatRoom
|
||||
* 对应数据库表:chat_rooms
|
||||
* 作用:聊天房间表,支持点对点和群聊
|
||||
*/
|
||||
type ChatRoom struct {
|
||||
// 房间ID - 主键
|
||||
RoomID string `gorm:"primaryKey;type:varchar(100);comment:房间ID" json:"room_id"`
|
||||
// 房间类型 - "p2p" 点对点 / "group" 群聊
|
||||
RoomType string `gorm:"type:varchar(20);index;comment:房间类型" json:"room_type"`
|
||||
// 房间名称 - 群聊时显示
|
||||
RoomName string `gorm:"type:varchar(200);comment:房间名称" json:"room_name"`
|
||||
// 房间头像 - 群聊时显示
|
||||
RoomAvatar string `gorm:"type:varchar(500);comment:房间头像" json:"room_avatar"`
|
||||
// 成员列表 - JSON数组
|
||||
Members string `gorm:"type:text;comment:成员列表JSON" json:"members"`
|
||||
// 创建者ID
|
||||
CreatorID string `gorm:"type:varchar(100);index;comment:创建者ID" json:"creator_id"`
|
||||
// 最后消息时间
|
||||
LastMessageTime *time.Time `gorm:"type:datetime;index;comment:最后消息时间" json:"last_message_time"`
|
||||
// 最后消息内容
|
||||
LastMessage string `gorm:"type:text;comment:最后消息内容" json:"last_message"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
// 更新时间
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (ChatRoom) TableName() string {
|
||||
return "chat_rooms"
|
||||
}
|
||||
|
||||
/**
|
||||
* FriendRequest
|
||||
* 对应数据库表:friend_requests
|
||||
* 作用:好友申请表
|
||||
*/
|
||||
type FriendRequest struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 发送者用户ID
|
||||
FromUserID string `gorm:"type:varchar(100);index;comment:发送者用户ID" json:"from_user_id"`
|
||||
// 接收者用户ID
|
||||
ToUserID string `gorm:"type:varchar(100);index;comment:接收者用户ID" json:"to_user_id"`
|
||||
// 申请消息
|
||||
Message string `gorm:"type:text;comment:申请消息" json:"message"`
|
||||
// 状态 - "pending", "accepted", "rejected"
|
||||
Status string `gorm:"type:varchar(20);default:'pending';index;comment:状态" json:"status"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
// 更新时间
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (FriendRequest) TableName() string {
|
||||
return "friend_requests"
|
||||
}
|
||||
|
||||
/**
|
||||
* VerificationCode
|
||||
* 对应数据库表:verification_codes
|
||||
* 作用:验证码表
|
||||
*/
|
||||
type VerificationCode struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 邮箱或手机号
|
||||
Target string `gorm:"type:varchar(255);index;comment:邮箱或手机号" json:"target"`
|
||||
// 验证码
|
||||
Code string `gorm:"type:varchar(10);comment:验证码" json:"-"`
|
||||
// 类型 - "email" 或 "sms"
|
||||
Type string `gorm:"type:varchar(20);comment:类型" json:"type"`
|
||||
// 过期时间
|
||||
ExpiresAt time.Time `gorm:"type:datetime;index;comment:过期时间" json:"expires_at"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (VerificationCode) TableName() string {
|
||||
return "verification_codes"
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachment
|
||||
* 对应数据库表:attachments
|
||||
* 作用:附件表,记录上传的文件信息
|
||||
*/
|
||||
type Attachment struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 上传者ID
|
||||
UploaderID string `gorm:"type:varchar(100);index;comment:上传者ID" json:"uploader_id"`
|
||||
// 文件名
|
||||
FileName string `gorm:"type:varchar(255);comment:文件名" json:"file_name"`
|
||||
// 文件类型 - "image" 或 "video"
|
||||
FileType string `gorm:"type:varchar(20);index;comment:文件类型" json:"file_type"`
|
||||
// 文件大小(字节)
|
||||
FileSize int64 `gorm:"type:bigint;comment:文件大小(字节)" json:"file_size"`
|
||||
// 文件路径
|
||||
FilePath string `gorm:"type:varchar(500);comment:文件路径" json:"file_path"`
|
||||
// 文件URL
|
||||
FileURL string `gorm:"type:varchar(500);comment:文件URL" json:"file_url"`
|
||||
// MIME类型
|
||||
MimeType string `gorm:"type:varchar(100);comment:MIME类型" json:"mime_type"`
|
||||
// 宽度(图片/视频)
|
||||
Width int `gorm:"type:int;comment:宽度" json:"width"`
|
||||
// 高度(图片/视频)
|
||||
Height int `gorm:"type:int;comment:高度" json:"height"`
|
||||
// 时长(视频,秒)
|
||||
Duration int `gorm:"type:int;comment:时长(秒)" json:"duration"`
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (Attachment) TableName() string {
|
||||
return "attachments"
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiRequestLog
|
||||
* 对应数据库表:api_request_logs
|
||||
* 作用:接口请求日志表,记录所有API请求信息
|
||||
*/
|
||||
type ApiRequestLog struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 请求路由
|
||||
Route string `gorm:"type:varchar(255);index;comment:请求路由" json:"route"`
|
||||
// 请求IP
|
||||
IP string `gorm:"type:varchar(50);index;comment:请求IP" json:"ip"`
|
||||
// IP归属地
|
||||
IPLocation string `gorm:"type:varchar(255);comment:IP归属地" json:"ip_location"`
|
||||
// 请求用户ID(未登录为0)
|
||||
UserID string `gorm:"type:varchar(100);index;comment:请求用户ID" json:"user_id"`
|
||||
// 请求方式
|
||||
Method string `gorm:"type:varchar(10);comment:请求方式" json:"method"`
|
||||
// 请求参数(JSON)
|
||||
RequestParams string `gorm:"type:text;comment:请求参数" json:"request_params"`
|
||||
// 返回参数(JSON)
|
||||
ResponseParams string `gorm:"type:text;comment:返回参数" json:"response_params"`
|
||||
// 返回code
|
||||
ResponseCode int `gorm:"type:int;index;comment:返回code" json:"response_code"`
|
||||
// 返回http状态
|
||||
HTTPStatus int `gorm:"type:int;comment:返回http状态" json:"http_status"`
|
||||
// 请求时间
|
||||
RequestTime time.Time `gorm:"type:datetime;index;comment:请求时间" json:"request_time"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (ApiRequestLog) TableName() string {
|
||||
return "api_request_logs"
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginLog
|
||||
* 对应数据库表:login_logs
|
||||
* 作用:登录日志表,记录所有登录尝试
|
||||
*/
|
||||
type LoginLog struct {
|
||||
// 主键ID
|
||||
ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
|
||||
// 登录账号
|
||||
Account string `gorm:"type:varchar(255);index;comment:登录账号" json:"account"`
|
||||
// 登录ID(失败记录0)
|
||||
UserID string `gorm:"type:varchar(100);index;comment:登录ID" json:"user_id"`
|
||||
// 归属地(IP归属地)
|
||||
Location string `gorm:"type:varchar(255);comment:归属地" json:"location"`
|
||||
// 地址(IP地址)
|
||||
IP string `gorm:"type:varchar(50);index;comment:IP地址" json:"ip"`
|
||||
// 成功/失败状态(true=成功,false=失败)
|
||||
Success bool `gorm:"type:tinyint(1);index;comment:成功状态" json:"success"`
|
||||
// 登录时间
|
||||
LoginTime time.Time `gorm:"type:datetime;index;comment:登录时间" json:"login_time"`
|
||||
}
|
||||
|
||||
// TableName 指定表名和注释
|
||||
func (LoginLog) TableName() string {
|
||||
return "login_logs"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +420,75 @@ type ClusterMessage struct {
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 认证相关 DTO
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* LoginReq
|
||||
* 作用:登录请求参数
|
||||
*/
|
||||
type LoginReq struct {
|
||||
// 账号(邮箱或手机号)
|
||||
Account string `json:"account" binding:"required"`
|
||||
// 密码
|
||||
Password string `json:"password" binding:"required"`
|
||||
// 记住我
|
||||
Remember bool `json:"remember,omitempty"`
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterReq
|
||||
* 作用:注册请求参数
|
||||
*/
|
||||
type RegisterReq struct {
|
||||
// 邮箱
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
// 手机号
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
// 密码
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
// 确认密码
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required"`
|
||||
// 验证码(可选)
|
||||
Code string `json:"code,omitempty"`
|
||||
// 同意用户协议
|
||||
AgreeTerms bool `json:"agree_terms" binding:"required"`
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginResponse
|
||||
* 作用:登录响应
|
||||
*/
|
||||
type LoginResponse struct {
|
||||
// Token
|
||||
Token string `json:"token"`
|
||||
// 用户信息
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterResponse
|
||||
* 作用:注册响应
|
||||
*/
|
||||
type RegisterResponse struct {
|
||||
// Token
|
||||
Token string `json:"token"`
|
||||
// 用户信息
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
||||
/**
|
||||
* SendCodeReq
|
||||
* 作用:发送验证码请求
|
||||
*/
|
||||
type SendCodeReq struct {
|
||||
// 邮箱或手机号
|
||||
Target string `json:"target" binding:"required"`
|
||||
// 类型 - "email" 或 "sms"
|
||||
Type string `json:"type" binding:"required,oneof=email sms"`
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 常量定义
|
||||
// ==========================================
|
||||
@@ -162,4 +498,8 @@ const (
|
||||
KeyUserNodeMap = "ws:user:node:"
|
||||
// Redis Channel: 集群广播频道
|
||||
ChanClusterBroadcast = "ws:cluster:broadcast"
|
||||
|
||||
// 文件大小限制(字节)
|
||||
MaxImageSize = 10 * 1024 * 1024 // 10MB
|
||||
MaxVideoSize = 500 * 1024 * 1024 // 500MB
|
||||
)
|
||||
|
||||
207
internal/service/attachment_service.go
Normal file
207
internal/service/attachment_service.go
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:附件管理服务
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AttachmentService 附件服务结构体
|
||||
type AttachmentService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// AttachmentSvc 全局单例
|
||||
var AttachmentSvc *AttachmentService
|
||||
|
||||
/**
|
||||
* InitAttachmentService
|
||||
* 功能:初始化附件服务
|
||||
*/
|
||||
func InitAttachmentService(db *gorm.DB) {
|
||||
AttachmentSvc = &AttachmentService{DB: db}
|
||||
// 创建上传目录
|
||||
os.MkdirAll("./uploads/images", os.ModePerm)
|
||||
os.MkdirAll("./uploads/videos", os.ModePerm)
|
||||
}
|
||||
|
||||
/**
|
||||
* UploadFile
|
||||
* 功能:上传文件
|
||||
* @param userID 上传者ID
|
||||
* @param fileName 文件名
|
||||
* @param fileType 文件类型(image/video)
|
||||
* @param fileSize 文件大小
|
||||
* @param fileData 文件数据
|
||||
* @returns 附件信息和错误
|
||||
*/
|
||||
func (s *AttachmentService) UploadFile(userID, fileName, fileType string, fileSize int64, fileData io.Reader) (*model.Attachment, error) {
|
||||
// 验证文件类型
|
||||
if fileType != "image" && fileType != "video" {
|
||||
return nil, errors.New("文件类型必须是image或video")
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if fileType == "image" && fileSize > model.MaxImageSize {
|
||||
return nil, fmt.Errorf("图片大小不能超过%dMB", model.MaxImageSize/(1024*1024))
|
||||
}
|
||||
if fileType == "video" && fileSize > model.MaxVideoSize {
|
||||
return nil, fmt.Errorf("视频大小不能超过%dMB", model.MaxVideoSize/(1024*1024))
|
||||
}
|
||||
|
||||
// 验证文件扩展名
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
allowedExts := s.getAllowedExtensions(fileType)
|
||||
if !contains(allowedExts, ext) {
|
||||
return nil, fmt.Errorf("不支持的文件类型,允许的类型: %v", allowedExts)
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
timestamp := time.Now().UnixNano()
|
||||
randomStr := fmt.Sprintf("%d", timestamp%1000000)
|
||||
newFileName := fmt.Sprintf("%d_%s%s", timestamp, randomStr, ext)
|
||||
|
||||
// 确定保存路径
|
||||
var saveDir string
|
||||
if fileType == "image" {
|
||||
saveDir = "./uploads/images"
|
||||
} else {
|
||||
saveDir = "./uploads/videos"
|
||||
}
|
||||
|
||||
filePath := filepath.Join(saveDir, newFileName)
|
||||
|
||||
// 保存文件
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, fileData)
|
||||
if err != nil {
|
||||
os.Remove(filePath) // 删除失败的文件
|
||||
return nil, fmt.Errorf("保存文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成访问URL
|
||||
fileURL := fmt.Sprintf("/uploads/%s/%s", fileType+"s", newFileName)
|
||||
|
||||
// 获取MIME类型
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// 创建附件记录
|
||||
attachment := model.Attachment{
|
||||
UploaderID: userID,
|
||||
FileName: fileName,
|
||||
FileType: fileType,
|
||||
FileSize: fileSize,
|
||||
FilePath: filePath,
|
||||
FileURL: fileURL,
|
||||
MimeType: mimeType,
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&attachment).Error; err != nil {
|
||||
os.Remove(filePath) // 删除文件
|
||||
return nil, fmt.Errorf("保存附件记录失败: %v", err)
|
||||
}
|
||||
|
||||
return &attachment, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAttachment
|
||||
* 功能:获取附件信息
|
||||
*/
|
||||
func (s *AttachmentService) GetAttachment(attachmentID uint) (*model.Attachment, error) {
|
||||
var attachment model.Attachment
|
||||
result := s.DB.First(&attachment, attachmentID)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &attachment, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteAttachment
|
||||
* 功能:删除附件(验证上传者权限)
|
||||
*/
|
||||
func (s *AttachmentService) DeleteAttachment(attachmentID uint, userID string) error {
|
||||
var attachment model.Attachment
|
||||
if err := s.DB.First(&attachment, attachmentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证权限
|
||||
if attachment.UploaderID != userID {
|
||||
return errors.New("无权删除此附件")
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if _, err := os.Stat(attachment.FilePath); err == nil {
|
||||
os.Remove(attachment.FilePath)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
return s.DB.Delete(&attachment).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserAttachments
|
||||
* 功能:获取用户附件列表
|
||||
*/
|
||||
func (s *AttachmentService) GetUserAttachments(userID, fileType string, page, pageSize int) ([]model.Attachment, int64, error) {
|
||||
var attachments []model.Attachment
|
||||
var total int64
|
||||
|
||||
query := s.DB.Where("uploader_id = ?", userID)
|
||||
if fileType != "" {
|
||||
query = query.Where("file_type = ?", fileType)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Model(&model.Attachment{}).Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
result := query.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&attachments)
|
||||
|
||||
return attachments, total, result.Error
|
||||
}
|
||||
|
||||
// 辅助函数:获取允许的文件扩展名
|
||||
func (s *AttachmentService) getAllowedExtensions(fileType string) []string {
|
||||
if fileType == "image" {
|
||||
return []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
}
|
||||
return []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv"}
|
||||
}
|
||||
|
||||
// 辅助函数:检查字符串是否在切片中
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
235
internal/service/auth_service.go
Normal file
235
internal/service/auth_service.go
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:认证服务,处理登录、注册、验证码等功能
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuthService 认证服务结构体
|
||||
type AuthService struct {
|
||||
DB *gorm.DB
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
// AuthSvc 全局单例
|
||||
var AuthSvc *AuthService
|
||||
|
||||
/**
|
||||
* InitAuthService
|
||||
* 功能:初始化认证服务
|
||||
*/
|
||||
func InitAuthService(db *gorm.DB, rdb *redis.Client) {
|
||||
AuthSvc = &AuthService{DB: db, Redis: rdb}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
* 功能:用户登录验证
|
||||
* @param account 账号(邮箱或手机号)
|
||||
* @param password 密码
|
||||
* @returns 用户信息和错误
|
||||
*/
|
||||
func (s *AuthService) Login(account, password string) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
// 根据邮箱或手机号查询用户
|
||||
result := s.DB.Where("email = ? OR phone = ?", account, account).First(&user)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !utils.CheckPassword(password, user.Password) {
|
||||
return nil, errors.New("密码错误")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Register
|
||||
* 功能:用户注册
|
||||
* @param req 注册请求
|
||||
* @returns 用户信息和错误
|
||||
*/
|
||||
func (s *AuthService) Register(req *model.RegisterReq) (*model.User, error) {
|
||||
// 验证密码一致性
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return nil, errors.New("两次密码输入不一致")
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
var existingUser model.User
|
||||
if err := s.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("邮箱已被注册")
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if err := s.DB.Where("phone = ?", req.Phone).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("手机号已被注册")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := utils.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("密码加密失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成用户ID(使用时间戳+随机数)
|
||||
userID := fmt.Sprintf("%d%s", time.Now().UnixNano(), generateRandomString(6))
|
||||
|
||||
// 生成默认名称(使用邮箱前缀)
|
||||
defaultName := req.Email
|
||||
if atIndex := strings.Index(req.Email, "@"); atIndex > 0 {
|
||||
defaultName = req.Email[:atIndex]
|
||||
}
|
||||
if len(defaultName) > 20 {
|
||||
defaultName = defaultName[:20]
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := model.User{
|
||||
ID: userID,
|
||||
Email: req.Email,
|
||||
Phone: req.Phone,
|
||||
Password: hashedPassword,
|
||||
Name: defaultName,
|
||||
Avatar: generateAvatar(userID),
|
||||
Desc: "",
|
||||
Region: "",
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* SendEmailCode
|
||||
* 功能:发送邮箱验证码(模拟)
|
||||
* @param email 邮箱
|
||||
* @returns 验证码和错误
|
||||
*/
|
||||
func (s *AuthService) SendEmailCode(email string) (string, error) {
|
||||
// 生成6位验证码
|
||||
code := generateCode(6)
|
||||
|
||||
// 保存验证码到数据库(5分钟过期)
|
||||
vc := model.VerificationCode{
|
||||
Target: email,
|
||||
Code: code,
|
||||
Type: "email",
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&vc).Error; err != nil {
|
||||
return "", fmt.Errorf("保存验证码失败: %v", err)
|
||||
}
|
||||
|
||||
// 模拟发送(实际应调用邮件服务)
|
||||
// 这里直接返回验证码,生产环境应通过邮件发送
|
||||
return code, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* SendSmsCode
|
||||
* 功能:发送短信验证码(模拟)
|
||||
* @param phone 手机号
|
||||
* @returns 验证码和错误
|
||||
*/
|
||||
func (s *AuthService) SendSmsCode(phone string) (string, error) {
|
||||
// 生成6位验证码
|
||||
code := generateCode(6)
|
||||
|
||||
// 保存验证码到数据库(5分钟过期)
|
||||
vc := model.VerificationCode{
|
||||
Target: phone,
|
||||
Code: code,
|
||||
Type: "sms",
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&vc).Error; err != nil {
|
||||
return "", fmt.Errorf("保存验证码失败: %v", err)
|
||||
}
|
||||
|
||||
// 模拟发送(实际应调用短信服务)
|
||||
// 这里直接返回验证码,生产环境应通过短信发送
|
||||
return code, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* VerifyCode
|
||||
* 功能:验证验证码
|
||||
* @param target 邮箱或手机号
|
||||
* @param code 验证码
|
||||
* @param codeType 类型(email/sms)
|
||||
* @returns 是否有效
|
||||
*/
|
||||
func (s *AuthService) VerifyCode(target, code, codeType string) (bool, error) {
|
||||
var vc model.VerificationCode
|
||||
|
||||
// 查询验证码
|
||||
result := s.DB.Where("target = ? AND code = ? AND type = ? AND expires_at > ?",
|
||||
target, code, codeType, time.Now()).
|
||||
Order("created_at DESC").
|
||||
First(&vc)
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return false, errors.New("验证码无效或已过期")
|
||||
}
|
||||
return false, result.Error
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 辅助函数:生成随机字符串
|
||||
func generateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)[:length]
|
||||
}
|
||||
|
||||
// 辅助函数:生成验证码
|
||||
func generateCode(length int) string {
|
||||
code := ""
|
||||
for i := 0; i < length; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(10))
|
||||
code += n.String()
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// 辅助函数:生成默认头像
|
||||
func generateAvatar(userID string) string {
|
||||
// 简单实现:使用用户ID的第一个字符
|
||||
if len(userID) > 0 {
|
||||
return string(userID[0])
|
||||
}
|
||||
return "U"
|
||||
}
|
||||
|
||||
310
internal/service/contact_service.go
Normal file
310
internal/service/contact_service.go
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:联系人管理服务
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"xk-websocket-v2/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ContactService 联系人服务结构体
|
||||
type ContactService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// ContactSvc 全局单例
|
||||
var ContactSvc *ContactService
|
||||
|
||||
/**
|
||||
* InitContactService
|
||||
* 功能:初始化联系人服务
|
||||
*/
|
||||
func InitContactService(db *gorm.DB) {
|
||||
ContactSvc = &ContactService{DB: db}
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchUsers
|
||||
* 功能:搜索用户(按用户名、ID、邮箱、手机号)
|
||||
*/
|
||||
func (s *ContactService) SearchUsers(keyword string, limit int) ([]model.User, error) {
|
||||
return UserSvc.SearchUsers(keyword, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* AddFriend
|
||||
* 功能:发送好友申请
|
||||
*/
|
||||
func (s *ContactService) AddFriend(fromUserID, toUserID, message string) error {
|
||||
// 检查是否已经是好友
|
||||
var existingContact model.UserContact
|
||||
result := s.DB.Where("user_id = ? AND contact_id = ?", fromUserID, toUserID).First(&existingContact)
|
||||
if result.Error == nil {
|
||||
return errors.New("已经是好友关系")
|
||||
}
|
||||
|
||||
// 检查是否已有待处理的申请
|
||||
var existingRequest model.FriendRequest
|
||||
result = s.DB.Where("from_user_id = ? AND to_user_id = ? AND status = ?",
|
||||
fromUserID, toUserID, "pending").First(&existingRequest)
|
||||
if result.Error == nil {
|
||||
return errors.New("已发送过好友申请")
|
||||
}
|
||||
|
||||
// 创建好友申请
|
||||
request := model.FriendRequest{
|
||||
FromUserID: fromUserID,
|
||||
ToUserID: toUserID,
|
||||
Message: message,
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
return s.DB.Create(&request).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetFriendRequests
|
||||
* 功能:获取好友申请列表
|
||||
*/
|
||||
func (s *ContactService) GetFriendRequests(userID string) ([]model.FriendRequest, error) {
|
||||
var requests []model.FriendRequest
|
||||
result := s.DB.Where("to_user_id = ? AND status = ?", userID, "pending").
|
||||
Order("created_at DESC").
|
||||
Find(&requests)
|
||||
return requests, result.Error
|
||||
}
|
||||
|
||||
/**
|
||||
* AcceptFriendRequest
|
||||
* 功能:接受好友申请
|
||||
*/
|
||||
func (s *ContactService) AcceptFriendRequest(requestID uint, userID string) error {
|
||||
// 查找申请
|
||||
var request model.FriendRequest
|
||||
if err := s.DB.First(&request, requestID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证是否为接收者
|
||||
if request.ToUserID != userID {
|
||||
return errors.New("无权操作此申请")
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.DB.Begin()
|
||||
|
||||
// 更新申请状态
|
||||
if err := tx.Model(&request).Update("status", "accepted").Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建双向好友关系
|
||||
contact1 := model.UserContact{
|
||||
UserID: request.FromUserID,
|
||||
ContactID: request.ToUserID,
|
||||
}
|
||||
contact2 := model.UserContact{
|
||||
UserID: request.ToUserID,
|
||||
ContactID: request.FromUserID,
|
||||
}
|
||||
|
||||
if err := tx.Create(&contact1).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&contact2).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
/**
|
||||
* RejectFriendRequest
|
||||
* 功能:拒绝好友申请
|
||||
*/
|
||||
func (s *ContactService) RejectFriendRequest(requestID uint, userID string) error {
|
||||
var request model.FriendRequest
|
||||
if err := s.DB.First(&request, requestID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.ToUserID != userID {
|
||||
return errors.New("无权操作此申请")
|
||||
}
|
||||
|
||||
return s.DB.Model(&request).Update("status", "rejected").Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetContacts
|
||||
* 功能:获取好友列表
|
||||
*/
|
||||
func (s *ContactService) GetContacts(userID string) ([]model.UserContact, error) {
|
||||
var contacts []model.UserContact
|
||||
result := s.DB.Where("user_id = ?", userID).
|
||||
Order("is_top DESC, last_chat_time DESC, created_at DESC").
|
||||
Find(&contacts)
|
||||
return contacts, result.Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetContactDetail
|
||||
* 功能:获取好友详情
|
||||
*/
|
||||
func (s *ContactService) GetContactDetail(userID, contactID string) (*model.UserContact, error) {
|
||||
var contact model.UserContact
|
||||
result := s.DB.Where("user_id = ? AND contact_id = ?", userID, contactID).First(&contact)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &contact, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateContact
|
||||
* 功能:更新好友信息(备注、分组等)
|
||||
*/
|
||||
func (s *ContactService) UpdateContact(userID, contactID string, updates map[string]interface{}) error {
|
||||
return s.DB.Model(&model.UserContact{}).
|
||||
Where("user_id = ? AND contact_id = ?", userID, contactID).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteContact
|
||||
* 功能:删除好友
|
||||
*/
|
||||
func (s *ContactService) DeleteContact(userID, contactID string) error {
|
||||
// 删除双向好友关系
|
||||
tx := s.DB.Begin()
|
||||
|
||||
if err := tx.Where("user_id = ? AND contact_id = ?", userID, contactID).Delete(&model.UserContact{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Where("user_id = ? AND contact_id = ?", contactID, userID).Delete(&model.UserContact{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetGroups
|
||||
* 功能:获取分组列表
|
||||
*/
|
||||
func (s *ContactService) GetGroups(userID string) ([]model.ContactGroup, error) {
|
||||
var groups []model.ContactGroup
|
||||
result := s.DB.Where("user_id = ?", userID).
|
||||
Order("sort_order ASC, created_at ASC").
|
||||
Find(&groups)
|
||||
return groups, result.Error
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateGroup
|
||||
* 功能:创建分组
|
||||
*/
|
||||
func (s *ContactService) CreateGroup(userID, groupName string) (*model.ContactGroup, error) {
|
||||
// 获取当前最大排序值
|
||||
var maxOrder int
|
||||
s.DB.Model(&model.ContactGroup{}).
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(MAX(sort_order), 0)").
|
||||
Scan(&maxOrder)
|
||||
|
||||
group := model.ContactGroup{
|
||||
UserID: userID,
|
||||
GroupName: groupName,
|
||||
SortOrder: maxOrder + 1,
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&group).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateGroup
|
||||
* 功能:更新分组
|
||||
*/
|
||||
func (s *ContactService) UpdateGroup(groupID uint, userID string, updates map[string]interface{}) error {
|
||||
return s.DB.Model(&model.ContactGroup{}).
|
||||
Where("id = ? AND user_id = ?", groupID, userID).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteGroup
|
||||
* 功能:删除分组
|
||||
*/
|
||||
func (s *ContactService) DeleteGroup(groupID uint, userID string) error {
|
||||
// 检查分组是否存在且属于该用户
|
||||
var group model.ContactGroup
|
||||
if err := s.DB.Where("id = ? AND user_id = ?", groupID, userID).First(&group).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将该分组下的联系人移到默认分组(group_id = 0)
|
||||
if err := s.DB.Model(&model.UserContact{}).
|
||||
Where("user_id = ? AND group_id = ?", userID, groupID).
|
||||
Update("group_id", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除分组
|
||||
return s.DB.Delete(&group).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetContactsWithUserInfo
|
||||
* 功能:获取好友列表(包含用户信息)
|
||||
*/
|
||||
func (s *ContactService) GetContactsWithUserInfo(userID string) ([]map[string]interface{}, error) {
|
||||
var contacts []model.UserContact
|
||||
if err := s.DB.Where("user_id = ?", userID).
|
||||
Order("is_top DESC, last_chat_time DESC").
|
||||
Find(&contacts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, contact := range contacts {
|
||||
// 获取联系人用户信息
|
||||
var user model.User
|
||||
if err := s.DB.Where("id = ?", contact.ContactID).First(&user).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
user.Password = ""
|
||||
|
||||
// 组合数据
|
||||
item := map[string]interface{}{
|
||||
"id": contact.ContactID,
|
||||
"name": user.Name,
|
||||
"avatar": user.Avatar,
|
||||
"desc": user.Desc,
|
||||
"remark_name": contact.RemarkName,
|
||||
"group_id": contact.GroupID,
|
||||
"is_top": contact.IsTop,
|
||||
"is_muted": contact.IsMuted,
|
||||
"last_chat_time": contact.LastChatTime,
|
||||
"last_message": contact.LastMessage,
|
||||
"unread_count": contact.UnreadCount,
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
56
internal/service/login_log_service.go
Normal file
56
internal/service/login_log_service.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:登录日志服务
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LoginLogService 登录日志服务
|
||||
type LoginLogService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// LoginLogSvc 全局单例
|
||||
var LoginLogSvc *LoginLogService
|
||||
|
||||
/**
|
||||
* InitLoginLogService
|
||||
* 功能:初始化登录日志服务
|
||||
*/
|
||||
func InitLoginLogService(db *gorm.DB) {
|
||||
LoginLogSvc = &LoginLogService{DB: db}
|
||||
}
|
||||
|
||||
/**
|
||||
* LogLogin
|
||||
* 功能:记录登录日志
|
||||
* @param account 登录账号
|
||||
* @param userID 用户ID(失败为"0")
|
||||
* @param ip IP地址
|
||||
* @param success 是否成功
|
||||
*/
|
||||
func (s *LoginLogService) LogLogin(account, userID, ip string, success bool) {
|
||||
// 获取IP归属地
|
||||
location := utils.GetIPLocation(ip)
|
||||
|
||||
// 创建登录日志
|
||||
log := model.LoginLog{
|
||||
Account: account,
|
||||
UserID: userID,
|
||||
Location: location,
|
||||
IP: ip,
|
||||
Success: success,
|
||||
LoginTime: time.Now(),
|
||||
}
|
||||
|
||||
// 异步写入数据库
|
||||
go s.DB.Create(&log)
|
||||
}
|
||||
|
||||
148
internal/service/room_service.go
Normal file
148
internal/service/room_service.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:房间管理服务
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RoomService 房间服务结构体
|
||||
type RoomService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// RoomSvc 全局单例
|
||||
var RoomSvc *RoomService
|
||||
|
||||
/**
|
||||
* InitRoomService
|
||||
* 功能:初始化房间服务
|
||||
*/
|
||||
func InitRoomService(db *gorm.DB) {
|
||||
RoomSvc = &RoomService{DB: db}
|
||||
}
|
||||
|
||||
/**
|
||||
* GenerateP2PRoomID
|
||||
* 功能:生成点对点聊天房间ID
|
||||
* 规则:将两个用户ID按字母序排序后,用下划线连接
|
||||
*/
|
||||
func GenerateP2PRoomID(userID1, userID2 string) string {
|
||||
if userID1 < userID2 {
|
||||
return fmt.Sprintf("%s_%s", userID1, userID2)
|
||||
}
|
||||
return fmt.Sprintf("%s_%s", userID2, userID1)
|
||||
}
|
||||
|
||||
/**
|
||||
* GenerateGroupRoomID
|
||||
* 功能:生成群聊房间ID
|
||||
* 规则:使用雪花ID生成全局唯一ID
|
||||
*/
|
||||
func GenerateGroupRoomID() string {
|
||||
id, err := utils.NextID()
|
||||
if err != nil {
|
||||
// 如果雪花ID生成失败,使用备用方案
|
||||
return fmt.Sprintf("group_%d", time.Now().UnixNano())
|
||||
}
|
||||
return fmt.Sprintf("group_%d", id)
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateRoom
|
||||
* 功能:创建房间
|
||||
*/
|
||||
func (s *RoomService) CreateRoom(roomType string, members []string, creatorID string) (*model.ChatRoom, error) {
|
||||
var roomID string
|
||||
if roomType == "p2p" {
|
||||
if len(members) != 2 {
|
||||
return nil, fmt.Errorf("点对点房间需要2个成员")
|
||||
}
|
||||
roomID = GenerateP2PRoomID(members[0], members[1])
|
||||
} else {
|
||||
roomID = GenerateGroupRoomID()
|
||||
}
|
||||
|
||||
// 检查房间是否已存在
|
||||
var existingRoom model.ChatRoom
|
||||
if err := s.DB.Where("room_id = ?", roomID).First(&existingRoom).Error; err == nil {
|
||||
return &existingRoom, nil
|
||||
}
|
||||
|
||||
// 序列化成员列表
|
||||
membersJSON, err := json.Marshal(members)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
room := model.ChatRoom{
|
||||
RoomID: roomID,
|
||||
RoomType: roomType,
|
||||
Members: string(membersJSON),
|
||||
CreatorID: creatorID,
|
||||
}
|
||||
|
||||
if roomType == "group" {
|
||||
room.RoomName = "群聊"
|
||||
}
|
||||
|
||||
if err := s.DB.Create(&room).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetRoom
|
||||
* 功能:获取房间信息
|
||||
*/
|
||||
func (s *RoomService) GetRoom(roomID string) (*model.ChatRoom, error) {
|
||||
var room model.ChatRoom
|
||||
result := s.DB.Where("room_id = ?", roomID).First(&room)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetOrCreateP2PRoom
|
||||
* 功能:获取或创建点对点房间
|
||||
*/
|
||||
func (s *RoomService) GetOrCreateP2PRoom(userID1, userID2 string) (*model.ChatRoom, error) {
|
||||
roomID := GenerateP2PRoomID(userID1, userID2)
|
||||
|
||||
// 尝试获取现有房间
|
||||
var room model.ChatRoom
|
||||
if err := s.DB.Where("room_id = ?", roomID).First(&room).Error; err == nil {
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
// 创建新房间
|
||||
members := []string{userID1, userID2}
|
||||
return s.CreateRoom("p2p", members, userID1)
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateRoomLastMessage
|
||||
* 功能:更新房间最后消息
|
||||
*/
|
||||
func (s *RoomService) UpdateRoomLastMessage(roomID, message string) error {
|
||||
now := time.Now()
|
||||
return s.DB.Model(&model.ChatRoom{}).
|
||||
Where("room_id = ?", roomID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_message": message,
|
||||
"last_message_time": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
165
internal/service/user_service.go
Normal file
165
internal/service/user_service.go
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* package service
|
||||
* 作用:用户管理服务
|
||||
*/
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserService 用户服务结构体
|
||||
type UserService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// UserSvc 全局单例
|
||||
var UserSvc *UserService
|
||||
|
||||
/**
|
||||
* InitUserService
|
||||
* 功能:初始化用户服务
|
||||
*/
|
||||
func InitUserService(db *gorm.DB) {
|
||||
UserSvc = &UserService{DB: db}
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserByID
|
||||
* 功能:根据ID获取用户
|
||||
*/
|
||||
func (s *UserService) GetUserByID(userID string) (*model.User, error) {
|
||||
var user model.User
|
||||
result := s.DB.Where("id = ?", userID).First(&user)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
user.Password = "" // 清除密码
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserByEmail
|
||||
* 功能:根据邮箱获取用户
|
||||
*/
|
||||
func (s *UserService) GetUserByEmail(email string) (*model.User, error) {
|
||||
var user model.User
|
||||
result := s.DB.Where("email = ?", email).First(&user)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
user.Password = "" // 清除密码
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserByPhone
|
||||
* 功能:根据手机号获取用户
|
||||
*/
|
||||
func (s *UserService) GetUserByPhone(phone string) (*model.User, error) {
|
||||
var user model.User
|
||||
result := s.DB.Where("phone = ?", phone).First(&user)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
user.Password = "" // 清除密码
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateUser
|
||||
* 功能:创建用户
|
||||
*/
|
||||
func (s *UserService) CreateUser(user *model.User) error {
|
||||
// 如果提供了密码,加密密码
|
||||
if user.Password != "" {
|
||||
hashedPassword, err := utils.HashPassword(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
}
|
||||
return s.DB.Create(user).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateUser
|
||||
* 功能:更新用户信息
|
||||
*/
|
||||
func (s *UserService) UpdateUser(userID string, updates map[string]interface{}) error {
|
||||
// 如果更新密码,需要加密
|
||||
if password, ok := updates["password"].(string); ok && password != "" {
|
||||
hashedPassword, err := utils.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates["password"] = hashedPassword
|
||||
}
|
||||
return s.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteUser
|
||||
* 功能:删除用户
|
||||
*/
|
||||
func (s *UserService) DeleteUser(userID string) error {
|
||||
return s.DB.Where("id = ?", userID).Delete(&model.User{}).Error
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserList
|
||||
* 功能:获取用户列表(分页)
|
||||
*/
|
||||
func (s *UserService) GetUserList(page, pageSize int) ([]model.User, int64, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
s.DB.Model(&model.User{}).Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
result := s.DB.Offset(offset).Limit(pageSize).Find(&users)
|
||||
if result.Error != nil {
|
||||
return nil, 0, result.Error
|
||||
}
|
||||
|
||||
// 清除所有用户的密码
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchUsers
|
||||
* 功能:搜索用户(按用户名、ID、邮箱、手机号)
|
||||
*/
|
||||
func (s *UserService) SearchUsers(keyword string, limit int) ([]model.User, error) {
|
||||
var users []model.User
|
||||
|
||||
query := s.DB.Where("name LIKE ? OR id LIKE ? OR email LIKE ? OR phone LIKE ?",
|
||||
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
|
||||
result := query.Find(&users)
|
||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
// 清除所有用户的密码
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
163
internal/utils/ip_location.go
Normal file
163
internal/utils/ip_location.go
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* package utils
|
||||
* 作用:IP归属地查询工具
|
||||
* 说明:使用第三方API查询IP归属地信息
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPLocationInfo IP归属地信息
|
||||
type IPLocationInfo struct {
|
||||
Country string `json:"country"` // 国家
|
||||
Region string `json:"region"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
ISP string `json:"isp"` // 运营商
|
||||
FullLocation string `json:"full_location"` // 完整归属地
|
||||
}
|
||||
|
||||
// 缓存结构(简单内存缓存)
|
||||
var (
|
||||
ipCache = make(map[string]*IPLocationInfo)
|
||||
cacheTimeout = 24 * time.Hour
|
||||
)
|
||||
|
||||
/**
|
||||
* GetIPLocation
|
||||
* 功能:获取IP归属地信息
|
||||
* @param ip IP地址
|
||||
* @returns 归属地信息字符串
|
||||
*/
|
||||
func GetIPLocation(ip string) string {
|
||||
// 排除本地IP
|
||||
if isLocalIP(ip) {
|
||||
return "本地"
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if info, ok := ipCache[ip]; ok {
|
||||
return info.FullLocation
|
||||
}
|
||||
|
||||
// 查询IP归属地
|
||||
info := queryIPLocation(ip)
|
||||
if info != nil {
|
||||
// 构建完整归属地字符串
|
||||
parts := []string{}
|
||||
if info.Country != "" {
|
||||
parts = append(parts, info.Country)
|
||||
}
|
||||
if info.Region != "" {
|
||||
parts = append(parts, info.Region)
|
||||
}
|
||||
if info.City != "" {
|
||||
parts = append(parts, info.City)
|
||||
}
|
||||
if info.ISP != "" {
|
||||
parts = append(parts, info.ISP)
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
info.FullLocation = strings.Join(parts, " ")
|
||||
} else {
|
||||
info.FullLocation = "未知"
|
||||
}
|
||||
|
||||
// 存入缓存
|
||||
ipCache[ip] = info
|
||||
return info.FullLocation
|
||||
}
|
||||
|
||||
return "未知"
|
||||
}
|
||||
|
||||
/**
|
||||
* isLocalIP
|
||||
* 功能:判断是否为本地IP
|
||||
*/
|
||||
func isLocalIP(ip string) bool {
|
||||
// 本地回环地址
|
||||
if ip == "127.0.0.1" || ip == "localhost" || ip == "::1" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 内网地址
|
||||
if strings.HasPrefix(ip, "192.168.") ||
|
||||
strings.HasPrefix(ip, "10.") ||
|
||||
strings.HasPrefix(ip, "172.16.") ||
|
||||
strings.HasPrefix(ip, "172.17.") ||
|
||||
strings.HasPrefix(ip, "172.18.") ||
|
||||
strings.HasPrefix(ip, "172.19.") ||
|
||||
strings.HasPrefix(ip, "172.20.") ||
|
||||
strings.HasPrefix(ip, "172.21.") ||
|
||||
strings.HasPrefix(ip, "172.22.") ||
|
||||
strings.HasPrefix(ip, "172.23.") ||
|
||||
strings.HasPrefix(ip, "172.24.") ||
|
||||
strings.HasPrefix(ip, "172.25.") ||
|
||||
strings.HasPrefix(ip, "172.26.") ||
|
||||
strings.HasPrefix(ip, "172.27.") ||
|
||||
strings.HasPrefix(ip, "172.28.") ||
|
||||
strings.HasPrefix(ip, "172.29.") ||
|
||||
strings.HasPrefix(ip, "172.30.") ||
|
||||
strings.HasPrefix(ip, "172.31.") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* queryIPLocation
|
||||
* 功能:查询IP归属地(使用ip-api.com免费API)
|
||||
*/
|
||||
func queryIPLocation(ip string) *IPLocationInfo {
|
||||
// 使用ip-api.com免费API(限制:每分钟45次请求)
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN&fields=status,message,country,regionName,city,isp", ip)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"regionName"`
|
||||
City string `json:"city"`
|
||||
ISP string `json:"isp"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Status != "success" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &IPLocationInfo{
|
||||
Country: result.Country,
|
||||
Region: result.Region,
|
||||
City: result.City,
|
||||
ISP: result.ISP,
|
||||
}
|
||||
}
|
||||
|
||||
166
internal/utils/jwt.go
Normal file
166
internal/utils/jwt.go
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* package utils
|
||||
*
|
||||
* JWT Token生成和验证工具包
|
||||
*
|
||||
* 功能概述:
|
||||
* 1. 生成JWT Token(包含用户ID和过期时间)
|
||||
* 2. 解析JWT Token(验证签名和过期时间)
|
||||
* 3. 验证Token有效性(提取用户ID)
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户登录后生成Token
|
||||
* - API请求时验证Token
|
||||
* - 从Token中提取用户信息
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// jwtSecret JWT签名密钥(从配置文件读取)
|
||||
var jwtSecret []byte
|
||||
|
||||
/**
|
||||
* init
|
||||
*
|
||||
* 功能:初始化JWT签名密钥
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 从配置文件读取JWT密钥
|
||||
* 2. 如果配置文件中没有,使用默认密钥(仅用于开发环境)
|
||||
* 3. 将密钥转换为字节数组存储
|
||||
*
|
||||
* 注意:生产环境必须使用配置文件中的强随机密钥
|
||||
*/
|
||||
func init() {
|
||||
secret := viper.GetString("jwt.secret")
|
||||
if secret == "" {
|
||||
secret = "xk-websocket-secret-key-2025" // 默认密钥,生产环境应使用配置
|
||||
}
|
||||
jwtSecret = []byte(secret)
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims
|
||||
*
|
||||
* JWT Token的载荷结构
|
||||
*
|
||||
* 字段说明:
|
||||
* - UserID: 用户ID(业务数据)
|
||||
* - RegisteredClaims: JWT标准声明(过期时间、签发时间等)
|
||||
*/
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"` // 用户ID
|
||||
jwt.RegisteredClaims // JWT标准声明
|
||||
}
|
||||
|
||||
/**
|
||||
* GenerateToken
|
||||
*
|
||||
* 功能:生成JWT Token
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 设置Token过期时间(默认7天)
|
||||
* 2. 创建Claims对象,包含用户ID和标准声明
|
||||
* 3. 使用HS256算法签名Token
|
||||
* 4. 返回Token字符串
|
||||
*
|
||||
* @param userID 用户ID
|
||||
* @returns token字符串和错误
|
||||
*/
|
||||
func GenerateToken(userID string) (string, error) {
|
||||
// 步骤1: 设置Token过期时间(7天后过期)
|
||||
expirationTime := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
// 步骤2: 创建Claims对象,包含用户ID和标准声明
|
||||
claims := &Claims{
|
||||
UserID: userID, // 业务数据:用户ID
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime), // 过期时间
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
|
||||
NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间(立即生效)
|
||||
},
|
||||
}
|
||||
|
||||
// 步骤3: 使用HS256算法创建Token并签名
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 步骤4: 返回Token字符串
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* ParseToken
|
||||
*
|
||||
* 功能:解析JWT Token
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 创建空的Claims对象
|
||||
* 2. 使用密钥解析Token并验证签名
|
||||
* 3. 检查Token是否有效(签名正确、未过期)
|
||||
* 4. 返回Claims对象
|
||||
*
|
||||
* @param tokenString token字符串
|
||||
* @returns Claims和错误
|
||||
*/
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
// 步骤1: 创建空的Claims对象
|
||||
claims := &Claims{}
|
||||
|
||||
// 步骤2: 解析Token并验证签名
|
||||
// 使用密钥验证Token的签名是否有效
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil // 返回签名密钥
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err // 解析失败(签名错误、格式错误等)
|
||||
}
|
||||
|
||||
// 步骤3: 检查Token是否有效(签名正确、未过期)
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// 步骤4: 返回解析后的Claims对象
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* ValidateToken
|
||||
*
|
||||
* 功能:验证Token有效性并提取用户ID
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 调用ParseToken解析Token
|
||||
* 2. 如果解析成功,从Claims中提取用户ID
|
||||
* 3. 返回用户ID
|
||||
*
|
||||
* 使用场景:
|
||||
* - 中间件中验证Token
|
||||
* - API处理器中获取当前用户ID
|
||||
*
|
||||
* @param tokenString token字符串
|
||||
* @returns 用户ID和错误
|
||||
*/
|
||||
func ValidateToken(tokenString string) (string, error) {
|
||||
// 步骤1: 解析Token
|
||||
claims, err := ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err // Token无效或已过期
|
||||
}
|
||||
|
||||
// 步骤2: 从Claims中提取用户ID
|
||||
return claims.UserID, nil
|
||||
}
|
||||
|
||||
82
internal/utils/password.go
Normal file
82
internal/utils/password.go
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* package utils
|
||||
*
|
||||
* 密码加密和验证工具包
|
||||
*
|
||||
* 功能概述:
|
||||
* 1. 使用bcrypt算法加密密码(单向哈希,不可逆)
|
||||
* 2. 验证明文密码与加密密码是否匹配
|
||||
*
|
||||
* 安全特性:
|
||||
* - 使用bcrypt算法,自动加盐
|
||||
* - 计算成本可调,防止暴力破解
|
||||
* - 相同密码每次加密结果不同(因为盐值随机)
|
||||
*/
|
||||
package utils
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
/**
|
||||
* HashPassword
|
||||
*
|
||||
* 功能:使用bcrypt算法加密密码
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 将明文密码转换为字节数组
|
||||
* 2. 使用bcrypt算法生成哈希值(自动加盐)
|
||||
* 3. 将哈希值转换为字符串返回
|
||||
*
|
||||
* 特点:
|
||||
* - 每次加密结果不同(因为盐值随机)
|
||||
* - 使用默认计算成本(10轮)
|
||||
* - 单向加密,不可逆
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户注册时加密密码
|
||||
* - 用户修改密码时加密新密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @returns 加密后的密码哈希字符串和错误
|
||||
*/
|
||||
func HashPassword(password string) (string, error) {
|
||||
// 步骤1-2: 使用bcrypt算法生成密码哈希
|
||||
// bcrypt.DefaultCost = 10,表示进行2^10=1024轮哈希计算
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 步骤3: 将哈希值转换为字符串返回
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
/**
|
||||
* CheckPassword
|
||||
*
|
||||
* 功能:验证明文密码与加密密码是否匹配
|
||||
*
|
||||
* 步骤:
|
||||
* 1. 从加密密码中提取盐值
|
||||
* 2. 使用相同的盐值对明文密码进行哈希
|
||||
* 3. 比较两个哈希值是否相同
|
||||
*
|
||||
* 特点:
|
||||
* - 即使密码相同,每次加密的哈希值也不同
|
||||
* - 但可以通过CompareHashAndPassword正确验证
|
||||
* - 验证过程是安全的,不会泄露密码信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户登录时验证密码
|
||||
* - 修改密码时验证旧密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @param hash 加密后的密码哈希
|
||||
* @returns 是否匹配(true=匹配,false=不匹配)
|
||||
*/
|
||||
func CheckPassword(password, hash string) bool {
|
||||
// 步骤1-3: 比较明文密码的哈希值与存储的哈希值
|
||||
// 如果匹配,err为nil,返回true;否则返回false
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
172
internal/utils/response.go
Normal file
172
internal/utils/response.go
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* package utils
|
||||
* 作用:提供统一的API响应工具函数
|
||||
* 说明:所有响应统一返回HTTP 200状态码,错误通过code字段标识
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"xk-websocket-v2/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 业务状态码常量
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeBadRequest = 400 // 参数错误
|
||||
CodeUnauthorized = 401 // 未认证
|
||||
CodeForbidden = 403 // 无权限
|
||||
CodeNotFound = 404 // 资源不存在
|
||||
CodeInternalError = 500 // 服务器错误
|
||||
)
|
||||
|
||||
// 响应类型常量
|
||||
const (
|
||||
TypeSuccess = "success"
|
||||
TypeError = "error"
|
||||
)
|
||||
|
||||
/**
|
||||
* getECS
|
||||
* 作用:获取服务器标识
|
||||
*/
|
||||
func getECS() string {
|
||||
ecs := viper.GetString("server.ecs")
|
||||
if ecs == "" {
|
||||
return "localhost"
|
||||
}
|
||||
return ecs
|
||||
}
|
||||
|
||||
/**
|
||||
* formatDuration
|
||||
* 作用:格式化响应时间为 "XX ms" 格式
|
||||
*/
|
||||
func formatDuration(d time.Duration) string {
|
||||
ms := d.Milliseconds()
|
||||
return strconv.FormatInt(ms, 10) + " ms"
|
||||
}
|
||||
|
||||
/**
|
||||
* Response
|
||||
* 作用:统一的响应函数,所有响应都通过此函数返回
|
||||
* 说明:统一返回HTTP 200状态码
|
||||
*/
|
||||
func Response(c *gin.Context, code int, message string, result interface{}, responseType string) {
|
||||
// 从Context获取请求开始时间
|
||||
startTime, exists := c.Get("request_start_time")
|
||||
var duration time.Duration
|
||||
if exists {
|
||||
duration = time.Since(startTime.(time.Time))
|
||||
} else {
|
||||
duration = 0
|
||||
}
|
||||
|
||||
response := model.ApiResponse{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Result: result,
|
||||
Type: responseType,
|
||||
InterfaceInfo: model.InterfaceInfo{
|
||||
ResultTime: formatDuration(duration),
|
||||
Ecs: getECS(),
|
||||
},
|
||||
}
|
||||
|
||||
// 统一返回HTTP 200状态码
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Success
|
||||
* 作用:成功响应(无数据)
|
||||
*/
|
||||
func Success(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "操作成功"
|
||||
}
|
||||
Response(c, CodeSuccess, message, nil, TypeSuccess)
|
||||
}
|
||||
|
||||
/**
|
||||
* SuccessWithData
|
||||
* 作用:成功响应(带数据)
|
||||
*/
|
||||
func SuccessWithData(c *gin.Context, data interface{}, message string) {
|
||||
if message == "" {
|
||||
message = "获取成功"
|
||||
}
|
||||
Response(c, CodeSuccess, message, data, TypeSuccess)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error
|
||||
* 作用:错误响应(自定义状态码和消息)
|
||||
*/
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
if message == "" {
|
||||
message = "操作失败"
|
||||
}
|
||||
Response(c, code, message, nil, TypeError)
|
||||
}
|
||||
|
||||
/**
|
||||
* BadRequest
|
||||
* 作用:参数错误响应(code=400)
|
||||
*/
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "参数错误"
|
||||
}
|
||||
Response(c, CodeBadRequest, message, nil, TypeError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized
|
||||
* 作用:未认证响应(code=401)
|
||||
*/
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "未认证"
|
||||
}
|
||||
Response(c, CodeUnauthorized, message, nil, TypeError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbidden
|
||||
* 作用:无权限响应(code=403)
|
||||
*/
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "无权限"
|
||||
}
|
||||
Response(c, CodeForbidden, message, nil, TypeError)
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFound
|
||||
* 作用:资源不存在响应(code=404)
|
||||
*/
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "资源不存在"
|
||||
}
|
||||
Response(c, CodeNotFound, message, nil, TypeError)
|
||||
}
|
||||
|
||||
/**
|
||||
* InternalError
|
||||
* 作用:服务器错误响应(code=500)
|
||||
*/
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = "服务器错误"
|
||||
}
|
||||
Response(c, CodeInternalError, message, nil, TypeError)
|
||||
}
|
||||
|
||||
182
internal/utils/snowflake.go
Normal file
182
internal/utils/snowflake.go
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* package utils
|
||||
* 作用:雪花ID生成器,生成全局唯一的ID
|
||||
* 说明:使用Twitter的雪花算法,生成64位整数ID
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// 时间戳占用位数(41位,可以使用69年)
|
||||
timestampBits = 41
|
||||
// 数据中心ID占用位数(5位,最多32个数据中心)
|
||||
datacenterIDBits = 5
|
||||
// 机器ID占用位数(5位,每个数据中心最多32台机器)
|
||||
machineIDBits = 5
|
||||
// 序列号占用位数(12位,每毫秒最多4096个ID)
|
||||
sequenceBits = 12
|
||||
|
||||
// 最大值
|
||||
maxDatacenterID = -1 ^ (-1 << datacenterIDBits)
|
||||
maxMachineID = -1 ^ (-1 << machineIDBits)
|
||||
maxSequence = -1 ^ (-1 << sequenceBits)
|
||||
|
||||
// 位移
|
||||
machineIDShift = sequenceBits
|
||||
datacenterIDShift = sequenceBits + machineIDBits
|
||||
timestampShift = sequenceBits + machineIDBits + datacenterIDBits
|
||||
|
||||
// 起始时间戳(2024-01-01 00:00:00)
|
||||
epoch int64 = 1704067200000
|
||||
)
|
||||
|
||||
// Snowflake 雪花ID生成器
|
||||
type Snowflake struct {
|
||||
mutex sync.Mutex
|
||||
datacenterID int64
|
||||
machineID int64
|
||||
sequence int64
|
||||
lastStamp int64
|
||||
}
|
||||
|
||||
var (
|
||||
// 全局雪花ID生成器实例
|
||||
globalSnowflake *Snowflake
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
/**
|
||||
* InitSnowflake
|
||||
* 功能:初始化全局雪花ID生成器
|
||||
* @param datacenterID 数据中心ID(0-31)
|
||||
* @param machineID 机器ID(0-31)
|
||||
*/
|
||||
func InitSnowflake(datacenterID, machineID int64) error {
|
||||
if datacenterID < 0 || datacenterID > maxDatacenterID {
|
||||
return errors.New("datacenter ID must be between 0 and 31")
|
||||
}
|
||||
if machineID < 0 || machineID > maxMachineID {
|
||||
return errors.New("machine ID must be between 0 and 31")
|
||||
}
|
||||
|
||||
once.Do(func() {
|
||||
globalSnowflake = &Snowflake{
|
||||
datacenterID: datacenterID,
|
||||
machineID: machineID,
|
||||
sequence: 0,
|
||||
lastStamp: -1,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* NextID
|
||||
* 功能:生成下一个ID
|
||||
* @returns 64位整数ID
|
||||
*/
|
||||
func NextID() (int64, error) {
|
||||
if globalSnowflake == nil {
|
||||
// 默认使用datacenterID=1, machineID=1
|
||||
if err := InitSnowflake(1, 1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return globalSnowflake.nextID()
|
||||
}
|
||||
|
||||
/**
|
||||
* nextID
|
||||
* 功能:生成下一个ID(内部方法)
|
||||
*/
|
||||
func (s *Snowflake) nextID() (int64, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
// 如果当前时间小于上次时间,说明时钟回拨
|
||||
if now < s.lastStamp {
|
||||
return 0, errors.New("clock moved backwards")
|
||||
}
|
||||
|
||||
// 如果是同一毫秒内
|
||||
if now == s.lastStamp {
|
||||
s.sequence = (s.sequence + 1) & maxSequence
|
||||
// 序列号溢出,等待下一毫秒
|
||||
if s.sequence == 0 {
|
||||
now = s.waitNextMillis(s.lastStamp)
|
||||
}
|
||||
} else {
|
||||
// 新的毫秒,序列号重置
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.lastStamp = now
|
||||
|
||||
// 生成ID
|
||||
id := ((now - epoch) << timestampShift) |
|
||||
(s.datacenterID << datacenterIDShift) |
|
||||
(s.machineID << machineIDShift) |
|
||||
s.sequence
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* waitNextMillis
|
||||
* 功能:等待下一毫秒
|
||||
*/
|
||||
func (s *Snowflake) waitNextMillis(lastStamp int64) int64 {
|
||||
now := time.Now().UnixMilli()
|
||||
for now <= lastStamp {
|
||||
now = time.Now().UnixMilli()
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
/**
|
||||
* NextIDString
|
||||
* 功能:生成下一个ID(字符串格式)
|
||||
*/
|
||||
func NextIDString() (string, error) {
|
||||
id, err := NextID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return int64ToString(id), nil
|
||||
}
|
||||
|
||||
/**
|
||||
* int64ToString
|
||||
* 功能:将int64转换为字符串
|
||||
*/
|
||||
func int64ToString(id int64) string {
|
||||
if id == 0 {
|
||||
return "0"
|
||||
}
|
||||
negative := id < 0
|
||||
if negative {
|
||||
id = -id
|
||||
}
|
||||
|
||||
var result []byte
|
||||
for id > 0 {
|
||||
result = append([]byte{byte('0' + id%10)}, result...)
|
||||
id /= 10
|
||||
}
|
||||
|
||||
if negative {
|
||||
result = append([]byte{'-'}, result...)
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
BIN
server.exe
Normal file
BIN
server.exe
Normal file
Binary file not shown.
38
添加默认用户信息.sql
Normal file
38
添加默认用户信息.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- ==========================================
|
||||
-- IM系统 - 添加默认用户信息
|
||||
-- 说明:为用户表添加20个测试用户,ID从10001开始
|
||||
-- 所有用户默认密码:12345678
|
||||
-- ==========================================
|
||||
|
||||
-- 插入20个测试用户
|
||||
INSERT INTO `users` (`id`, `email`, `phone`, `password`, `name`, `avatar`, `desc`, `region`, `created_at`, `updated_at`) VALUES
|
||||
('10001', 'user001@example.com', '13800138001', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '张三', '张', 'Golang 开发工程师', '北京', NOW(), NOW()),
|
||||
('10002', 'user002@example.com', '13800138002', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '李四', '李', '前端开发工程师', '上海', NOW(), NOW()),
|
||||
('10003', 'user003@example.com', '13800138003', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王五', '王', '产品经理', '深圳', NOW(), NOW()),
|
||||
('10004', 'user004@example.com', '13800138004', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '赵六', '赵', 'UI设计师', '广州', NOW(), NOW()),
|
||||
('10005', 'user005@example.com', '13800138005', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '钱七', '钱', '测试工程师', '杭州', NOW(), NOW()),
|
||||
('10006', 'user006@example.com', '13800138006', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '孙八', '孙', '运维工程师', '成都', NOW(), NOW()),
|
||||
('10007', 'user007@example.com', '13800138007', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '周九', '周', '数据分析师', '武汉', NOW(), NOW()),
|
||||
('10008', 'user008@example.com', '13800138008', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '吴十', '吴', '项目经理', '西安', NOW(), NOW()),
|
||||
('10009', 'user009@example.com', '13800138009', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '郑十一', '郑', '架构师', '南京', NOW(), NOW()),
|
||||
('10010', 'user010@example.com', '13800138010', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王十二', '王', '技术总监', '苏州', NOW(), NOW()),
|
||||
('10011', 'user011@example.com', '13800138011', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '冯十三', '冯', 'HR经理', '天津', NOW(), NOW()),
|
||||
('10012', 'user012@example.com', '13800138012', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '陈十四', '陈', '财务经理', '重庆', NOW(), NOW()),
|
||||
('10013', 'user013@example.com', '13800138013', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '褚十五', '褚', '市场经理', '长沙', NOW(), NOW()),
|
||||
('10014', 'user014@example.com', '13800138014', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '卫十六', '卫', '销售经理', '郑州', NOW(), NOW()),
|
||||
('10015', 'user015@example.com', '13800138015', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '蒋十七', '蒋', '客服主管', '济南', NOW(), NOW()),
|
||||
('10016', 'user016@example.com', '13800138016', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '沈十八', '沈', '运营专员', '青岛', NOW(), NOW()),
|
||||
('10017', 'user017@example.com', '13800138017', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '韩十九', '韩', '内容编辑', '大连', NOW(), NOW()),
|
||||
('10018', 'user018@example.com', '13800138018', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '杨二十', '杨', '商务拓展', '厦门', NOW(), NOW()),
|
||||
('10019', 'user019@example.com', '13800138019', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '朱二一', '朱', '法务专员', '福州', NOW(), NOW()),
|
||||
('10020', 'user020@example.com', '13800138020', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '秦二二', '秦', '行政助理', '合肥', NOW(), NOW());
|
||||
|
||||
-- 说明:
|
||||
-- 1. 所有用户默认密码为:12345678
|
||||
-- 2. 密码已使用bcrypt加密存储
|
||||
-- 3. 用户ID从10001开始,连续到10020
|
||||
-- 4. 邮箱格式:user001@example.com 到 user020@example.com
|
||||
-- 5. 手机号格式:13800138001 到 13800138020
|
||||
-- 6. 头像使用用户姓氏的首字符
|
||||
-- 7. 每个用户都有不同的职位和地区信息
|
||||
|
||||
Reference in New Issue
Block a user