add(接口):添加授权、用户、好友关系服务接口

This commit is contained in:
西街长安 2025-10-19 14:56:05 +08:00
parent d85e906b39
commit c67c666135
20 changed files with 967 additions and 8 deletions

View File

@ -0,0 +1,57 @@
namespace IM_API.Dtos
{
public class BaseResponse<T>
{
//响应状态码
public int Code { get; set; }
//响应消息
public string Message { get; set; }
//响应数据
public T? Data { get; set; }
/// <summary>
/// 默认成功响应返回
/// </summary>
/// <param name="msg"></param>
/// <param name="data"></param>
public BaseResponse(string msg,T data)
{
this.Code = 0;
this.Message = msg;
this.Data = data;
}
/// <summary>
/// 默认成功响应返回,不带数据
/// </summary>
/// <param name="msg"></param>
/// <param name="data"></param>
public BaseResponse(string msg)
{
this.Code = 0;
this.Message = msg;
}
/// <summary>
/// 非成功响应且带数据
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public BaseResponse(int code, string message, T? data)
{
Code = code;
Message = message;
Data = data;
}
/// <summary>
/// 非成功响应且不带数据
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
public BaseResponse(int code, string message)
{
Code = code;
Message = message;
}
public BaseResponse() { }
}
}

View File

@ -0,0 +1,9 @@
namespace IM_API.Dtos
{
public class FriendRequestDto
{
public int FromUserId { get; set; }
public int ToUserId { get; set; }
public string? Description { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace IM_API.Dtos
{
public class LoginDto
{
public int Id { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace IM_API.Dtos
{
public class LoginRequestDto
{
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace IM_API.Dtos
{
public class RegisterRequestDto
{
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace IM_API.Dtos
{
public class UpdateUserDto
{
public string? NickName { get; set; }
}
}

View File

@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Dtos
{
public class UserInfoDto
{
public int Id { get; set; }
/// <summary>
/// 唯一用户名
/// </summary>
public string Username { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
public string NickName { get; set; }
/// <summary>
/// 用户在线状态
/// 0默认不在线
/// 1在线
/// </summary>
public sbyte OlineStatus { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// 账户状态
/// (0未激活,1正常,2封禁)
/// </summary>
public sbyte Status { get; set; }
}
}

View File

@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,39 @@
using IM_API.Dtos;
using IM_API.Models;
namespace IM_API.Interface.Services
{
public interface IAuthService
{
/// <summary>
/// 登录
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<LoginDto> LoginAsync(LoginRequestDto dto);
/// <summary>
/// 注册
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<UserInfoDto> RegisterAsync(RegisterRequestDto dto);
/// <summary>
/// 生成登录凭证
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
string GenerateToken(User user);
/// <summary>
/// 验证登录凭证
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
int? ValidateToken(string token);
/// <summary>
/// 刷新令牌
/// </summary>
/// <param name="refreshToken"></param>
/// <returns></returns>
LoginDto RefreshToken(string refreshToken);
}
}

View File

@ -0,0 +1,32 @@
using IM_API.Dtos;
using IM_API.Models;
namespace IM_API.Interface.Services
{
public interface IFriendSerivce
{
/// <summary>
/// 获取好友列表
/// </summary>
/// <param name="userId">指定用户</param>
/// <param name="page">当前页</param>
/// <param name="limit">分页大小</param>
/// <returns></returns>
Task<List<UserInfoDto>> GetFriendListAsync(int userId,int page,int limit);
/// <summary>
/// 新增好友请求
/// </summary>
/// <param name="friendRequest"></param>
/// <returns></returns>
Task<bool> SendFriendRequestAsync(FriendRequestDto friendRequest);
/// <summary>
/// 获取好友请求
/// </summary>
/// <param name="userId"></param>
/// <param name="isReceived">是否为接受请求方</param>
/// <param name="page"></param>
/// <param name="limit"></param>
/// <returns></returns>
Task<Friendrequest> GetFriendRequestListAsync(int userId,bool isReceived,int page,int limit);
}
}

View File

@ -0,0 +1,39 @@
using IM_API.Dtos;
using IM_API.Models;
namespace IM_API.Interface.Services
{
public interface IUserService
{
/// <summary>
/// 获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<UserInfoDto> GetUserInfoAsync(int userId);
/// <summary>
/// 用户名查找用户
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
Task<UserInfoDto> GetUserInfoByUsernameAsync(string username);
/// <summary>
/// 更新用户信息
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<UserInfoDto> UpdateUserAsync(UpdateUserDto dto);
/// <summary>
/// 重置用户密码
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> ResetPasswordAsync(string password);
/// <summary>
/// 更新用户在线状态
/// </summary>
/// <param name="onlineStatus"></param>
/// <returns></returns>
Task<bool> UpdateOlineStatusAsync(UserOnlineStatus onlineStatus);
}
}

View File

@ -0,0 +1,7 @@
namespace IM_API.Models
{
public enum FriendStatus:SByte
{
}
}

View File

@ -41,7 +41,7 @@ public partial class User
/// 1在线
/// </summary>
[Column(TypeName = "tinyint(4)")]
public sbyte OlineStatus { get; set; }
public UserOnlineStatus OlineStatus { get; set; }
/// <summary>
/// 创建时间
@ -60,7 +60,7 @@ public partial class User
/// (0未激活,1正常,2封禁)
/// </summary>
[Column(TypeName = "tinyint(4)")]
public sbyte Status { get; set; }
public UserStatus Status { get; set; }
/// <summary>
/// 软删除标识

View File

@ -0,0 +1,18 @@
namespace IM_API.Models
{
/// <summary>
/// 用户在线状态
/// </summary>
public enum UserOnlineStatus : sbyte
{
/// <summary>
/// 不在线 (0)
/// </summary>
Offline = 0,
/// <summary>
/// 在线 (1)
/// </summary>
Online = 1
}
}

View File

@ -0,0 +1,9 @@
namespace IM_API.Models
{
public enum UserStatus:SByte
{
Inactive = 0,
Normal = 1,
Banned = 2
}
}

View File

@ -0,0 +1,22 @@
using System.Security.Cryptography;
using System.Text;
namespace IM_API.Tools
{
public static class PasswordHasher
{
public static string HashPassword(string password)
{
using var sha256 = SHA256.Create();
var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
return Convert.ToBase64String(hashedBytes);
}
public static bool VerifyPassword(string password, string hashedPassword)
{
var hashedInput = HashPassword(password);
return hashedInput == hashedPassword;
}
}
}
}

View File

@ -1,11 +1,17 @@
<script setup></script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
<div id="app">
<WindowLayout>
<RouterView></RouterView>
</WindowLayout>
</div>
</template>
<script setup>
import WindowLayout from './components/Layout.vue'
</script>
<style scoped></style>

View File

@ -0,0 +1,262 @@
<template>
<div class="window-container">
<div class="window">
<!-- Windows 风格标题栏 -->
<div class="window-header">
<div class="window-title-area">
<div class="window-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 1.5H1.5V6.5H6.5V1.5Z" fill="currentColor"/>
<path d="M14.5 1.5H9.5V6.5H14.5V1.5Z" fill="currentColor"/>
<path d="M6.5 9.5H1.5V14.5H6.5V9.5Z" fill="currentColor"/>
<path d="M14.5 9.5H9.5V14.5H14.5V9.5Z" fill="currentColor"/>
</svg>
</div>
<div class="window-title">我的窗口</div>
</div>
<div class="window-controls">
<button class="control-btn minimize" @click="minimize" title="最小化">
<span class="control-icon"></span>
</button>
<button class="control-btn maximize" @click="maximize" title="最大化">
<span class="control-icon"></span>
</button>
<button class="control-btn close" @click="close" title="关闭">
<span class="control-icon"></span>
</button>
</div>
</div>
<!-- 窗口内容 -->
<div class="window-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
function minimize() {
console.log('最小化')
}
function maximize() {
console.log('最大化/还原')
}
function close() {
console.log('关闭窗口')
}
</script>
<style scoped>
/* 外层容器,居中 - 保持不变 */
.window-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f0f0;
padding: 20px;
box-sizing: border-box;
}
/* 窗口主体 - 保持不变 */
.window {
width: 800px;
height: 550px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 窗口顶部栏 - 优化按钮样式 */
.window-header {
height: 42px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
background: linear-gradient(135deg, #1a73e8 0%, #4285f4 100%);
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: 600;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
user-select: none;
position: relative;
overflow: hidden;
}
/* 添加顶栏的微光效果 */
.window-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 20%,
rgba(255, 255, 255, 0.7) 50%,
rgba(255, 255, 255, 0.4) 80%,
transparent 100%);
}
/* 标题区域 */
.window-title-area {
display: flex;
align-items: center;
gap: 8px;
}
/* 窗口图标 */
.window-icon {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.9);
}
/* 窗口标题 */
.window-title {
font-size: 14px;
letter-spacing: 0.3px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}
/* 标题栏按钮容器 - 增加间距 */
.window-controls {
display: flex;
gap: 8px;
}
/* 控制按钮通用样式 - 优化尺寸和交互 */
.control-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.85);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.control-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0);
border-radius: 6px;
transition: background 0.2s ease;
}
.control-btn:hover::before {
background: rgba(255, 255, 255, 0.15);
}
.control-btn:active {
transform: scale(0.95);
}
/* 控制按钮图标 - 使用CSS绘制精致图标 */
.control-icon {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 最小化按钮图标 */
.minimize .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.minimize .control-icon::before {
content: '';
position: absolute;
width: 10px;
height: 2px;
background: currentColor;
border-radius: 1px;
bottom: 2px;
}
/* 最大化按钮图标 */
.maximize .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.maximize .control-icon::before {
content: '';
position: absolute;
width: 8px;
height: 8px;
border: 1.5px solid currentColor;
border-radius: 1px;
}
/* 关闭按钮图标 */
.close .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.close .control-icon::before,
.close .control-icon::after {
content: '';
position: absolute;
width: 12px;
height: 1.5px;
background: currentColor;
border-radius: 1px;
top: 4px;
left: -1px;
}
.close .control-icon::before {
transform: rotate(45deg);
}
.close .control-icon::after {
transform: rotate(-45deg);
}
/* 关闭按钮特殊样式 */
.close:hover {
color: #fff;
}
.close:hover::before {
background: rgba(232, 17, 35, 0.9);
}
/* 窗口内容 - 保持不变 */
.window-content {
flex: 1;
overflow-y: hidden;
background-color: #fdfdfd;
}
</style>

View File

@ -1,8 +1,10 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') }]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
routes,
})
export default router

View File

@ -0,0 +1,386 @@
<template>
<div class="login-content">
<div class="login-header">
<h2 class="login-title">欢迎登录</h2>
<p class="login-subtitle">请输入您的账号信息</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="input-group">
<div class="input-container">
<input id="username" type="text" v-model="username" placeholder=" " required>
<label for="username" class="floating-label">用户名</label>
<div class="input-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21V19C20 16.7909 18.2091 15 16 15H8C5.79086 15 4 16.7909 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
</div>
</div>
<div class="input-group">
<div class="input-container">
<input id="password" type="password" v-model="password" placeholder=" " required>
<label for="password" class="floating-label">密码</label>
<div class="input-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M7 11V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V11" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
</div>
</div>
<div class="options-row">
<label class="remember-me">
<input type="checkbox">
<span class="checkmark"></span>
记住我
</label>
<a href="#" class="forgot-link">忘记密码?</a>
</div>
<button type="submit" class="login-btn">
<span>登录</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
</form>
<div class="login-footer">
<p>还没有账号? <a href="#" class="register-link">立即注册</a></p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const password = ref('')
const errorMsg = ref('')
const handleLogin = () => {
if (!username.value || !password.value) {
errorMsg.value = '用户名或密码不能为空'
return
}
if (username.value === 'admin' && password.value === '123456') {
alert('登录成功!')
errorMsg.value = ''
// TODO: IM
} else {
errorMsg.value = '用户名或密码错误'
}
}
</script>
<style scoped>
.login-content {
padding: 40px 30px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 0 0 8px 8px;
position: relative;
overflow: hidden;
}
/* 添加背景装饰元素 */
.login-content::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 300px;
height: 300px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.1) 100%);
z-index: 0;
}
.login-content::after {
content: '';
position: absolute;
bottom: -30%;
left: -10%;
width: 200px;
height: 200px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.08) 100%);
z-index: 0;
}
.login-header {
text-align: center;
margin-bottom: 32px;
position: relative;
z-index: 1;
}
.login-title {
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.login-subtitle {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 320px;
margin: 0 auto;
width: 100%;
position: relative;
z-index: 1;
}
/* 输入组 */
.input-group {
margin-bottom: 8px;
}
.input-container {
position: relative;
margin-top: 8px;
}
.input-container input {
width: 100%;
padding: 16px 16px 16px 48px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
font-size: 15px;
outline: none;
transition: all 0.3s ease;
background-color: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 1;
}
.input-container input:focus {
border-color: #3b82f6;
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 2px 6px rgba(59, 130, 246, 0.1);
transform: translateY(-1px);
}
.input-container input:focus + .floating-label,
.input-container input:not(:placeholder-shown) + .floating-label {
top: -8px;
left: 48px;
font-size: 12px;
color: #3b82f6;
background: linear-gradient(180deg, #f8fafc 50%, #ffffff 50%);
padding: 0 6px;
font-weight: 600;
}
.floating-label {
position: absolute;
top: 16px;
left: 48px;
font-size: 15px;
color: #64748b;
pointer-events: none;
transition: all 0.3s ease;
z-index: 2;
}
.input-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
transition: color 0.3s ease;
z-index: 2;
}
.input-container input:focus ~ .input-icon {
color: #3b82f6;
}
/* 选项行 */
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0 5px;
}
.remember-me {
display: flex;
align-items: center;
font-size: 14px;
color: #64748b;
cursor: pointer;
font-weight: 500;
}
.remember-me input {
display: none;
}
.checkmark {
width: 18px;
height: 18px;
border: 2px solid #cbd5e1;
border-radius: 4px;
margin-right: 8px;
position: relative;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.remember-me:hover .checkmark {
border-color: #94a3b8;
}
.remember-me input:checked + .checkmark {
background-color: #3b82f6;
border-color: #3b82f6;
}
.remember-me input:checked + .checkmark::after {
content: '';
position: absolute;
left: 5px;
top: 2px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.forgot-link {
font-size: 14px;
color: #3b82f6;
text-decoration: none;
transition: color 0.3s ease;
font-weight: 500;
}
.forgot-link:hover {
color: #2563eb;
text-decoration: underline;
}
/* 按钮 */
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.25);
position: relative;
overflow: hidden;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-btn:hover {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
box-shadow: 0 6px 12px rgba(59, 130, 246, 0.35);
transform: translateY(-2px);
}
.login-btn:hover::before {
left: 100%;
}
.login-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.4);
}
/* 错误提示 */
.error {
color: #ef4444;
margin-top: 16px;
font-size: 14px;
text-align: center;
padding: 12px;
background-color: #fef2f2;
border-radius: 8px;
border-left: 4px solid #ef4444;
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1);
font-weight: 500;
}
/* 登录页脚 */
.login-footer {
margin-top: 24px;
text-align: center;
font-size: 14px;
color: #64748b;
position: relative;
z-index: 1;
}
.register-link {
color: #3b82f6;
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
position: relative;
}
.register-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: #3b82f6;
transition: width 0.3s ease;
}
.register-link:hover {
color: #2563eb;
}
.register-link:hover::after {
width: 100%;
}
</style>