diff --git a/src/Oms.Application/Dtos/Orders/OrderReceiverParseAddressDtos.cs b/src/Oms.Application/Dtos/Orders/OrderReceiverParseAddressDtos.cs
new file mode 100644
index 0000000..58113ef
--- /dev/null
+++ b/src/Oms.Application/Dtos/Orders/OrderReceiverParseAddressDtos.cs
@@ -0,0 +1,38 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Oms.Application.Dtos.Orders;
+
+/// 粘贴整段地址 → AI 拆分为收件人字段。
+public class OrderReceiverParseAddressInput
+{
+ /// 客户粘贴的原始地址文本(姓名/电话/国家/省市区/街道/邮编等)。
+ [Required]
+ [MaxLength(4000)]
+ public string RawText { get; set; } = string.Empty;
+
+ /// 可选;为空时取客户/平台默认已启用 AI 提供商。
+ [MaxLength(64)]
+ public string? ProviderKey { get; set; }
+
+ /// 可选模型覆盖。
+ [MaxLength(128)]
+ public string? Model { get; set; }
+}
+
+/// 地址解析结果 + 完整性校验。
+public class OrderReceiverParseAddressOutput
+{
+ public OrderParcelReceiverInput Receiver { get; set; } = new();
+
+ /// 必填项是否齐全(姓名/手机/国家/邮编/地址1)。
+ public bool IsComplete { get; set; }
+
+ /// 缺失的必填字段中文名。
+ public List MissingRequired { get; set; } = [];
+
+ /// 建议补全的字段(省/州、城市等)。
+ public List MissingRecommended { get; set; } = [];
+
+ /// 模型或规则给出的提示(歧义、格式可疑等)。
+ public List Warnings { get; set; } = [];
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs b/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs
new file mode 100644
index 0000000..8bf89d1
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/OrderService.ReceiverAddressParse.cs
@@ -0,0 +1,299 @@
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using Fake.Application;
+using Microsoft.AspNetCore.Mvc;
+using Oms.Application.Dtos.Orders;
+using Oms.Application.Shared;
+using Oms.Domain.XhsAggregate.Entities;
+using Oms.Domain.XhsAggregate.Repositories;
+
+namespace Oms.Application.Services.Orders;
+
+public partial class OrderService
+{
+ private static readonly string[] ReceiverRequiredFieldLabels =
+ [
+ "收件人名称",
+ "收件人手机",
+ "国家",
+ "邮编",
+ "地址1",
+ ];
+
+ private IXhsAiProviderRepository AiProviderRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ private XhsLlmClient LlmClient => LazyServiceProvider.GetRequiredService();
+
+ ///
+ /// AI 解析粘贴的收件地址文本,并校验是否完整。
+ ///
+ [HttpPost("/order/receiver/parse-address")]
+ public async Task ParseReceiverAddressAsync(
+ [FromBody] OrderReceiverParseAddressInput input,
+ CancellationToken cancellationToken = default)
+ {
+ var raw = (input.RawText ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ throw new BusinessException("请先粘贴收件地址文本");
+ }
+
+ if (raw.Length > 4000)
+ {
+ throw new BusinessException("地址文本过长,请精简后再解析");
+ }
+
+ var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
+ var provider = await ResolveAiProviderAsync(customerId, input.ProviderKey);
+
+ var systemPrompt =
+ """
+ 你是国际物流 OMS 的收件地址解析助手(跨境打单场景)。
+ 目标:把客户粘贴的整段中/英收件信息拆成结构化字段,并做「粗校验」提示——只关心下单/预报大概能不能用,不做地图精校、不验证门牌是否真实、不臆造邮编与城市匹配。
+
+ 只输出 JSON,不要 Markdown,不要解释。字段:
+ {
+ "recipientsName": "",
+ "companyName": "",
+ "phone": "",
+ "telePhone": "",
+ "email": "",
+ "countryCode": "",
+ "province": "",
+ "city": "",
+ "streetNumber": "",
+ "streetAddress1": "",
+ "streetAddress2": "",
+ "streetAddress3": "",
+ "zipCode": "",
+ "warnings": []
+ }
+
+ 规则:
+ 1) countryCode 必须是 ISO 二字码大写(中国→CN,美国→US,英国→GB 不要用 UK,香港→HK,台湾→TW,澳门→MO)。无法判断则留空并在 warnings 说明。
+ 2) phone 优先填手机;固定电话放 telePhone;保留 + 国家码。
+ 3) 街道门牌放入 streetAddress1;过长拆到 streetAddress2/3;单独门牌号可放 streetNumber。
+ 4) 中国地址:省→province,市→city,区县街道合并进 streetAddress1(不要把省市区塞进姓名)。
+ 5) 英文地址:State→province,City→city,Street→streetAddress*,ZIP/Postcode→zipCode。
+ 6) 粗风险才写 warnings(短中文):缺国家/邮编/电话不像号码/街道与姓名粘连拆不清等。禁止写「地图不存在」「邮编与城市不匹配」这类无依据结论。
+ 7) 不确定的字段留空;不要编造原文没有的省市区邮编。
+ 8) 税号/EORI/VAT 不是本任务重点,原文没有就忽略。
+ """;
+
+ var userPrompt = "请解析以下收件信息:\n" + raw;
+ string modelText;
+ try
+ {
+ modelText = await LlmClient.ChatCompletionAsync(
+ provider,
+ [("system", systemPrompt), ("user", userPrompt)],
+ string.IsNullOrWhiteSpace(input.Model) ? null : input.Model.Trim(),
+ cancellationToken);
+ }
+ catch (BusinessException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new BusinessException($"AI 地址解析失败:{ex.Message}");
+ }
+
+ var receiver = ParseReceiverJson(modelText, out var modelWarnings);
+ NormalizeReceiver(receiver);
+ var (isComplete, missingRequired, missingRecommended) = EvaluateCompleteness(receiver);
+
+ var warnings = new List();
+ warnings.AddRange(modelWarnings);
+ if (!string.IsNullOrWhiteSpace(receiver.CountryCode) &&
+ receiver.CountryCode!.Length != 2)
+ {
+ warnings.Add("国家代码格式异常,请手动确认");
+ }
+
+ return new OrderReceiverParseAddressOutput
+ {
+ Receiver = receiver,
+ IsComplete = isComplete,
+ MissingRequired = missingRequired,
+ MissingRecommended = missingRecommended,
+ Warnings = warnings.Distinct(StringComparer.Ordinal).ToList(),
+ };
+ }
+
+ private async Task ResolveAiProviderAsync(int customerId, string? providerKey)
+ {
+ var key = (providerKey ?? string.Empty).Trim();
+ if (!string.IsNullOrWhiteSpace(key))
+ {
+ var byKey = await AiProviderRepository.GetByProviderKeyAsync(customerId, key);
+ if (byKey is not { Enabled: true })
+ {
+ byKey = await AiProviderRepository.GetByProviderKeyAsync(0, key);
+ }
+
+ if (byKey is not { Enabled: true })
+ {
+ throw new BusinessException($"AI 提供商 {key} 未配置或未启用");
+ }
+
+ return byKey;
+ }
+
+ var list = await AiProviderRepository.GetListAsync(x =>
+ (x.CustomerId == customerId || x.CustomerId == 0) && x.Enabled);
+ var preferred = list
+ .OrderByDescending(x => x.CustomerId == customerId)
+ .ThenByDescending(x => x.IsDefault)
+ .ThenBy(x => x.Id)
+ .FirstOrDefault();
+
+ if (preferred == null)
+ {
+ throw new BusinessException("未配置可用的 AI 提供商,请先在系统中启用 AI 服务");
+ }
+
+ return preferred;
+ }
+
+ private static OrderParcelReceiverInput ParseReceiverJson(string modelText, out List warnings)
+ {
+ warnings = [];
+ var json = ExtractJsonObject(modelText);
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ throw new BusinessException("AI 未返回可识别的地址结构,请调整粘贴内容后重试");
+ }
+
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ throw new BusinessException("AI 返回格式异常,请重试");
+ }
+
+ if (root.TryGetProperty("warnings", out var warnNode) && warnNode.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var item in warnNode.EnumerateArray())
+ {
+ var w = item.GetString()?.Trim();
+ if (!string.IsNullOrWhiteSpace(w))
+ {
+ warnings.Add(w);
+ }
+ }
+ }
+
+ return new OrderParcelReceiverInput
+ {
+ RecipientsName = ReadStr(root, "recipientsName"),
+ CompanyName = ReadStr(root, "companyName"),
+ Phone = ReadStr(root, "phone"),
+ TelePhone = ReadStr(root, "telePhone"),
+ Email = ReadStr(root, "email"),
+ CountryCode = ReadStr(root, "countryCode"),
+ Province = ReadStr(root, "province"),
+ City = ReadStr(root, "city"),
+ StreetNumber = ReadStr(root, "streetNumber"),
+ StreetAddress1 = ReadStr(root, "streetAddress1"),
+ StreetAddress2 = ReadStr(root, "streetAddress2"),
+ StreetAddress3 = ReadStr(root, "streetAddress3"),
+ ZipCode = ReadStr(root, "zipCode"),
+ };
+ }
+
+ private static void NormalizeReceiver(OrderParcelReceiverInput receiver)
+ {
+ receiver.RecipientsName = CleanText(receiver.RecipientsName);
+ receiver.CompanyName = CleanText(receiver.CompanyName);
+ receiver.Phone = CleanText(receiver.Phone);
+ receiver.TelePhone = CleanText(receiver.TelePhone);
+ receiver.Email = CleanText(receiver.Email);
+ receiver.Province = CleanText(receiver.Province);
+ receiver.City = CleanText(receiver.City);
+ receiver.StreetNumber = CleanText(receiver.StreetNumber);
+ receiver.StreetAddress1 = CleanText(receiver.StreetAddress1);
+ receiver.StreetAddress2 = CleanText(receiver.StreetAddress2);
+ receiver.StreetAddress3 = CleanText(receiver.StreetAddress3);
+ receiver.ZipCode = CleanText(receiver.ZipCode);
+
+ var cc = CleanText(receiver.CountryCode);
+ if (!string.IsNullOrWhiteSpace(cc))
+ {
+ receiver.CountryCode = cc.ToUpperInvariant();
+ }
+ else
+ {
+ receiver.CountryCode = null;
+ }
+ }
+
+ private static (bool IsComplete, List MissingRequired, List MissingRecommended)
+ EvaluateCompleteness(OrderParcelReceiverInput receiver)
+ {
+ var missingRequired = new List();
+ if (string.IsNullOrWhiteSpace(receiver.RecipientsName)) missingRequired.Add(ReceiverRequiredFieldLabels[0]);
+ if (string.IsNullOrWhiteSpace(receiver.Phone)) missingRequired.Add(ReceiverRequiredFieldLabels[1]);
+ if (string.IsNullOrWhiteSpace(receiver.CountryCode)) missingRequired.Add(ReceiverRequiredFieldLabels[2]);
+ if (string.IsNullOrWhiteSpace(receiver.ZipCode)) missingRequired.Add(ReceiverRequiredFieldLabels[3]);
+ if (string.IsNullOrWhiteSpace(receiver.StreetAddress1)) missingRequired.Add(ReceiverRequiredFieldLabels[4]);
+
+ var missingRecommended = new List();
+ if (string.IsNullOrWhiteSpace(receiver.Province)) missingRecommended.Add("省/州");
+ if (string.IsNullOrWhiteSpace(receiver.City)) missingRecommended.Add("城市");
+
+ return (missingRequired.Count == 0, missingRequired, missingRecommended);
+ }
+
+ private static string? ReadStr(JsonElement root, string name)
+ {
+ if (!root.TryGetProperty(name, out var node))
+ {
+ // 兼容 PascalCase
+ var pascal = char.ToUpperInvariant(name[0]) + name[1..];
+ if (!root.TryGetProperty(pascal, out node))
+ {
+ return null;
+ }
+ }
+
+ return node.ValueKind switch
+ {
+ JsonValueKind.String => node.GetString(),
+ JsonValueKind.Number => node.ToString(),
+ JsonValueKind.Null => null,
+ _ => node.ToString(),
+ };
+ }
+
+ private static string? CleanText(string? value) =>
+ string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+
+ private static string? ExtractJsonObject(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ return null;
+ }
+
+ var trimmed = text.Trim();
+ var fence = Regex.Match(
+ trimmed,
+ @"```(?:json)?\s*([\s\S]*?)```",
+ RegexOptions.IgnoreCase);
+ if (fence.Success)
+ {
+ trimmed = fence.Groups[1].Value.Trim();
+ }
+
+ var start = trimmed.IndexOf('{');
+ var end = trimmed.LastIndexOf('}');
+ if (start < 0 || end <= start)
+ {
+ return null;
+ }
+
+ return trimmed[start..(end + 1)];
+ }
+}