feat: 仓配开放 API 与后台用户启停
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
新增 warehouseOpenApi(Token/入库/件重尺/红外图),并在 fk_user 支持 IsEnabled 禁用登录。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ff9c589956
commit
e85dfb80b5
24
sql/migrate_user_is_enabled.sql
Normal file
24
sql/migrate_user_is_enabled.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- 后台用户启用/禁用(可重复执行)。
|
||||
-- 表名:fk_user(Fake RBAC)。默认启用;禁用后拒绝账号密码登录与仓配 Token 换取。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'fk_user'
|
||||
AND COLUMN_NAME = 'IsEnabled'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `fk_user` ADD COLUMN `IsEnabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否启用:1启用 0禁用'' AFTER `IsDeleted`',
|
||||
'SELECT ''fk_user.IsEnabled already exists'' AS info'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE `fk_user` SET `IsEnabled` = 1 WHERE `IsEnabled` IS NULL;
|
||||
108
src/Oms.Application/Dtos/OpenApi/OpenApiTokenDtos.cs
Normal file
108
src/Oms.Application/Dtos/OpenApi/OpenApiTokenDtos.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口获取 Token 入参。
|
||||
/// </summary>
|
||||
public class OpenApiGetTokenInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名(OMS 后台账号)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(128)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口简易结果(件重尺上传等)。
|
||||
/// </summary>
|
||||
public class OpenApiSimpleResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string Msg { get; set; } = string.Empty;
|
||||
|
||||
public static OpenApiSimpleResult Ok(string msg = "success") =>
|
||||
new() { Success = true, Msg = msg };
|
||||
|
||||
public static OpenApiSimpleResult Fail(string msg) =>
|
||||
new() { Success = false, Msg = msg };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓配上传件重尺入参(入库接口可忽略 mode,固定走入库扫描「入库」逻辑)。
|
||||
/// </summary>
|
||||
public class OpenApiUploadWeightInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号(服务单号 / 跟踪号 / 下游单号,同入库扫描扫码框)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 收货实重(同入库扫描「实重」)。
|
||||
/// </summary>
|
||||
public decimal Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 长(同入库扫描尺寸)。
|
||||
/// </summary>
|
||||
public decimal Length { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 宽。
|
||||
/// </summary>
|
||||
public decimal Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 高。
|
||||
/// </summary>
|
||||
public decimal Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模式:0=入库(同「入库」按钮),1=出库(同「出库」按钮)。调用 /inbound 时可省略。
|
||||
/// </summary>
|
||||
public int Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重量单位:KG(默认)/ G / LB。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? WeightUnit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 长度单位:CM(默认)/ MM / IN / M。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? LengthUnit { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓配上传红外图片入参(落订单附件 infraredImage,转存 COS)。
|
||||
/// </summary>
|
||||
public class OpenApiUploadImageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号(服务单号 / 跟踪号 / 下游单号)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图片 Base64(可带 data:image/...;base64, 前缀;jpg/png/gif/webp/bmp/pdf)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ImageBase64 { get; set; } = string.Empty;
|
||||
}
|
||||
@ -26,6 +26,11 @@ public class InternalUserListItemDto
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
public List<InternalUserRoleDto> Roles { get; set; } = [];
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,11 @@ public class UserProfileData
|
||||
/// 企业微信客服接入链接(微信外接入)。
|
||||
/// </summary>
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用;缺省按启用处理。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public class OmsUserCreateInput : UserCreateDto
|
||||
@ -29,6 +34,11 @@ public class OmsUserCreateInput : UserCreateDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用,默认 true。
|
||||
/// </summary>
|
||||
public bool? IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class OmsUserUpdateInput : UserUpdateDto
|
||||
@ -40,6 +50,11 @@ public class OmsUserUpdateInput : UserUpdateDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用;不传则不改。
|
||||
/// </summary>
|
||||
public bool? IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class OmsUserDetailDto : UserDto
|
||||
@ -53,6 +68,11 @@ public class OmsUserDetailDto : UserDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -65,3 +85,14 @@ public class ResetUserPasswordInput
|
||||
/// </summary>
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置用户启用状态入参。
|
||||
/// </summary>
|
||||
public class SetUserEnabledInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
|
||||
@ -21,6 +21,7 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
||||
)
|
||||
)
|
||||
";
|
||||
|
||||
public async Task<InternalUserListResultDto> GetInternalUsersAsync(
|
||||
string? keyword,
|
||||
Guid? organizationId,
|
||||
@ -66,7 +67,8 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
||||
u.`WechatId` AS `WechatId`,
|
||||
u.`OrganizationId` AS `OrganizationId`,
|
||||
o.`Name` AS `OrganizationName`,
|
||||
u.`CreateTime` AS `CreatedAt`
|
||||
u.`CreateTime` AS `CreatedAt`,
|
||||
COALESCE(u.`IsEnabled`, 1) AS `IsEnabled`
|
||||
FROM `{FakeTables.User}` u
|
||||
LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
{where}
|
||||
@ -95,6 +97,7 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
||||
OrganizationId = row.OrganizationId?.ToString(),
|
||||
OrganizationName = row.OrganizationName,
|
||||
CreatedAt = row.CreatedAt,
|
||||
IsEnabled = row.IsEnabled != 0,
|
||||
Roles = roleMap.TryGetValue(row.Id, out var roles) ? roles : []
|
||||
}).ToList();
|
||||
|
||||
@ -159,6 +162,8 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
||||
public string? OrganizationName { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
public int IsEnabled { get; set; } = 1;
|
||||
}
|
||||
|
||||
private sealed class UserRoleRow
|
||||
|
||||
@ -33,6 +33,7 @@ internal static class OmsUserProfileMapper
|
||||
detail.WechatId = profile.WechatId;
|
||||
detail.WechatQrCode = profile.WechatQrCode;
|
||||
detail.WecomKfUrl = profile.WecomKfUrl;
|
||||
detail.IsEnabled = profile.IsEnabled;
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
|
||||
@ -22,7 +23,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
u.`EmployeeNo` AS `EmployeeNo`,
|
||||
u.`WechatId` AS `WechatId`,
|
||||
u.`WechatQrCode` AS `WechatQrCode`,
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`,
|
||||
COALESCE(u.`IsEnabled`, 1) AS `IsEnabled`
|
||||
FROM `{FakeTables.User}` u
|
||||
WHERE u.`Id` = {{0}} AND u.`IsDeleted` = 0
|
||||
LIMIT 1
|
||||
@ -43,6 +45,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
WechatId = row.WechatId,
|
||||
WechatQrCode = row.WechatQrCode,
|
||||
WecomKfUrl = row.WecomKfUrl,
|
||||
IsEnabled = row.IsEnabled != 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -63,7 +66,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
u.`EmployeeNo` AS `EmployeeNo`,
|
||||
u.`WechatId` AS `WechatId`,
|
||||
u.`WechatQrCode` AS `WechatQrCode`,
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`,
|
||||
COALESCE(u.`IsEnabled`, 1) AS `IsEnabled`
|
||||
FROM `{FakeTables.User}` u
|
||||
WHERE u.`Id` IN ({inClause}) AND u.`IsDeleted` = 0
|
||||
";
|
||||
@ -78,6 +82,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
WechatId = x.WechatId,
|
||||
WechatQrCode = x.WechatQrCode,
|
||||
WecomKfUrl = x.WecomKfUrl,
|
||||
IsEnabled = x.IsEnabled != 0,
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,9 +95,11 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
`EmployeeNo` = {{2}},
|
||||
`WechatId` = {{3}},
|
||||
`WechatQrCode` = {{4}},
|
||||
`WecomKfUrl` = {{5}}
|
||||
`WecomKfUrl` = {{5}},
|
||||
`IsEnabled` = {{6}}
|
||||
WHERE `Id` = {{0}} AND `IsDeleted` = 0
|
||||
";
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
sql,
|
||||
[
|
||||
@ -103,10 +110,52 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
(object?)NormalizeOptional(profile.WechatId),
|
||||
(object?)NormalizeOptional(profile.WechatQrCode),
|
||||
(object?)NormalizeOptional(profile.WecomKfUrl),
|
||||
profile.IsEnabled ? 1 : 0,
|
||||
],
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SetEnabledAsync(Guid userId, bool isEnabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
UPDATE `{FakeTables.User}`
|
||||
SET `IsEnabled` = {{1}}
|
||||
WHERE `Id` = {{0}} AND `IsDeleted` = 0
|
||||
";
|
||||
|
||||
var affected = await dbContext.Database.ExecuteSqlRawAsync(
|
||||
sql,
|
||||
[userId.ToString(), isEnabled ? 1 : 0],
|
||||
cancellationToken);
|
||||
if (affected <= 0)
|
||||
{
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureEnabledOrThrowAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
SELECT COALESCE(u.`IsEnabled`, 1) AS `Value`
|
||||
FROM `{FakeTables.User}` u
|
||||
WHERE u.`Id` = {{0}} AND u.`IsDeleted` = 0
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
var rows = await dbContext.Database
|
||||
.SqlQueryRaw<long>(sql, userId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
if (rows[0] == 0)
|
||||
{
|
||||
throw new BusinessException("账号已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateContactAsync(
|
||||
Guid userId,
|
||||
UserProfileData profile,
|
||||
@ -186,8 +235,6 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
|
||||
if (int.TryParse(match.Groups[1].Value, out var number))
|
||||
{
|
||||
|
||||
|
||||
max = Math.Max(max, number);
|
||||
}
|
||||
}
|
||||
@ -219,6 +266,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
public int IsEnabled { get; set; } = 1;
|
||||
}
|
||||
|
||||
private sealed class UserProfileListRow
|
||||
@ -234,6 +283,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
public int IsEnabled { get; set; } = 1;
|
||||
}
|
||||
|
||||
private sealed class EmployeeNoRow
|
||||
|
||||
@ -14,9 +14,10 @@ using Oms.Domain.OrdersAggregate.Consts;
|
||||
namespace Oms.Application.Services.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 对外开放 API(Swagger 文档分组:openApi)。
|
||||
/// 客户对外开放 API(Swagger 文档分组:openApi)。
|
||||
/// 统一响应:{ code, msg, data }。
|
||||
/// 客户下单鉴权:Header appKey + timestamp + sign(HMAC-SHA256,5 分钟窗口);Secret 不上传。
|
||||
/// 鉴权:Header appKey + timestamp + sign(HMAC-SHA256,5 分钟窗口);Secret 不上传。
|
||||
/// 仓配 Token 接口见分组 warehouseOpenApi。
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(GroupName = "openApi")]
|
||||
public class OpenApiService : ApplicationService
|
||||
|
||||
201
src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs
Normal file
201
src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs
Normal file
@ -0,0 +1,201 @@
|
||||
using Fake.Application;
|
||||
using Fake.Rbac.Application.Jwt;
|
||||
using Fake.Rbac.Domain.Managers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.OpenApi;
|
||||
using Oms.Application.Dtos.Operations;
|
||||
using Oms.Application.Rbac;
|
||||
using Oms.Application.Services.Operations;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 仓配对外开放 API(Swagger 文档分组:warehouseOpenApi)。
|
||||
/// 鉴权:Authorization: Bearer {token}(账号密码换取,有效期 48 小时)。
|
||||
/// 与客户 HMAC 分组 openApi 相互独立。
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(GroupName = "warehouseOpenApi")]
|
||||
public class WarehouseOpenApiService : ApplicationService
|
||||
{
|
||||
private AccountManager AccountManager =>
|
||||
LazyServiceProvider.GetRequiredService<AccountManager>();
|
||||
|
||||
private IJwtService JwtService =>
|
||||
LazyServiceProvider.GetRequiredService<IJwtService>();
|
||||
|
||||
private OpenApiTokenIssuer TokenIssuer =>
|
||||
LazyServiceProvider.GetRequiredService<OpenApiTokenIssuer>();
|
||||
|
||||
private IOrderRepository OrderRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||||
|
||||
private OrderFileManager OrderFileManager =>
|
||||
LazyServiceProvider.GetRequiredService<OrderFileManager>();
|
||||
|
||||
private InboundRecordService InboundRecordService =>
|
||||
LazyServiceProvider.GetRequiredService<InboundRecordService>();
|
||||
|
||||
private OutboundRecordService OutboundRecordService =>
|
||||
LazyServiceProvider.GetRequiredService<OutboundRecordService>();
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Token(有效期 48 小时)。后续请求 Header:Authorization: Bearer {token}。
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("/openapi/warehouse/token")]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public async Task<string> GetTokenAsync(
|
||||
[FromBody] OpenApiGetTokenInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null
|
||||
|| string.IsNullOrWhiteSpace(input.Username)
|
||||
|| string.IsNullOrWhiteSpace(input.Password))
|
||||
{
|
||||
throw new BusinessException("用户名或密码不能为空");
|
||||
}
|
||||
|
||||
var user = await AccountManager.ValidateCredentialsAsync(
|
||||
input.Username.Trim(),
|
||||
input.Password,
|
||||
cancellationToken);
|
||||
var profileRepository = LazyServiceProvider.GetRequiredService<UserProfileRepository>();
|
||||
await profileRepository.EnsureEnabledOrThrowAsync(user.Id, cancellationToken);
|
||||
var claims = await JwtService.GenerateClaimsByUserIdAsync(user.Id, cancellationToken);
|
||||
return TokenIssuer.Issue(claims);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入库上传件重尺。与操作台「入库扫描」页「入库」按钮同源(POST /operation/inbound/create)。
|
||||
/// 写入收货件重尺、入库记录,并触发入库计费/自动触发。重量默认 kg,长度默认 cm。
|
||||
/// Header:Authorization: Bearer {token}。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("/openapi/warehouse/inbound")]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public Task<OpenApiSimpleResult> InboundAsync([FromBody] OpenApiUploadWeightInput input) =>
|
||||
UploadWeightCoreAsync(input, forceMode: 0);
|
||||
|
||||
/// <summary>
|
||||
/// 上传件重尺。mode:0=入库(同「入库」按钮),1=出库(同「出库」按钮)。
|
||||
/// Header:Authorization: Bearer {token}。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("/openapi/warehouse/weight/upload")]
|
||||
[Consumes("application/json")]
|
||||
[Produces("application/json")]
|
||||
public Task<OpenApiSimpleResult> UploadWeightAsync([FromBody] OpenApiUploadWeightInput input) =>
|
||||
UploadWeightCoreAsync(input, forceMode: null);
|
||||
|
||||
private async Task<OpenApiSimpleResult> UploadWeightCoreAsync(
|
||||
OpenApiUploadWeightInput? input,
|
||||
int? forceMode)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (input == null || string.IsNullOrWhiteSpace(input.OrderNo))
|
||||
{
|
||||
return OpenApiSimpleResult.Fail("订单号不能为空");
|
||||
}
|
||||
|
||||
var mode = forceMode ?? input.Mode;
|
||||
var weightKg = OpenApiMeasureUnitConverter.ToKilograms(input.Weight, input.WeightUnit);
|
||||
var lengthCm = OpenApiMeasureUnitConverter.ToCentimeters(input.Length, input.LengthUnit);
|
||||
var widthCm = OpenApiMeasureUnitConverter.ToCentimeters(input.Width, input.LengthUnit);
|
||||
var heightCm = OpenApiMeasureUnitConverter.ToCentimeters(input.Height, input.LengthUnit);
|
||||
var orderNo = input.OrderNo.Trim();
|
||||
|
||||
if (weightKg <= 0 || lengthCm <= 0 || widthCm <= 0 || heightCm <= 0)
|
||||
{
|
||||
return OpenApiSimpleResult.Fail("实重与长宽高须大于 0");
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
await OutboundRecordService.CreateAsync(new CreateOutboundRecordInput
|
||||
{
|
||||
WaybillNo = orderNo,
|
||||
Weight = weightKg,
|
||||
Length = lengthCm,
|
||||
Width = widthCm,
|
||||
Height = heightCm
|
||||
});
|
||||
return OpenApiSimpleResult.Ok("出库成功");
|
||||
}
|
||||
|
||||
if (mode == 0)
|
||||
{
|
||||
// 与入库扫描页「入库」按钮同一实现:InboundRecordService.CreateAsync
|
||||
var result = await InboundRecordService.CreateAsync(new CreateInboundRecordInput
|
||||
{
|
||||
WaybillNo = orderNo,
|
||||
Weight = weightKg,
|
||||
Length = lengthCm,
|
||||
Width = widthCm,
|
||||
Height = heightCm
|
||||
});
|
||||
var statusName = string.IsNullOrWhiteSpace(result.OrderStatusName)
|
||||
? "入库成功"
|
||||
: $"入库成功({result.OrderStatusName})";
|
||||
return OpenApiSimpleResult.Ok(statusName);
|
||||
}
|
||||
|
||||
return OpenApiSimpleResult.Fail("mode 仅支持 0(入库)或 1(出库)");
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
return OpenApiSimpleResult.Fail(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return OpenApiSimpleResult.Fail(
|
||||
string.IsNullOrWhiteSpace(ex.Message) ? "系统异常" : ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传红外图片。Base64 解码后转存 COS,写入订单附件 fileType=infraredImage(Source=upload),
|
||||
/// 与订单详情「附件」Tab →「红外图片」→「点击上传」(POST /order/files/upload)同源展示。
|
||||
/// 成功无响应体。Header:Authorization: Bearer {token}。
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("/openapi/warehouse/image/upload")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<IActionResult> UploadImageAsync(
|
||||
[FromBody] OpenApiUploadImageInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (input == null || string.IsNullOrWhiteSpace(input.OrderNo))
|
||||
{
|
||||
throw new BusinessException("订单号不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.ImageBase64))
|
||||
{
|
||||
throw new BusinessException("图片内容不能为空");
|
||||
}
|
||||
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(input.OrderNo.Trim(), cancellationToken)
|
||||
?? throw new BusinessException("未找到对应订单,请检查单号");
|
||||
|
||||
var operatorUserId = CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0;
|
||||
// 固定红外图片类型,路径 uploads/order-files/{orderId}/infraredImage/...
|
||||
await OrderFileManager.AddFromBase64Async(
|
||||
order.Id,
|
||||
OrderFileTypeCodes.InfraredImage,
|
||||
input.ImageBase64,
|
||||
operatorUserId,
|
||||
cancellationToken);
|
||||
|
||||
return new NoContentResult();
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ public class InboundRecordService : ApplicationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增入库记录。
|
||||
/// 新增入库记录(入库扫描页「入库」按钮;仓配开放接口 POST /openapi/warehouse/inbound 同源)。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:入库记录创建参数。</param>
|
||||
/// <returns>入库记录结果。</returns>
|
||||
|
||||
@ -363,6 +363,221 @@ public sealed class OrderFileManager(
|
||||
return ".pdf";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Base64 写入订单附件:先转存对象存储(COS),再落 t_order_file。
|
||||
/// 与订单详情附件 Tab「点击上传」(POST /order/files/upload)同源校验与路径约定。
|
||||
/// </summary>
|
||||
public async Task<OrderFileItemOutput> AddFromBase64Async(
|
||||
int orderId,
|
||||
string fileType,
|
||||
string imageBase64,
|
||||
int operatorUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!OrderFileTypeCodes.IsValid(fileType))
|
||||
{
|
||||
throw new BusinessException("不支持的附件类型");
|
||||
}
|
||||
|
||||
var type = OrderFileTypeCodes.Normalize(fileType);
|
||||
if (string.IsNullOrWhiteSpace(imageBase64))
|
||||
{
|
||||
throw new BusinessException("图片内容不能为空");
|
||||
}
|
||||
|
||||
var (bytes, contentType, ext) = DecodeBase64Image(imageBase64);
|
||||
if (bytes.Length == 0)
|
||||
{
|
||||
throw new BusinessException("图片内容不能为空");
|
||||
}
|
||||
|
||||
var existing = await orderFileRepository.GetByOrderAndTypeListAsync(orderId, type, cancellationToken);
|
||||
var max = OrderFileTypeCodes.GetMaxCount(type);
|
||||
if (existing.Count >= max)
|
||||
{
|
||||
throw new BusinessException($"类型 {type} 最多 {max} 个文件");
|
||||
}
|
||||
|
||||
var fileName = $"{type}_{DateTime.Now:yyyyMMddHHmmss}{ext}";
|
||||
var savedPath = await SaveBytesAsync(bytes, fileName, contentType, orderId, type, cancellationToken);
|
||||
|
||||
var sort = existing.Count == 0 ? 0 : existing.Max(x => x.Sort) + 1;
|
||||
var entity = new OrderFile
|
||||
{
|
||||
OrderId = orderId,
|
||||
FileType = type,
|
||||
FileName = savedPath.FileName,
|
||||
FileUrl = savedPath.Url,
|
||||
ContentType = savedPath.ContentType,
|
||||
FileSize = savedPath.Size,
|
||||
Sort = sort,
|
||||
Source = OrderFileSourceCodes.Upload,
|
||||
CreateUserId = operatorUserId,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
var saved = await orderFileRepository.InsertAsync(entity, cancellationToken);
|
||||
return MapItem(saved);
|
||||
}
|
||||
|
||||
private static (byte[] Bytes, string ContentType, string Ext) DecodeBase64Image(string imageBase64)
|
||||
{
|
||||
var raw = imageBase64.Trim();
|
||||
string? headerContentType = null;
|
||||
|
||||
if (raw.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var comma = raw.IndexOf(',');
|
||||
if (comma <= 0)
|
||||
{
|
||||
throw new BusinessException("图片 Base64 格式错误");
|
||||
}
|
||||
|
||||
var header = raw[..comma];
|
||||
raw = raw[(comma + 1)..];
|
||||
var mimeStart = header.IndexOf(':');
|
||||
var mimeEnd = header.IndexOf(';');
|
||||
if (mimeStart >= 0 && mimeEnd > mimeStart)
|
||||
{
|
||||
headerContentType = header[(mimeStart + 1)..mimeEnd].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = Convert.FromBase64String(raw);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new BusinessException("图片 Base64 解码失败");
|
||||
}
|
||||
|
||||
var sniffed = SniffImageFormat(bytes);
|
||||
var contentType = headerContentType ?? sniffed.ContentType;
|
||||
var ext = sniffed.Ext;
|
||||
if (!string.IsNullOrWhiteSpace(headerContentType))
|
||||
{
|
||||
ext = headerContentType.ToLowerInvariant() switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/webp" => ".webp",
|
||||
"image/gif" => ".gif",
|
||||
"image/bmp" => ".bmp",
|
||||
"application/pdf" => ".pdf",
|
||||
"image/jpeg" or "image/jpg" => ".jpg",
|
||||
_ => sniffed.Ext
|
||||
};
|
||||
}
|
||||
|
||||
return (bytes, contentType, ext);
|
||||
}
|
||||
|
||||
private static (string ContentType, string Ext) SniffImageFormat(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length >= 3 && bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF)
|
||||
{
|
||||
return ("image/jpeg", ".jpg");
|
||||
}
|
||||
|
||||
if (bytes.Length >= 8
|
||||
&& bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47)
|
||||
{
|
||||
return ("image/png", ".png");
|
||||
}
|
||||
|
||||
if (bytes.Length >= 6
|
||||
&& bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46)
|
||||
{
|
||||
return ("image/gif", ".gif");
|
||||
}
|
||||
|
||||
if (bytes.Length >= 12
|
||||
&& bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46
|
||||
&& bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50)
|
||||
{
|
||||
return ("image/webp", ".webp");
|
||||
}
|
||||
|
||||
if (bytes.Length >= 2 && bytes[0] == 0x42 && bytes[1] == 0x4D)
|
||||
{
|
||||
return ("image/bmp", ".bmp");
|
||||
}
|
||||
|
||||
if (bytes.Length >= 5
|
||||
&& bytes[0] == 0x25 && bytes[1] == 0x50 && bytes[2] == 0x44 && bytes[3] == 0x46)
|
||||
{
|
||||
return ("application/pdf", ".pdf");
|
||||
}
|
||||
|
||||
return ("image/jpeg", ".jpg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 与 multipart 上传共用:校验扩展名/大小后写入 COS,返回逻辑 object key。
|
||||
/// </summary>
|
||||
private async Task<(string Url, string FileName, string? ContentType, long Size)> SaveBytesAsync(
|
||||
byte[] bytes,
|
||||
string fileName,
|
||||
string? contentType,
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (bytes == null || bytes.Length <= 0)
|
||||
{
|
||||
throw new BusinessException("上传文件不能为空");
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(fileName);
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
{
|
||||
ext = ".bin";
|
||||
}
|
||||
|
||||
var isLabelType = fileType is OrderFileTypeCodes.SystemLabel
|
||||
or OrderFileTypeCodes.DownstreamLabel
|
||||
or OrderFileTypeCodes.LastMileLabel;
|
||||
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
||||
or OrderFileTypeCodes.TradeProof
|
||||
or OrderFileTypeCodes.Pod;
|
||||
|
||||
if (isLabelType)
|
||||
{
|
||||
if (!LabelExt.Contains(ext))
|
||||
{
|
||||
throw new BusinessException("面单仅支持 pdf / jpg / png");
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && bytes.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("PDF 不能超过 10MB");
|
||||
}
|
||||
|
||||
if (!string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && bytes.Length > MaxImageBytes)
|
||||
{
|
||||
throw new BusinessException("图片不能超过 5MB");
|
||||
}
|
||||
}
|
||||
else if (isInfraredOrProof)
|
||||
{
|
||||
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BusinessException("交易证明/红外图片/POD 仅支持图片或 pdf");
|
||||
}
|
||||
|
||||
if (bytes.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("文件不能超过 10MB");
|
||||
}
|
||||
}
|
||||
|
||||
var storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}";
|
||||
await using var stream = new MemoryStream(bytes);
|
||||
var key = await fileStorage.SaveObjectAsync(stream, relativeKey, contentType, cancellationToken);
|
||||
return (key, Path.GetFileName(fileName), contentType, bytes.Length);
|
||||
}
|
||||
|
||||
public async Task<List<OrderFileItemOutput>> UploadAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
@ -469,54 +684,15 @@ public sealed class OrderFileManager(
|
||||
throw new BusinessException("上传文件不能为空");
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
{
|
||||
ext = ".bin";
|
||||
}
|
||||
|
||||
var isLabelType = fileType is OrderFileTypeCodes.SystemLabel
|
||||
or OrderFileTypeCodes.DownstreamLabel
|
||||
or OrderFileTypeCodes.LastMileLabel;
|
||||
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
||||
or OrderFileTypeCodes.TradeProof
|
||||
or OrderFileTypeCodes.Pod;
|
||||
|
||||
if (isLabelType)
|
||||
{
|
||||
if (!LabelExt.Contains(ext))
|
||||
{
|
||||
throw new BusinessException("面单仅支持 pdf / jpg / png");
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && file.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("PDF 不能超过 10MB");
|
||||
}
|
||||
|
||||
if (!string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && file.Length > MaxImageBytes)
|
||||
{
|
||||
throw new BusinessException("图片不能超过 5MB");
|
||||
}
|
||||
}
|
||||
else if (isInfraredOrProof)
|
||||
{
|
||||
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BusinessException("交易证明/红外图片/POD 仅支持图片或 pdf");
|
||||
}
|
||||
|
||||
if (file.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("文件不能超过 10MB");
|
||||
}
|
||||
}
|
||||
|
||||
var storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}";
|
||||
await using var stream = file.OpenReadStream();
|
||||
var key = await fileStorage.SaveObjectAsync(stream, relativeKey, file.ContentType);
|
||||
return (key, Path.GetFileName(file.FileName), file.ContentType, file.Length);
|
||||
await using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
return await SaveBytesAsync(
|
||||
ms.ToArray(),
|
||||
file.FileName,
|
||||
file.ContentType,
|
||||
orderId,
|
||||
fileType);
|
||||
}
|
||||
|
||||
private OrderFileItemOutput MapItem(OrderFile x) =>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Fake.Application;
|
||||
using Fake.Domain.Exceptions;
|
||||
using Fake.ObjectStorage;
|
||||
using Fake.Rbac.Application.Dtos.Common;
|
||||
using Fake.Rbac.Application.Dtos.Role;
|
||||
using Fake.Rbac.Application.Dtos.User;
|
||||
@ -8,7 +10,6 @@ using Fake.Rbac.Domain.Managers;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fake.ObjectStorage;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
using Oms.Application.Rbac;
|
||||
using Oms.Application.Shared;
|
||||
@ -157,6 +158,7 @@ public class OmsUserService(
|
||||
WechatId = input.WechatId,
|
||||
WechatQrCode = input.WechatQrCode,
|
||||
WecomKfUrl = input.WecomKfUrl,
|
||||
IsEnabled = input.IsEnabled ?? true,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
@ -242,6 +244,12 @@ public class OmsUserService(
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (input.IsEnabled.HasValue)
|
||||
{
|
||||
EnsureCanChangeEnabled(id, input.IsEnabled.Value);
|
||||
await profileRepository.SetEnabledAsync(id, input.IsEnabled.Value, cancellationToken);
|
||||
}
|
||||
|
||||
var profile = await profileRepository.GetAsync(id, cancellationToken);
|
||||
return MapUserDetail(user, profile);
|
||||
}
|
||||
@ -251,6 +259,29 @@ public class OmsUserService(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用或禁用用户。禁用后无法登录后台。
|
||||
/// </summary>
|
||||
[HttpPost("/rbac/user/set-enabled")]
|
||||
public async Task SetEnabledAsync(
|
||||
[FromQuery] Guid id,
|
||||
[FromBody] SetUserEnabledInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new BusinessException("用户 Id 无效");
|
||||
}
|
||||
|
||||
if (input == null)
|
||||
{
|
||||
throw new BusinessException("请求体不能为空");
|
||||
}
|
||||
|
||||
EnsureCanChangeEnabled(id, input.IsEnabled);
|
||||
await profileRepository.SetEnabledAsync(id, input.IsEnabled, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改用户密码。
|
||||
/// </summary>
|
||||
@ -391,4 +422,18 @@ public class OmsUserService(
|
||||
detail.Avatar = fileStorage.GetPublicUrl(detail.Avatar);
|
||||
return detail;
|
||||
}
|
||||
|
||||
private void EnsureCanChangeEnabled(Guid targetUserId, bool isEnabled)
|
||||
{
|
||||
if (isEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentIdText = CurrentUser.FindClaimOrNull(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (Guid.TryParse(currentIdText, out var currentId) && currentId == targetUserId)
|
||||
{
|
||||
throw new BusinessException("不能禁用当前登录账号");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Oms.Application/Shared/OpenApiMeasureUnitConverter.cs
Normal file
34
src/Oms.Application/Shared/OpenApiMeasureUnitConverter.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Fake.Application;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口件重尺单位换算为系统内部单位(kg / cm)。
|
||||
/// </summary>
|
||||
public static class OpenApiMeasureUnitConverter
|
||||
{
|
||||
public static decimal ToKilograms(decimal weight, string? weightUnit)
|
||||
{
|
||||
var unit = string.IsNullOrWhiteSpace(weightUnit) ? "KG" : weightUnit.Trim().ToUpperInvariant();
|
||||
return unit switch
|
||||
{
|
||||
"KG" or "KGS" or "KILOGRAM" or "KILOGRAMS" => weight,
|
||||
"G" or "GRAM" or "GRAMS" => weight / 1000m,
|
||||
"LB" or "LBS" or "POUND" or "POUNDS" => weight * 0.45359237m,
|
||||
_ => throw new BusinessException($"不支持的重量单位:{weightUnit}")
|
||||
};
|
||||
}
|
||||
|
||||
public static decimal ToCentimeters(decimal length, string? lengthUnit)
|
||||
{
|
||||
var unit = string.IsNullOrWhiteSpace(lengthUnit) ? "CM" : lengthUnit.Trim().ToUpperInvariant();
|
||||
return unit switch
|
||||
{
|
||||
"CM" or "CMS" or "CENTIMETER" or "CENTIMETERS" => length,
|
||||
"MM" or "MILLIMETER" or "MILLIMETERS" => length / 10m,
|
||||
"M" or "METER" or "METERS" => length * 100m,
|
||||
"IN" or "INCH" or "INCHES" => length * 2.54m,
|
||||
_ => throw new BusinessException($"不支持的长度单位:{lengthUnit}")
|
||||
};
|
||||
}
|
||||
}
|
||||
47
src/Oms.Application/Shared/OpenApiTokenIssuer.cs
Normal file
47
src/Oms.Application/Shared/OpenApiTokenIssuer.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Fake.AspNetCore.Authentication;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 仓配开放接口 JWT 签发(固定 48 小时有效期,签名密钥与门户共用)。
|
||||
/// </summary>
|
||||
public class OpenApiTokenIssuer(IOptions<FakeJwtOptions> jwtOptions) : ITransientDependency
|
||||
{
|
||||
public static readonly TimeSpan TokenLifetime = TimeSpan.FromHours(48);
|
||||
|
||||
public const string TokenKindClaim = "token_kind";
|
||||
public const string TokenKindOpenApi = "openapi";
|
||||
|
||||
public string Issue(IEnumerable<Claim> claims)
|
||||
{
|
||||
var options = jwtOptions.Value;
|
||||
if (string.IsNullOrWhiteSpace(options.SecretKey))
|
||||
{
|
||||
throw new InvalidOperationException("Jwt SecretKey 未配置");
|
||||
}
|
||||
|
||||
var list = claims
|
||||
.Where(x => !string.Equals(x.Type, TokenKindClaim, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer))
|
||||
.ToList();
|
||||
list.Add(new Claim(TokenKindClaim, TokenKindOpenApi));
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.SecretKey));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
var now = DateTime.UtcNow;
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: options.Issuer,
|
||||
audience: options.Audience,
|
||||
claims: list,
|
||||
notBefore: now,
|
||||
expires: now.Add(TokenLifetime),
|
||||
signingCredentials: credentials);
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@ -433,6 +433,8 @@ public partial class AuthService(
|
||||
private async Task<OmsLoginResultDto> LoginAdminAsync(string account, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken);
|
||||
var profileRepository = LazyServiceProvider.GetRequiredService<Oms.Application.Rbac.UserProfileRepository>();
|
||||
await profileRepository.EnsureEnabledOrThrowAsync(user.Id, cancellationToken);
|
||||
return await BuildLoginResultAsync(user.Id, OmsLoginType.User, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user