diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppBoundLocationDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppBoundLocationDto.cs
new file mode 100644
index 0000000..7ae9a4f
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppBoundLocationDto.cs
@@ -0,0 +1,22 @@
+namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
+
+///
+/// App 端展示的绑定门店信息
+///
+public class UsAppBoundLocationDto
+{
+ /// 门店主键 Id(Guid 字符串)
+ public string Id { get; set; } = string.Empty;
+
+ /// 业务编码,如 LOC-1
+ public string LocationCode { get; set; } = string.Empty;
+
+ /// 门店名称
+ public string LocationName { get; set; } = string.Empty;
+
+ /// 拼接后的完整地址,便于移动端展示
+ public string FullAddress { get; set; } = string.Empty;
+
+ /// 门店是否启用
+ public bool State { get; set; }
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginInputVo.cs
new file mode 100644
index 0000000..b04588c
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginInputVo.cs
@@ -0,0 +1,18 @@
+namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
+
+///
+/// 美国版 App 登录入参(使用邮箱作为账号,支持图形验证码)
+///
+public class UsAppLoginInputVo
+{
+ /// 登录邮箱(对应 user.Email)
+ public string Email { get; set; } = string.Empty;
+
+ public string Password { get; set; } = string.Empty;
+
+ /// 图形验证码 UUID(开启验证码时必填)
+ public string? Uuid { get; set; }
+
+ /// 图形验证码(开启验证码时必填)
+ public string? Code { get; set; }
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginOutputDto.cs
new file mode 100644
index 0000000..302bded
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLoginOutputDto.cs
@@ -0,0 +1,14 @@
+namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
+
+///
+/// 美国版 App 登录返回(含 Token 与绑定门店)
+///
+public class UsAppLoginOutputDto
+{
+ public string Token { get; set; } = string.Empty;
+
+ public string RefreshToken { get; set; } = string.Empty;
+
+ /// 当前账号在 userlocation 中绑定的门店列表
+ public List Locations { get; set; } = new();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
new file mode 100644
index 0000000..1b439e0
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
@@ -0,0 +1,20 @@
+using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
+using Volo.Abp.Application.Services;
+
+namespace FoodLabeling.Application.Contracts.IServices;
+
+///
+/// 美国版移动端认证(登录返回绑定门店)
+///
+public interface IUsAppAuthAppService : IApplicationService
+{
+ ///
+ /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店
+ ///
+ Task LoginAsync(UsAppLoginInputVo input);
+
+ ///
+ /// 获取当前登录账号已绑定的门店(用于切换门店等场景)
+ ///
+ Task> GetMyLocationsAsync();
+}
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
index 5133544..9c45fa3 100644
--- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
@@ -2,6 +2,10 @@
+
+
+
+
diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
new file mode 100644
index 0000000..93cd84c
--- /dev/null
+++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
+using FoodLabeling.Application.Contracts.IServices;
+using FoodLabeling.Application.Services.DbModels;
+using FoodLabeling.Domain.Entities;
+using Lazy.Captcha.Core;
+using Mapster;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using SqlSugar;
+using Volo.Abp;
+using Volo.Abp.Application.Services;
+using Volo.Abp.EventBus.Local;
+using Volo.Abp.Security.Claims;
+using Volo.Abp.Users;
+using Yi.Framework.Core.Helper;
+using Yi.Framework.Rbac.Domain.Entities;
+using Yi.Framework.Rbac.Domain.Managers;
+using Yi.Framework.Rbac.Domain.Shared.Consts;
+using Yi.Framework.Rbac.Domain.Shared.Dtos;
+using Yi.Framework.Rbac.Domain.Shared.Etos;
+using Yi.Framework.Rbac.Domain.Shared.Options;
+using Yi.Framework.SqlSugarCore.Abstractions;
+
+namespace FoodLabeling.Application.Services;
+
+///
+/// 美国版 App 登录:邮箱 + 密码(与 AccountManager 相同盐值哈希)签发 JWT,并返回 userlocation 绑定门店
+///
+public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
+{
+ private readonly IAccountManager _accountManager;
+ private readonly ISqlSugarRepository _userRepository;
+ private readonly ISqlSugarDbContext _dbContext;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly ICaptcha _captcha;
+ private readonly RbacOptions _rbacOptions;
+ private readonly JwtOptions _jwtOptions;
+
+ public UsAppAuthAppService(
+ IAccountManager accountManager,
+ ISqlSugarRepository userRepository,
+ ISqlSugarDbContext dbContext,
+ IHttpContextAccessor httpContextAccessor,
+ ICaptcha captcha,
+ IOptions jwtOptions,
+ IOptions rbacOptions)
+ {
+ _accountManager = accountManager;
+ _userRepository = userRepository;
+ _dbContext = dbContext;
+ _httpContextAccessor = httpContextAccessor;
+ _captcha = captcha;
+ _jwtOptions = jwtOptions.Value;
+ _rbacOptions = rbacOptions.Value;
+ }
+
+ protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService();
+
+ ///
+ /// App 登录:签发 Token / RefreshToken,并返回当前账号绑定的门店列表
+ ///
+ ///
+ /// 行为与系统 AccountService.PostLoginAsync 一致(含验证码、登录日志事件)。
+ /// 门店数据来自 userlocation 与 location 表。
+ ///
+ /// 邮箱、密码;若系统开启验证码则需传 Uuid、Code
+ /// Token、RefreshToken 与绑定门店
+ /// 登录成功
+ /// 参数或验证码错误
+ /// 服务器错误
+ [AllowAnonymous]
+ public virtual async Task LoginAsync(UsAppLoginInputVo input)
+ {
+ if (string.IsNullOrWhiteSpace(input.Password) || string.IsNullOrWhiteSpace(input.Email))
+ {
+ throw new UserFriendlyException("请输入合理数据!");
+ }
+
+ ValidationImageCaptcha(input.Uuid, input.Code);
+
+ var user = await FindActiveUserByEmailAsync(input.Email.Trim());
+ if (user is null)
+ {
+ throw new UserFriendlyException("登录失败!邮箱不存在!");
+ }
+
+ if (user.EncryPassword.Password != MD5Helper.SHA2Encode(input.Password, user.EncryPassword.Salt))
+ {
+ throw new UserFriendlyException(UserConst.Login_Error);
+ }
+
+ // App 端不依赖 RBAC 权限体系:允许“无权限账号”登录拿 Token(H5 再做权限控制)
+ var accessToken = CreateAppAccessToken(user);
+ var refreshToken = _accountManager.CreateRefreshToken(user.Id);
+
+ if (_httpContextAccessor.HttpContext is not null)
+ {
+ var loginEntity = new LoginLogAggregateRoot().GetInfoByHttpContext(_httpContextAccessor.HttpContext);
+ var loginEto = loginEntity.Adapt();
+ loginEto.UserName = user.UserName;
+ loginEto.UserId = user.Id;
+ await LocalEventBus.PublishAsync(loginEto);
+ }
+
+ var locations = await LoadBoundLocationsAsync(user.Id);
+
+ return new UsAppLoginOutputDto
+ {
+ Token = accessToken,
+ RefreshToken = refreshToken,
+ Locations = locations
+ };
+ }
+
+ ///
+ /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取)
+ ///
+ [Authorize]
+ public virtual async Task> GetMyLocationsAsync()
+ {
+ if (!CurrentUser.Id.HasValue)
+ {
+ throw new UserFriendlyException("用户未登录");
+ }
+
+ return await LoadBoundLocationsAsync(CurrentUser.Id.Value);
+ }
+
+ private void ValidationImageCaptcha(string? uuid, string? code)
+ {
+ if (!_rbacOptions.EnableCaptcha)
+ {
+ return;
+ }
+
+ if (!_captcha.Validate(uuid, code))
+ {
+ throw new UserFriendlyException("验证码错误");
+ }
+ }
+
+ ///
+ /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写)
+ ///
+ private async Task FindActiveUserByEmailAsync(string email)
+ {
+ var normalized = email.Trim().ToLowerInvariant();
+ var users = await _userRepository._DbQueryable
+ .Where(u => !u.IsDeleted && u.State == true)
+ .Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized)
+ .ToListAsync();
+ return users.FirstOrDefault();
+ }
+
+ private string CreateAppAccessToken(UserAggregateRoot user)
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey));
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var claims = new List
+ {
+ new(AbpClaimTypes.UserId, user.Id.ToString()),
+ new(AbpClaimTypes.UserName, user.UserName)
+ };
+
+ if (!string.IsNullOrWhiteSpace(user.Email))
+ {
+ claims.Add(new Claim(AbpClaimTypes.Email, user.Email));
+ }
+
+ var token = new JwtSecurityToken(
+ issuer: _jwtOptions.Issuer,
+ audience: _jwtOptions.Audience,
+ claims: claims,
+ expires: DateTime.Now.AddMinutes(_jwtOptions.ExpiresMinuteTime),
+ notBefore: DateTime.Now,
+ signingCredentials: creds);
+
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ private async Task> LoadBoundLocationsAsync(Guid userId)
+ {
+ var userIdStr = userId.ToString();
+ var links = await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted && x.UserId == userIdStr)
+ .Select(x => x.LocationId)
+ .ToListAsync();
+
+ if (links.Count == 0)
+ {
+ return new List();
+ }
+
+ var wanted = links.Distinct().ToList();
+ var locations = (await _dbContext.SqlSugarClient.Queryable()
+ .Where(x => !x.IsDeleted)
+ .Where(x => wanted.Contains(x.Id.ToString()))
+ .ToListAsync())
+ .OrderBy(x => x.OrderNum)
+ .ThenBy(x => x.LocationName)
+ .ToList();
+
+ return locations.Select(x => new UsAppBoundLocationDto
+ {
+ Id = x.Id.ToString(),
+ LocationCode = x.LocationCode ?? string.Empty,
+ LocationName = x.LocationName ?? string.Empty,
+ FullAddress = BuildFullAddress(x),
+ State = x.State
+ }).ToList();
+ }
+
+ private static string BuildFullAddress(LocationAggregateRoot loc)
+ {
+ var street = loc.Street?.Trim();
+ var city = loc.City?.Trim();
+ var state = loc.StateCode?.Trim();
+ var zip = loc.ZipCode?.Trim();
+
+ var line2Parts = new List();
+ if (!string.IsNullOrEmpty(city))
+ {
+ line2Parts.Add(city);
+ }
+
+ if (!string.IsNullOrEmpty(state))
+ {
+ line2Parts.Add(state);
+ }
+
+ var line2 = line2Parts.Count > 0 ? string.Join(", ", line2Parts) : string.Empty;
+ if (!string.IsNullOrEmpty(zip))
+ {
+ line2 = string.IsNullOrEmpty(line2) ? zip : $"{line2} {zip}";
+ }
+
+ var segments = new List();
+ if (!string.IsNullOrEmpty(street))
+ {
+ segments.Add(street);
+ }
+
+ if (!string.IsNullOrEmpty(line2))
+ {
+ segments.Add(line2);
+ }
+
+ return segments.Count == 0 ? "无" : string.Join(", ", segments);
+ }
+}
diff --git a/项目相关文档/美国版App登录接口说明.md b/项目相关文档/美国版App登录接口说明.md
new file mode 100644
index 0000000..fd04601
--- /dev/null
+++ b/项目相关文档/美国版App登录接口说明.md
@@ -0,0 +1,159 @@
+# 美国版 App 登录接口说明
+
+## 概述
+
+美国版移动端认证由 `food-labeling-us` 模块的 **`UsAppAuthAppService`** 提供,采用 ABP 约定式动态 API。宿主统一前缀为 **`/api/app`**,建议以 Swagger 为准核对路径(本地示例:`http://localhost:19001/swagger`,搜索 `UsAppAuth`)。
+
+| 说明 | 内容 |
+|------|------|
+| 账号标识 | 使用 **`User.Email`**(邮箱)登录,邮箱比对**忽略大小写** |
+| 密码 | 与 Web 共用 `User` 表,校验方式与 RBAC **`AccountManager`** 一致(盐值 + `MD5Helper.SHA2Encode`) |
+| 权限 | **App 登录不校验角色/权限**(允许“未配置权限”的账号登录);H5 管理端再按 RBAC 权限控制 |
+| 验证码 | 当配置 **`Rbac:EnableCaptcha`** 为 `true` 时,需先拉取图形验证码,本接口入参传 `uuid`、`code`;未开启时可传空或不传 |
+
+---
+
+## 接口 1:App 登录
+
+签发 **Access Token**、**Refresh Token**,并返回当前用户在 **`userlocation`** 中绑定的门店列表(关联 **`location`** 表详情)。
+
+### HTTP
+
+- **方法**:`POST`
+- **路径**:`/api/app/us-app-auth/login`
+- **Content-Type**:`application/json`
+- **鉴权**:无需登录(匿名)
+
+### 请求体参数(UsAppLoginInputVo)
+
+| 参数名(JSON) | 类型 | 必填 | 说明 |
+|----------------|------|------|------|
+| `email` | string | 是 | 登录邮箱,对应数据库 `User.Email` |
+| `password` | string | 是 | 明文密码 |
+| `uuid` | string | 条件 | 图形验证码 UUID;**开启验证码时必填** |
+| `code` | string | 条件 | 图形验证码;**开启验证码时必填** |
+
+### 传参示例(请求 Body)
+
+未开启图形验证码时:
+
+```json
+{
+ "email": "admin@example.com",
+ "password": "123456"
+}
+```
+
+开启图形验证码时(需与系统验证码接口返回的 `uuid`、用户输入的验证码一致):
+
+```json
+{
+ "email": "test@example.com",
+ "password": "您的密码",
+ "uuid": "验证码接口返回的 uuid",
+ "code": "用户看到的验证码"
+}
+```
+
+### 响应体(UsAppLoginOutputDto)
+
+| 字段(JSON) | 类型 | 说明 |
+|--------------|------|------|
+| `token` | string | 访问令牌(Bearer),后续业务接口放在 Header `Authorization: Bearer {token}` |
+| `refreshToken` | string | 刷新令牌(与系统账号体系一致,用于刷新 access token,具体用法与 Web 一致) |
+| `locations` | array | 绑定门店列表,元素见下表 |
+
+#### `locations[]` 元素(UsAppBoundLocationDto)
+
+| 字段(JSON) | 类型 | 说明 |
+|--------------|------|------|
+| `id` | string | 门店主键(Guid 字符串) |
+| `locationCode` | string | 业务编码,如 LOC-1 |
+| `locationName` | string | 门店名称 |
+| `fullAddress` | string | 拼接后的完整地址(街道、城市、州、邮编等;无数据时可能为 `"无"`) |
+| `state` | bool | 门店是否启用 |
+
+### 响应示例
+
+```json
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "locations": [
+ {
+ "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
+ "locationCode": "LOC-1",
+ "locationName": "Downtown Kitchen",
+ "fullAddress": "123 Main St, New York, NY 10001",
+ "state": true
+ }
+ ]
+}
+```
+
+### 常见错误提示(业务异常文案)
+
+- 邮箱或密码为空:`请输入合理数据!`
+- 邮箱在库中不存在(未删除且启用用户中无匹配邮箱):`登录失败!邮箱不存在!`
+- 密码错误:`登录失败!用户名或密码错误!`(与 `UserConst.Login_Error` 一致)
+- 验证码错误(开启验证码时):`验证码错误`
+
+---
+
+## 接口 2:获取当前账号绑定门店
+
+无需重新登录即可刷新 **`userlocation`** 绑定门店列表(例如切换门店前先同步列表)。
+
+### HTTP
+
+- **方法**:`GET`
+- **路径**:`/api/app/us-app-auth/my-locations`
+- **鉴权**:需要登录,请求头携带 **`Authorization: Bearer {token}`**(使用接口 1 返回的 `token`)
+
+### 请求参数
+
+无 Query / Body 参数;用户身份由 JWT 解析。
+
+### 传参示例
+
+```http
+GET /api/app/us-app-auth/my-locations HTTP/1.1
+Host: localhost:19001
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+```
+
+若前端统一约定 GET 使用 `data` 封装,可自行在客户端组装;本接口服务端**不读取额外 Query 参数**。
+
+### 响应体
+
+与登录接口中 **`locations`** 相同:**`UsAppBoundLocationDto[]`**(数组)。
+
+### 响应示例
+
+```json
+[
+ {
+ "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
+ "locationCode": "LOC-1",
+ "locationName": "Downtown Kitchen",
+ "fullAddress": "123 Main St, New York, NY 10001",
+ "state": true
+ }
+]
+```
+
+### 常见错误
+
+- 未登录或 Token 无效:按网关/ABP 返回 401 及统一错误体
+- 无用户上下文:`用户未登录`
+
+---
+
+## 与其他登录方式的区别
+
+| 场景 | 说明 |
+|------|------|
+| Web 管理端 | 仍使用 RBAC **`AccountService.PostLoginAsync`**,一般为人 **`userName`** + 密码 |
+| 美国版 App | **仅**本模块 **`/api/app/us-app-auth/login`** 使用 **邮箱 + 密码** |
+
+两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。