Commit e5c6f34308abe54cbdc30a389b0a2871fb552752
1 parent
6faaf539
登陆接口实现
Showing
7 changed files
with
497 additions
and
0 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppBoundLocationDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// App 端展示的绑定门店信息 | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppBoundLocationDto | |
| 7 | +{ | |
| 8 | + /// <summary>门店主键 Id(Guid 字符串)</summary> | |
| 9 | + public string Id { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + /// <summary>业务编码,如 LOC-1</summary> | |
| 12 | + public string LocationCode { get; set; } = string.Empty; | |
| 13 | + | |
| 14 | + /// <summary>门店名称</summary> | |
| 15 | + public string LocationName { get; set; } = string.Empty; | |
| 16 | + | |
| 17 | + /// <summary>拼接后的完整地址,便于移动端展示</summary> | |
| 18 | + public string FullAddress { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + /// <summary>门店是否启用</summary> | |
| 21 | + public bool State { get; set; } | |
| 22 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 美国版 App 登录入参(使用邮箱作为账号,支持图形验证码) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppLoginInputVo | |
| 7 | +{ | |
| 8 | + /// <summary>登录邮箱(对应 user.Email)</summary> | |
| 9 | + public string Email { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + public string Password { get; set; } = string.Empty; | |
| 12 | + | |
| 13 | + /// <summary>图形验证码 UUID(开启验证码时必填)</summary> | |
| 14 | + public string? Uuid { get; set; } | |
| 15 | + | |
| 16 | + /// <summary>图形验证码(开启验证码时必填)</summary> | |
| 17 | + public string? Code { get; set; } | |
| 18 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 美国版 App 登录返回(含 Token 与绑定门店) | |
| 5 | +/// </summary> | |
| 6 | +public class UsAppLoginOutputDto | |
| 7 | +{ | |
| 8 | + public string Token { get; set; } = string.Empty; | |
| 9 | + | |
| 10 | + public string RefreshToken { get; set; } = string.Empty; | |
| 11 | + | |
| 12 | + /// <summary>当前账号在 userlocation 中绑定的门店列表</summary> | |
| 13 | + public List<UsAppBoundLocationDto> Locations { get; set; } = new(); | |
| 14 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 2 | +using Volo.Abp.Application.Services; | |
| 3 | + | |
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | |
| 5 | + | |
| 6 | +/// <summary> | |
| 7 | +/// 美国版移动端认证(登录返回绑定门店) | |
| 8 | +/// </summary> | |
| 9 | +public interface IUsAppAuthAppService : IApplicationService | |
| 10 | +{ | |
| 11 | + /// <summary> | |
| 12 | + /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店 | |
| 13 | + /// </summary> | |
| 14 | + Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input); | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 获取当前登录账号已绑定的门店(用于切换门店等场景) | |
| 18 | + /// </summary> | |
| 19 | + Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); | |
| 20 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
| ... | ... | @@ -2,6 +2,10 @@ |
| 2 | 2 | <Import Project="..\..\..\common.props" /> |
| 3 | 3 | |
| 4 | 4 | <ItemGroup> |
| 5 | + <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> | |
| 6 | + </ItemGroup> | |
| 7 | + | |
| 8 | + <ItemGroup> | |
| 5 | 9 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application\Yi.Framework.Ddd.Application.csproj" /> |
| 6 | 10 | <ProjectReference Include="..\FoodLabeling.Application.Contracts\FoodLabeling.Application.Contracts.csproj" /> |
| 7 | 11 | <ProjectReference Include="..\FoodLabeling.Domain\FoodLabeling.Domain.csproj" /> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
0 → 100644
| 1 | +using System; | |
| 2 | +using System.Collections.Generic; | |
| 3 | +using System.IdentityModel.Tokens.Jwt; | |
| 4 | +using System.Linq; | |
| 5 | +using System.Security.Claims; | |
| 6 | +using System.Text; | |
| 7 | +using System.Threading.Tasks; | |
| 8 | +using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | |
| 9 | +using FoodLabeling.Application.Contracts.IServices; | |
| 10 | +using FoodLabeling.Application.Services.DbModels; | |
| 11 | +using FoodLabeling.Domain.Entities; | |
| 12 | +using Lazy.Captcha.Core; | |
| 13 | +using Mapster; | |
| 14 | +using Microsoft.AspNetCore.Authorization; | |
| 15 | +using Microsoft.AspNetCore.Http; | |
| 16 | +using Microsoft.Extensions.Options; | |
| 17 | +using Microsoft.IdentityModel.Tokens; | |
| 18 | +using SqlSugar; | |
| 19 | +using Volo.Abp; | |
| 20 | +using Volo.Abp.Application.Services; | |
| 21 | +using Volo.Abp.EventBus.Local; | |
| 22 | +using Volo.Abp.Security.Claims; | |
| 23 | +using Volo.Abp.Users; | |
| 24 | +using Yi.Framework.Core.Helper; | |
| 25 | +using Yi.Framework.Rbac.Domain.Entities; | |
| 26 | +using Yi.Framework.Rbac.Domain.Managers; | |
| 27 | +using Yi.Framework.Rbac.Domain.Shared.Consts; | |
| 28 | +using Yi.Framework.Rbac.Domain.Shared.Dtos; | |
| 29 | +using Yi.Framework.Rbac.Domain.Shared.Etos; | |
| 30 | +using Yi.Framework.Rbac.Domain.Shared.Options; | |
| 31 | +using Yi.Framework.SqlSugarCore.Abstractions; | |
| 32 | + | |
| 33 | +namespace FoodLabeling.Application.Services; | |
| 34 | + | |
| 35 | +/// <summary> | |
| 36 | +/// 美国版 App 登录:邮箱 + 密码(与 AccountManager 相同盐值哈希)签发 JWT,并返回 userlocation 绑定门店 | |
| 37 | +/// </summary> | |
| 38 | +public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | |
| 39 | +{ | |
| 40 | + private readonly IAccountManager _accountManager; | |
| 41 | + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository; | |
| 42 | + private readonly ISqlSugarDbContext _dbContext; | |
| 43 | + private readonly IHttpContextAccessor _httpContextAccessor; | |
| 44 | + private readonly ICaptcha _captcha; | |
| 45 | + private readonly RbacOptions _rbacOptions; | |
| 46 | + private readonly JwtOptions _jwtOptions; | |
| 47 | + | |
| 48 | + public UsAppAuthAppService( | |
| 49 | + IAccountManager accountManager, | |
| 50 | + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository, | |
| 51 | + ISqlSugarDbContext dbContext, | |
| 52 | + IHttpContextAccessor httpContextAccessor, | |
| 53 | + ICaptcha captcha, | |
| 54 | + IOptions<JwtOptions> jwtOptions, | |
| 55 | + IOptions<RbacOptions> rbacOptions) | |
| 56 | + { | |
| 57 | + _accountManager = accountManager; | |
| 58 | + _userRepository = userRepository; | |
| 59 | + _dbContext = dbContext; | |
| 60 | + _httpContextAccessor = httpContextAccessor; | |
| 61 | + _captcha = captcha; | |
| 62 | + _jwtOptions = jwtOptions.Value; | |
| 63 | + _rbacOptions = rbacOptions.Value; | |
| 64 | + } | |
| 65 | + | |
| 66 | + protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>(); | |
| 67 | + | |
| 68 | + /// <summary> | |
| 69 | + /// App 登录:签发 Token / RefreshToken,并返回当前账号绑定的门店列表 | |
| 70 | + /// </summary> | |
| 71 | + /// <remarks> | |
| 72 | + /// 行为与系统 <c>AccountService.PostLoginAsync</c> 一致(含验证码、登录日志事件)。 | |
| 73 | + /// 门店数据来自 <c>userlocation</c> 与 <c>location</c> 表。 | |
| 74 | + /// </remarks> | |
| 75 | + /// <param name="input">邮箱、密码;若系统开启验证码则需传 Uuid、Code</param> | |
| 76 | + /// <returns>Token、RefreshToken 与绑定门店</returns> | |
| 77 | + /// <response code="200">登录成功</response> | |
| 78 | + /// <response code="400">参数或验证码错误</response> | |
| 79 | + /// <response code="500">服务器错误</response> | |
| 80 | + [AllowAnonymous] | |
| 81 | + public virtual async Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input) | |
| 82 | + { | |
| 83 | + if (string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email)) | |
| 84 | + { | |
| 85 | + throw new UserFriendlyException("请输入合理数据!"); | |
| 86 | + } | |
| 87 | + | |
| 88 | + ValidationImageCaptcha(input.Uuid, input.Code); | |
| 89 | + | |
| 90 | + var user = await FindActiveUserByEmailAsync(input.Email.Trim()); | |
| 91 | + if (user is null) | |
| 92 | + { | |
| 93 | + throw new UserFriendlyException("登录失败!邮箱不存在!"); | |
| 94 | + } | |
| 95 | + | |
| 96 | + if (user.EncryPassword.Password != MD5Helper.SHA2Encode(input.Password, user.EncryPassword.Salt)) | |
| 97 | + { | |
| 98 | + throw new UserFriendlyException(UserConst.Login_Error); | |
| 99 | + } | |
| 100 | + | |
| 101 | + // App 端不依赖 RBAC 权限体系:允许“无权限账号”登录拿 Token(H5 再做权限控制) | |
| 102 | + var accessToken = CreateAppAccessToken(user); | |
| 103 | + var refreshToken = _accountManager.CreateRefreshToken(user.Id); | |
| 104 | + | |
| 105 | + if (_httpContextAccessor.HttpContext is not null) | |
| 106 | + { | |
| 107 | + var loginEntity = new LoginLogAggregateRoot().GetInfoByHttpContext(_httpContextAccessor.HttpContext); | |
| 108 | + var loginEto = loginEntity.Adapt<LoginEventArgs>(); | |
| 109 | + loginEto.UserName = user.UserName; | |
| 110 | + loginEto.UserId = user.Id; | |
| 111 | + await LocalEventBus.PublishAsync(loginEto); | |
| 112 | + } | |
| 113 | + | |
| 114 | + var locations = await LoadBoundLocationsAsync(user.Id); | |
| 115 | + | |
| 116 | + return new UsAppLoginOutputDto | |
| 117 | + { | |
| 118 | + Token = accessToken, | |
| 119 | + RefreshToken = refreshToken, | |
| 120 | + Locations = locations | |
| 121 | + }; | |
| 122 | + } | |
| 123 | + | |
| 124 | + /// <summary> | |
| 125 | + /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) | |
| 126 | + /// </summary> | |
| 127 | + [Authorize] | |
| 128 | + public virtual async Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync() | |
| 129 | + { | |
| 130 | + if (!CurrentUser.Id.HasValue) | |
| 131 | + { | |
| 132 | + throw new UserFriendlyException("用户未登录"); | |
| 133 | + } | |
| 134 | + | |
| 135 | + return await LoadBoundLocationsAsync(CurrentUser.Id.Value); | |
| 136 | + } | |
| 137 | + | |
| 138 | + private void ValidationImageCaptcha(string? uuid, string? code) | |
| 139 | + { | |
| 140 | + if (!_rbacOptions.EnableCaptcha) | |
| 141 | + { | |
| 142 | + return; | |
| 143 | + } | |
| 144 | + | |
| 145 | + if (!_captcha.Validate(uuid, code)) | |
| 146 | + { | |
| 147 | + throw new UserFriendlyException("验证码错误"); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + | |
| 151 | + /// <summary> | |
| 152 | + /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写) | |
| 153 | + /// </summary> | |
| 154 | + private async Task<UserAggregateRoot?> FindActiveUserByEmailAsync(string email) | |
| 155 | + { | |
| 156 | + var normalized = email.Trim().ToLowerInvariant(); | |
| 157 | + var users = await _userRepository._DbQueryable | |
| 158 | + .Where(u => !u.IsDeleted && u.State == true) | |
| 159 | + .Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized) | |
| 160 | + .ToListAsync(); | |
| 161 | + return users.FirstOrDefault(); | |
| 162 | + } | |
| 163 | + | |
| 164 | + private string CreateAppAccessToken(UserAggregateRoot user) | |
| 165 | + { | |
| 166 | + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey)); | |
| 167 | + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); | |
| 168 | + | |
| 169 | + var claims = new List<Claim> | |
| 170 | + { | |
| 171 | + new(AbpClaimTypes.UserId, user.Id.ToString()), | |
| 172 | + new(AbpClaimTypes.UserName, user.UserName) | |
| 173 | + }; | |
| 174 | + | |
| 175 | + if (!string.IsNullOrWhiteSpace(user.Email)) | |
| 176 | + { | |
| 177 | + claims.Add(new Claim(AbpClaimTypes.Email, user.Email)); | |
| 178 | + } | |
| 179 | + | |
| 180 | + var token = new JwtSecurityToken( | |
| 181 | + issuer: _jwtOptions.Issuer, | |
| 182 | + audience: _jwtOptions.Audience, | |
| 183 | + claims: claims, | |
| 184 | + expires: DateTime.Now.AddMinutes(_jwtOptions.ExpiresMinuteTime), | |
| 185 | + notBefore: DateTime.Now, | |
| 186 | + signingCredentials: creds); | |
| 187 | + | |
| 188 | + return new JwtSecurityTokenHandler().WriteToken(token); | |
| 189 | + } | |
| 190 | + | |
| 191 | + private async Task<List<UsAppBoundLocationDto>> LoadBoundLocationsAsync(Guid userId) | |
| 192 | + { | |
| 193 | + var userIdStr = userId.ToString(); | |
| 194 | + var links = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 195 | + .Where(x => !x.IsDeleted && x.UserId == userIdStr) | |
| 196 | + .Select(x => x.LocationId) | |
| 197 | + .ToListAsync(); | |
| 198 | + | |
| 199 | + if (links.Count == 0) | |
| 200 | + { | |
| 201 | + return new List<UsAppBoundLocationDto>(); | |
| 202 | + } | |
| 203 | + | |
| 204 | + var wanted = links.Distinct().ToList(); | |
| 205 | + var locations = (await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 206 | + .Where(x => !x.IsDeleted) | |
| 207 | + .Where(x => wanted.Contains(x.Id.ToString())) | |
| 208 | + .ToListAsync()) | |
| 209 | + .OrderBy(x => x.OrderNum) | |
| 210 | + .ThenBy(x => x.LocationName) | |
| 211 | + .ToList(); | |
| 212 | + | |
| 213 | + return locations.Select(x => new UsAppBoundLocationDto | |
| 214 | + { | |
| 215 | + Id = x.Id.ToString(), | |
| 216 | + LocationCode = x.LocationCode ?? string.Empty, | |
| 217 | + LocationName = x.LocationName ?? string.Empty, | |
| 218 | + FullAddress = BuildFullAddress(x), | |
| 219 | + State = x.State | |
| 220 | + }).ToList(); | |
| 221 | + } | |
| 222 | + | |
| 223 | + private static string BuildFullAddress(LocationAggregateRoot loc) | |
| 224 | + { | |
| 225 | + var street = loc.Street?.Trim(); | |
| 226 | + var city = loc.City?.Trim(); | |
| 227 | + var state = loc.StateCode?.Trim(); | |
| 228 | + var zip = loc.ZipCode?.Trim(); | |
| 229 | + | |
| 230 | + var line2Parts = new List<string>(); | |
| 231 | + if (!string.IsNullOrEmpty(city)) | |
| 232 | + { | |
| 233 | + line2Parts.Add(city); | |
| 234 | + } | |
| 235 | + | |
| 236 | + if (!string.IsNullOrEmpty(state)) | |
| 237 | + { | |
| 238 | + line2Parts.Add(state); | |
| 239 | + } | |
| 240 | + | |
| 241 | + var line2 = line2Parts.Count > 0 ? string.Join(", ", line2Parts) : string.Empty; | |
| 242 | + if (!string.IsNullOrEmpty(zip)) | |
| 243 | + { | |
| 244 | + line2 = string.IsNullOrEmpty(line2) ? zip : $"{line2} {zip}"; | |
| 245 | + } | |
| 246 | + | |
| 247 | + var segments = new List<string>(); | |
| 248 | + if (!string.IsNullOrEmpty(street)) | |
| 249 | + { | |
| 250 | + segments.Add(street); | |
| 251 | + } | |
| 252 | + | |
| 253 | + if (!string.IsNullOrEmpty(line2)) | |
| 254 | + { | |
| 255 | + segments.Add(line2); | |
| 256 | + } | |
| 257 | + | |
| 258 | + return segments.Count == 0 ? "无" : string.Join(", ", segments); | |
| 259 | + } | |
| 260 | +} | ... | ... |
项目相关文档/美国版App登录接口说明.md
0 → 100644
| 1 | +# 美国版 App 登录接口说明 | |
| 2 | + | |
| 3 | +## 概述 | |
| 4 | + | |
| 5 | +美国版移动端认证由 `food-labeling-us` 模块的 **`UsAppAuthAppService`** 提供,采用 ABP 约定式动态 API。宿主统一前缀为 **`/api/app`**,建议以 Swagger 为准核对路径(本地示例:`http://localhost:19001/swagger`,搜索 `UsAppAuth`)。 | |
| 6 | + | |
| 7 | +| 说明 | 内容 | | |
| 8 | +|------|------| | |
| 9 | +| 账号标识 | 使用 **`User.Email`**(邮箱)登录,邮箱比对**忽略大小写** | | |
| 10 | +| 密码 | 与 Web 共用 `User` 表,校验方式与 RBAC **`AccountManager`** 一致(盐值 + `MD5Helper.SHA2Encode`) | | |
| 11 | +| 权限 | **App 登录不校验角色/权限**(允许“未配置权限”的账号登录);H5 管理端再按 RBAC 权限控制 | | |
| 12 | +| 验证码 | 当配置 **`Rbac:EnableCaptcha`** 为 `true` 时,需先拉取图形验证码,本接口入参传 `uuid`、`code`;未开启时可传空或不传 | | |
| 13 | + | |
| 14 | +--- | |
| 15 | + | |
| 16 | +## 接口 1:App 登录 | |
| 17 | + | |
| 18 | +签发 **Access Token**、**Refresh Token**,并返回当前用户在 **`userlocation`** 中绑定的门店列表(关联 **`location`** 表详情)。 | |
| 19 | + | |
| 20 | +### HTTP | |
| 21 | + | |
| 22 | +- **方法**:`POST` | |
| 23 | +- **路径**:`/api/app/us-app-auth/login` | |
| 24 | +- **Content-Type**:`application/json` | |
| 25 | +- **鉴权**:无需登录(匿名) | |
| 26 | + | |
| 27 | +### 请求体参数(UsAppLoginInputVo) | |
| 28 | + | |
| 29 | +| 参数名(JSON) | 类型 | 必填 | 说明 | | |
| 30 | +|----------------|------|------|------| | |
| 31 | +| `email` | string | 是 | 登录邮箱,对应数据库 `User.Email` | | |
| 32 | +| `password` | string | 是 | 明文密码 | | |
| 33 | +| `uuid` | string | 条件 | 图形验证码 UUID;**开启验证码时必填** | | |
| 34 | +| `code` | string | 条件 | 图形验证码;**开启验证码时必填** | | |
| 35 | + | |
| 36 | +### 传参示例(请求 Body) | |
| 37 | + | |
| 38 | +未开启图形验证码时: | |
| 39 | + | |
| 40 | +```json | |
| 41 | +{ | |
| 42 | + "email": "admin@example.com", | |
| 43 | + "password": "123456" | |
| 44 | +} | |
| 45 | +``` | |
| 46 | + | |
| 47 | +开启图形验证码时(需与系统验证码接口返回的 `uuid`、用户输入的验证码一致): | |
| 48 | + | |
| 49 | +```json | |
| 50 | +{ | |
| 51 | + "email": "test@example.com", | |
| 52 | + "password": "您的密码", | |
| 53 | + "uuid": "验证码接口返回的 uuid", | |
| 54 | + "code": "用户看到的验证码" | |
| 55 | +} | |
| 56 | +``` | |
| 57 | + | |
| 58 | +### 响应体(UsAppLoginOutputDto) | |
| 59 | + | |
| 60 | +| 字段(JSON) | 类型 | 说明 | | |
| 61 | +|--------------|------|------| | |
| 62 | +| `token` | string | 访问令牌(Bearer),后续业务接口放在 Header `Authorization: Bearer {token}` | | |
| 63 | +| `refreshToken` | string | 刷新令牌(与系统账号体系一致,用于刷新 access token,具体用法与 Web 一致) | | |
| 64 | +| `locations` | array | 绑定门店列表,元素见下表 | | |
| 65 | + | |
| 66 | +#### `locations[]` 元素(UsAppBoundLocationDto) | |
| 67 | + | |
| 68 | +| 字段(JSON) | 类型 | 说明 | | |
| 69 | +|--------------|------|------| | |
| 70 | +| `id` | string | 门店主键(Guid 字符串) | | |
| 71 | +| `locationCode` | string | 业务编码,如 LOC-1 | | |
| 72 | +| `locationName` | string | 门店名称 | | |
| 73 | +| `fullAddress` | string | 拼接后的完整地址(街道、城市、州、邮编等;无数据时可能为 `"无"`) | | |
| 74 | +| `state` | bool | 门店是否启用 | | |
| 75 | + | |
| 76 | +### 响应示例 | |
| 77 | + | |
| 78 | +```json | |
| 79 | +{ | |
| 80 | + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | |
| 81 | + "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | |
| 82 | + "locations": [ | |
| 83 | + { | |
| 84 | + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | |
| 85 | + "locationCode": "LOC-1", | |
| 86 | + "locationName": "Downtown Kitchen", | |
| 87 | + "fullAddress": "123 Main St, New York, NY 10001", | |
| 88 | + "state": true | |
| 89 | + } | |
| 90 | + ] | |
| 91 | +} | |
| 92 | +``` | |
| 93 | + | |
| 94 | +### 常见错误提示(业务异常文案) | |
| 95 | + | |
| 96 | +- 邮箱或密码为空:`请输入合理数据!` | |
| 97 | +- 邮箱在库中不存在(未删除且启用用户中无匹配邮箱):`登录失败!邮箱不存在!` | |
| 98 | +- 密码错误:`登录失败!用户名或密码错误!`(与 `UserConst.Login_Error` 一致) | |
| 99 | +- 验证码错误(开启验证码时):`验证码错误` | |
| 100 | + | |
| 101 | +--- | |
| 102 | + | |
| 103 | +## 接口 2:获取当前账号绑定门店 | |
| 104 | + | |
| 105 | +无需重新登录即可刷新 **`userlocation`** 绑定门店列表(例如切换门店前先同步列表)。 | |
| 106 | + | |
| 107 | +### HTTP | |
| 108 | + | |
| 109 | +- **方法**:`GET` | |
| 110 | +- **路径**:`/api/app/us-app-auth/my-locations` | |
| 111 | +- **鉴权**:需要登录,请求头携带 **`Authorization: Bearer {token}`**(使用接口 1 返回的 `token`) | |
| 112 | + | |
| 113 | +### 请求参数 | |
| 114 | + | |
| 115 | +无 Query / Body 参数;用户身份由 JWT 解析。 | |
| 116 | + | |
| 117 | +### 传参示例 | |
| 118 | + | |
| 119 | +```http | |
| 120 | +GET /api/app/us-app-auth/my-locations HTTP/1.1 | |
| 121 | +Host: localhost:19001 | |
| 122 | +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | |
| 123 | +``` | |
| 124 | + | |
| 125 | +若前端统一约定 GET 使用 `data` 封装,可自行在客户端组装;本接口服务端**不读取额外 Query 参数**。 | |
| 126 | + | |
| 127 | +### 响应体 | |
| 128 | + | |
| 129 | +与登录接口中 **`locations`** 相同:**`UsAppBoundLocationDto[]`**(数组)。 | |
| 130 | + | |
| 131 | +### 响应示例 | |
| 132 | + | |
| 133 | +```json | |
| 134 | +[ | |
| 135 | + { | |
| 136 | + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f", | |
| 137 | + "locationCode": "LOC-1", | |
| 138 | + "locationName": "Downtown Kitchen", | |
| 139 | + "fullAddress": "123 Main St, New York, NY 10001", | |
| 140 | + "state": true | |
| 141 | + } | |
| 142 | +] | |
| 143 | +``` | |
| 144 | + | |
| 145 | +### 常见错误 | |
| 146 | + | |
| 147 | +- 未登录或 Token 无效:按网关/ABP 返回 401 及统一错误体 | |
| 148 | +- 无用户上下文:`用户未登录` | |
| 149 | + | |
| 150 | +--- | |
| 151 | + | |
| 152 | +## 与其他登录方式的区别 | |
| 153 | + | |
| 154 | +| 场景 | 说明 | | |
| 155 | +|------|------| | |
| 156 | +| Web 管理端 | 仍使用 RBAC **`AccountService.PostLoginAsync`**,一般为人 **`userName`** + 密码 | | |
| 157 | +| 美国版 App | **仅**本模块 **`/api/app/us-app-auth/login`** 使用 **邮箱 + 密码** | | |
| 158 | + | |
| 159 | +两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。 | ... | ... |