创建项目
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application.Service.Pub\Application.Service.Pub.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
Service/Application.Domain.Entity/Class1.cs
Normal file
7
Service/Application.Domain.Entity/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Domain.Entity
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
13
Service/Application.Domain/Application.Domain.csproj
Normal file
13
Service/Application.Domain/Application.Domain.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application.Domain.Entity\Application.Domain.Entity.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
Service/Application.Domain/Class1.cs
Normal file
7
Service/Application.Domain/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Domain
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
12
Service/Application.Domain/RequestParms/Login/LoginParms.cs
Normal file
12
Service/Application.Domain/RequestParms/Login/LoginParms.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Application.Domain
|
||||
{
|
||||
public class LoginParms
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string pwd { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Photon.Core" Version="1.0.0.1" />
|
||||
<PackageReference Include="Photon.Core.Assist" Version="1.0.0.1" />
|
||||
<PackageReference Include="Photon.Core.Jwt" Version="1.0.0.2" />
|
||||
<PackageReference Include="Photon.Core.Queue" Version="1.0.0.1" />
|
||||
<PackageReference Include="Photon.Core.Redis" Version="1.0.0.1" />
|
||||
<PackageReference Include="Photon.Core.SqlSugar" Version="1.0.0.1" />
|
||||
<PackageReference Include="Photon.Core.Timer" Version="1.0.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
7
Service/Application.Service.Pub/Class1.cs
Normal file
7
Service/Application.Service.Pub/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Service.Pub
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
7
Service/Application.Service.Tool/Class1.cs
Normal file
7
Service/Application.Service.Tool/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Service.Tool
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
18
Service/Application.Web/Application.Web.csproj
Normal file
18
Service/Application.Web/Application.Web.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application.Domain\Application.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
Service/Application.Web/Application.Web.csproj.user
Normal file
8
Service/Application.Web/Application.Web.csproj.user
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ActiveDebugProfile>https</ActiveDebugProfile>
|
||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
40
Service/Application.Web/Common/JwtHandle.cs
Normal file
40
Service/Application.Web/Common/JwtHandle.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
|
||||
namespace Application.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// JWT验证
|
||||
/// </summary>
|
||||
public class JwtHandle : IJwtValidator
|
||||
{
|
||||
public int Priority => 1;
|
||||
public void ValidateAsync(TokenValidatedContext context, JwtTokenInfo tokenInfo)
|
||||
{
|
||||
string userId = StateHelper.userId;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
throw new JwtValidationException(
|
||||
message: "未登录",
|
||||
statusCode: 200,
|
||||
errorCode: 401); // 自定义错误码:用户禁用
|
||||
}
|
||||
//var userService = App.GetService<IUnitUserService>();
|
||||
|
||||
//var userInfo = userService.GetUserInfoAsyc(userId);
|
||||
//if (userInfo == null)
|
||||
//{
|
||||
// throw new JwtValidationException(
|
||||
// message: "未登录",
|
||||
// statusCode: 200,
|
||||
// errorCode: 401); // 自定义错误码:用户禁用
|
||||
//}
|
||||
//if (userInfo.token != StateHelper.sid)
|
||||
//{
|
||||
// throw new JwtValidationException(
|
||||
// message: "未登录",
|
||||
// statusCode: 200,
|
||||
// errorCode: 401); // 自定义错误码:用户禁用
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Service/Application.Web/Common/StateHelper.cs
Normal file
21
Service/Application.Web/Common/StateHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Application.Web
|
||||
{
|
||||
public class StateHelper
|
||||
{
|
||||
public static string userId
|
||||
{
|
||||
get {
|
||||
var token = App.HttpContext.GetTokenInfo();
|
||||
return token.Claims["userId"];
|
||||
}
|
||||
}
|
||||
public static string sid
|
||||
{
|
||||
get
|
||||
{
|
||||
var token = App.HttpContext.GetTokenInfo();
|
||||
return token.Claims["sid"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Service/Application.Web/Controllers/Login/LoginController.cs
Normal file
26
Service/Application.Web/Controllers/Login/LoginController.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Application.Web.Controllers.Login
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录接口
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(GroupName = "Login")]
|
||||
[Route("[controller]/[action]")]
|
||||
[ApiController]
|
||||
public class LoginController : ControllerBase
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 登录接口
|
||||
/// </summary>
|
||||
/// <param name="parms"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IPoAction> Login([FromBody] LoginParms parms)
|
||||
{
|
||||
return PoAction.Ok(parms.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Service/Application.Web/GlobalUsings.cs
Normal file
16
Service/Application.Web/GlobalUsings.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
global using Photon.Core;
|
||||
global using Photon.Core.Jwt;
|
||||
global using Photon.Core.Services;
|
||||
global using Photon.Core.SqlSugar;
|
||||
global using Photon.Core.Assist;
|
||||
global using Photon.Core.Timer;
|
||||
|
||||
global using Application.Service.Pub;
|
||||
global using Application.Domain;
|
||||
global using Application.Domain.Entity;
|
||||
global using Microsoft.AspNetCore.Authorization;
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Application.Web;
|
||||
|
||||
|
||||
|
||||
22
Service/Application.Web/Plug/Timer/AutoJob.cs
Normal file
22
Service/Application.Web/Plug/Timer/AutoJob.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Quartz;
|
||||
namespace Application.Web;
|
||||
|
||||
public class AutoJob: ITimerAutoJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
string jobId = context.JobDetail.Key.Name;
|
||||
if (jobId == "DayHandle")
|
||||
{
|
||||
|
||||
////处理日红包
|
||||
//await businessService.HandleRedPack();
|
||||
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MMMM-dd")}:红包奖金池已处理!");
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("定时程序测试执行");
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Service/Application.Web/Plug/Timer/JobStart.cs
Normal file
68
Service/Application.Web/Plug/Timer/JobStart.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Quartz;
|
||||
using Quartz.Spi;
|
||||
|
||||
namespace Application.Web;
|
||||
public class JobStart: ITimerAutoStart
|
||||
{
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly IJobFactory _jobFactory;
|
||||
|
||||
public JobStart(
|
||||
ISchedulerFactory schedulerFactory,
|
||||
IJobFactory jobFactory)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_jobFactory = jobFactory;
|
||||
}
|
||||
|
||||
public IScheduler Scheduler { get; private set; }
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
||||
Scheduler.JobFactory = _jobFactory;
|
||||
|
||||
// 创建作业
|
||||
var job = JobBuilder.Create<AutoJob>()
|
||||
.WithIdentity("DayHandle", "Handle")
|
||||
.Build();
|
||||
|
||||
// 创建触发器
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity($"DayHandle_trigger", "Handle")
|
||||
.WithCronSchedule("0 0 0 * * ? ")
|
||||
.Build();
|
||||
|
||||
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
|
||||
await Scheduler.Start(cancellationToken);
|
||||
|
||||
Console.WriteLine("----航海时代V3自动程序已启动-----");
|
||||
}
|
||||
|
||||
public async Task Begin()
|
||||
{
|
||||
Scheduler = await _schedulerFactory.GetScheduler();
|
||||
Scheduler.JobFactory = _jobFactory;
|
||||
|
||||
// 创建作业
|
||||
var job = JobBuilder.Create<AutoJob>()
|
||||
.WithIdentity("111", "test")
|
||||
.Build();
|
||||
|
||||
// 创建触发器
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity($"111_trigger", "test")
|
||||
.WithCronSchedule("0/1 * * * * ? ")
|
||||
.Build();
|
||||
|
||||
await Scheduler.ScheduleJob(job, trigger);
|
||||
await Scheduler.Start();
|
||||
|
||||
Console.WriteLine("----探玩驿站自动程序已启动-----");
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Scheduler?.Shutdown(cancellationToken);
|
||||
}
|
||||
}
|
||||
56
Service/Application.Web/Plug/Timer/TimerJobManager.cs
Normal file
56
Service/Application.Web/Plug/Timer/TimerJobManager.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Quartz;
|
||||
|
||||
namespace Application.Web;
|
||||
|
||||
public class TimerJobManager : ITimerJobManager
|
||||
{
|
||||
private readonly IScheduler _scheduler;
|
||||
|
||||
public TimerJobManager(IScheduler scheduler)
|
||||
{
|
||||
_scheduler = scheduler;
|
||||
}
|
||||
|
||||
public async Task CreateAndStartTask(string jobId, string group, string cronExpression)
|
||||
{
|
||||
// 创建作业
|
||||
var job = JobBuilder.Create<AutoJob>()
|
||||
.WithIdentity(jobId, group)
|
||||
.Build();
|
||||
|
||||
// 创建触发器
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity($"{jobId}_trigger", group)
|
||||
.WithCronSchedule(cronExpression)
|
||||
.Build();
|
||||
|
||||
// 安排作业
|
||||
await _scheduler.ScheduleJob(job, trigger);
|
||||
|
||||
// 启动调度器(如果尚未启动)
|
||||
if (!_scheduler.IsStarted)
|
||||
{
|
||||
await _scheduler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PauseTask(string jobId)
|
||||
{
|
||||
await _scheduler.PauseJob(new JobKey(jobId, "default"));
|
||||
}
|
||||
|
||||
public async Task ResumeTask(string jobId, string group)
|
||||
{
|
||||
await _scheduler.ResumeJob(new JobKey(jobId, group));
|
||||
}
|
||||
|
||||
public async Task DeleteTask(string jobId, string group)
|
||||
{
|
||||
await _scheduler.DeleteJob(new JobKey(jobId, group));
|
||||
}
|
||||
|
||||
public async Task DeleteTask(string jobId)
|
||||
{
|
||||
await _scheduler.DeleteJob(new JobKey(jobId));
|
||||
}
|
||||
}
|
||||
109
Service/Application.Web/Program.cs
Normal file
109
Service/Application.Web/Program.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.Inject(new List<string> {
|
||||
"Application.Web","Application.Domain"
|
||||
});//框架初始化
|
||||
|
||||
builder.Configuration.RegisterConfig();//注入配置
|
||||
builder.Services.InjectMemory();
|
||||
builder.Services.InjectSql(builder.Configuration);
|
||||
|
||||
#region JWT与接口配置
|
||||
|
||||
Dictionary<string, OpenApiInfo> groups = new Dictionary<string, OpenApiInfo>();
|
||||
groups.Add("Login", new OpenApiInfo
|
||||
{
|
||||
Title = "登录接口文档",
|
||||
Version = "v1",
|
||||
Description = "航海时代登录相关接口。",
|
||||
});
|
||||
groups.Add("User", new OpenApiInfo
|
||||
{
|
||||
Title = "用户接口文档",
|
||||
Version = "v1",
|
||||
Description = "航海时代用户相关接口。",
|
||||
});
|
||||
|
||||
builder.Services.InjectJwt(builder.Configuration, new OpenApiInfo
|
||||
{
|
||||
Title = "航海时代接口文档",
|
||||
Version = "v1",
|
||||
Description = "航海时代接口文档,包含航海时代的所有接口,项目采用AI开发。",
|
||||
}, "Application.Web", op =>
|
||||
{
|
||||
op.AddTransient<IJwtValidator, Application.Web.JwtHandle>();
|
||||
}, groups);
|
||||
|
||||
#endregion
|
||||
|
||||
//定时功能
|
||||
builder.Services.InjectTimer(services =>
|
||||
{
|
||||
services.AddTransient<ITimerJobManager, TimerJobManager>();//注册管理器
|
||||
services.AddSingleton<AutoJob>();
|
||||
services.AddHostedService<JobStart>();
|
||||
});
|
||||
|
||||
//日志
|
||||
builder.Logging.InjectLog();
|
||||
// Add services to the container.
|
||||
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
|
||||
#region 配置跨域处理,允许所有来源
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("all", builder =>
|
||||
{
|
||||
builder.AllowAnyOrigin() //允许任何来源的主机访问
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
#endregion 配置跨域处理,允许所有来源
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.Services.UseInject();//启用框架
|
||||
app.Configuration.UseConfig();//启用配置
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
|
||||
|
||||
#region 测试swagger配置
|
||||
app.UseJwtSwagger("swagger", "航海时代接口文档",new List<JwtGroupConfig> {
|
||||
new JwtGroupConfig(){ groupId="Login", groupName="登录接口文档"},
|
||||
new JwtGroupConfig(){ groupId="User", groupName="用户接口文档"}
|
||||
});
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
app.UseCors("all");
|
||||
app.UseStaticFiles();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller=Home}/{action=Index}/{id?}")
|
||||
.WithStaticAssets();
|
||||
|
||||
SnowflakeAssist.Initialize(0, 0);
|
||||
app.Run();
|
||||
23
Service/Application.Web/Properties/launchSettings.json
Normal file
23
Service/Application.Web/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5031",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7198;http://localhost:5031",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Service/Application.Web/applicationsettings.json
Normal file
33
Service/Application.Web/applicationsettings.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"AutoProgram": 0,
|
||||
"Redis": {
|
||||
"connection": "127.0.0.1,password=,defaultdatabase=1"
|
||||
},
|
||||
"SqlData": [
|
||||
{
|
||||
"type": 0,
|
||||
"name": "KgDb",
|
||||
"connect": "data source=kx.iyba.cn;database=kx.petera;user id=root;password=1f5ozxRGE3Y;pooling=true;port=23306;sslmode=Required;charset=utf8mb4;"
|
||||
}
|
||||
],
|
||||
"JwtTokenOptions": {
|
||||
"Issuer": "kx.petera",
|
||||
"Audience": "kx.petera",
|
||||
"SecurityKey": "DAbdA9fF9MYXDCFBEb75WKWWDAbDBWQi"
|
||||
},
|
||||
"ResUrl": {
|
||||
"local": "http://192.168.0.142:5298",
|
||||
"resUrl": "http://localhost:12206",
|
||||
"imgCDN": "/",
|
||||
"videoCDN": "/"
|
||||
},
|
||||
"Sms": {
|
||||
"SmsType": 1,
|
||||
"AccessKey": "AKIDVptgCRP5UcT4PTGm1yf5E6pKYVBajeKn",
|
||||
"Secret": "FG2atxlKflcEclgKhnc9XeU3LM6YjdGf",
|
||||
"signName": "探玩驿站",
|
||||
"TemplateCode": "963929",
|
||||
"SmsSdkAppId": "1400523979",
|
||||
"SmsOnTime": 300
|
||||
}
|
||||
}
|
||||
8
Service/Application.Web/appsettings.Development.json
Normal file
8
Service/Application.Web/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Service/Application.Web/appsettings.json
Normal file
9
Service/Application.Web/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user