feat: 货品编号支持同步到服务端 extJsonStr 字段

- ProductDto 增加 extJson 解析,从服务端读取货品编号
- ProductExtJson 类用于序列化/反序列化扩展属性
- 同步时优先使用本地货品编号,其次使用服务端的
- 新增"上传到服务端"按钮,批量上传货品编号
- 调用 /user/apiExtShopGoods/save 接口更新 extJsonStr

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-12-30 14:59:27 +08:00
parent f233aba6be
commit 83030a779c
5 changed files with 217 additions and 2 deletions

View File

@@ -264,18 +264,39 @@ namespace PackagingMallShipper.Models
[JsonProperty("pics")]
public List<string> Pics { get; set; }
/// <summary>
/// 扩展属性JSON用于存储货品编号等自定义字段
/// </summary>
[JsonProperty("extJson")]
public ProductExtJson ExtJson { get; set; }
/// <summary>
/// 获取扩展属性中的货品编号
/// </summary>
public string GetItemCode()
{
return ExtJson?.ItemCode;
}
/// <summary>
/// 转换为本地Product模型
/// </summary>
public Product ToProduct(Product existingProduct = null)
{
// 优先使用本地已有的货品编号,其次使用服务端的扩展属性
var itemCode = existingProduct?.ItemCode;
if (string.IsNullOrEmpty(itemCode))
{
itemCode = GetItemCode();
}
return new Product
{
Id = Id,
Name = Name,
BarCode = BarCode,
YyId = YyId,
ItemCode = existingProduct?.ItemCode,
ItemCode = itemCode,
MinPrice = MinPrice,
OriginalPrice = OriginalPrice,
Stores = Stores,
@@ -299,4 +320,46 @@ namespace PackagingMallShipper.Models
};
}
}
/// <summary>
/// 产品扩展属性(存储在服务端 extJsonStr 字段)
/// </summary>
public class ProductExtJson
{
/// <summary>
/// 货品编号
/// </summary>
[JsonProperty("itemCode")]
public string ItemCode { get; set; }
/// <summary>
/// 本地备注(可选同步)
/// </summary>
[JsonProperty("localNote")]
public string LocalNote { get; set; }
/// <summary>
/// 序列化为JSON字符串
/// </summary>
public string ToJsonString()
{
return JsonConvert.SerializeObject(this);
}
/// <summary>
/// 从JSON字符串解析
/// </summary>
public static ProductExtJson FromJsonString(string json)
{
if (string.IsNullOrEmpty(json)) return null;
try
{
return JsonConvert.DeserializeObject<ProductExtJson>(json);
}
catch
{
return null;
}
}
}
}

View File

@@ -64,7 +64,19 @@ namespace PackagingMallShipper.Services
public interface IProductSyncService
{
Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental);
Task<UploadResult> UploadItemCodesToServerAsync(List<Product> products);
event Action<int, int> OnSyncProgress;
event Action<string> OnSyncMessage;
}
/// <summary>
/// 上传结果
/// </summary>
public class UploadResult
{
public int TotalCount { get; set; }
public int SuccessCount { get; set; }
public int FailedCount { get; set; }
public List<string> Errors { get; set; } = new List<string>();
}
}

View File

@@ -151,6 +151,13 @@ namespace PackagingMallShipper.Services
var isNew = !exists;
var picsJson = dto.Pics != null ? JsonConvert.SerializeObject(dto.Pics) : "";
// 优先使用本地已有的货品编号,其次使用服务端 extJson 中的
var itemCode = existingItemCode;
if (string.IsNullOrEmpty(itemCode))
{
itemCode = dto.GetItemCode() ?? "";
}
var sql = isNew
? @"INSERT INTO products_cache
(id, name, bar_code, yy_id, item_code, min_price, original_price, stores, status,
@@ -171,7 +178,7 @@ namespace PackagingMallShipper.Services
cmd.Parameters.AddWithValue("@name", dto.Name ?? "");
cmd.Parameters.AddWithValue("@barCode", dto.BarCode ?? "");
cmd.Parameters.AddWithValue("@yyId", dto.YyId ?? "");
cmd.Parameters.AddWithValue("@itemCode", existingItemCode ?? "");
cmd.Parameters.AddWithValue("@itemCode", itemCode);
cmd.Parameters.AddWithValue("@minPrice", dto.MinPrice);
cmd.Parameters.AddWithValue("@originalPrice", dto.OriginalPrice);
cmd.Parameters.AddWithValue("@stores", dto.Stores);
@@ -187,5 +194,69 @@ namespace PackagingMallShipper.Services
}
});
}
/// <summary>
/// 上传货品编号到服务端(通过 extJsonStr 字段)
/// </summary>
public async Task<UploadResult> UploadItemCodesToServerAsync(List<Product> products)
{
var token = _authService.GetToken();
if (string.IsNullOrEmpty(token))
throw new UnauthorizedAccessException("未登录,请先登录");
var result = new UploadResult { TotalCount = products.Count };
for (int i = 0; i < products.Count; i++)
{
var product = products[i];
OnSyncMessage?.Invoke($"正在上传 {i + 1}/{products.Count}: {product.Name}");
OnSyncProgress?.Invoke(i + 1, products.Count);
try
{
var extJson = new ProductExtJson
{
ItemCode = product.ItemCode,
LocalNote = product.LocalNote
};
var url = "https://user.api.it120.cc/user/apiExtShopGoods/save";
var formData = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("id", product.Id.ToString()),
new KeyValuePair<string, string>("extJsonStr", extJson.ToJsonString())
});
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
var response = await _httpClient.PostAsync(url, formData);
var json = await response.Content.ReadAsStringAsync();
Debug.WriteLine($"[上传货品编号] 产品ID: {product.Id}, 响应: {json}");
var data = JsonConvert.DeserializeObject<ApiResponse<object>>(json);
if (data?.Code == 0)
{
result.SuccessCount++;
}
else
{
result.FailedCount++;
result.Errors.Add($"{product.Name}: {data?.Msg ?? ""}");
}
}
catch (Exception ex)
{
result.FailedCount++;
result.Errors.Add($"{product.Name}: {ex.Message}");
Debug.WriteLine($"[上传货品编号异常] {product.Name}: {ex.Message}");
}
}
OnSyncMessage?.Invoke($"上传完成!成功 {result.SuccessCount},失败 {result.FailedCount}");
return result;
}
}
}

View File

@@ -134,6 +134,69 @@ namespace PackagingMallShipper.ViewModels
}
}
[RelayCommand]
private async Task UploadItemCodesToServerAsync()
{
// 获取所有有货品编号的产品
var productsWithItemCode = new System.Collections.Generic.List<Product>();
foreach (var p in Products)
{
if (p.HasItemCode)
productsWithItemCode.Add(p);
}
if (productsWithItemCode.Count == 0)
{
MessageBox.Show("没有已设置货品编号的产品", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var confirmResult = MessageBox.Show(
$"将上传 {productsWithItemCode.Count} 个产品的货品编号到服务端,是否继续?",
"确认上传",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (confirmResult != MessageBoxResult.Yes) return;
IsBusy = true;
StatusMessage = "正在上传货品编号...";
try
{
var result = await _productSyncService.UploadItemCodesToServerAsync(productsWithItemCode);
if (result.FailedCount > 0)
{
var errorMsg = string.Join("\n", result.Errors);
MessageBox.Show($"上传完成!成功 {result.SuccessCount},失败 {result.FailedCount}\n\n失败详情:\n{errorMsg}",
"上传结果", MessageBoxButton.OK, MessageBoxImage.Warning);
}
else
{
MessageBox.Show($"上传完成!成功 {result.SuccessCount} 个产品",
"上传成功", MessageBoxButton.OK, MessageBoxImage.Information);
}
StatusMessage = $"上传完成!成功 {result.SuccessCount},失败 {result.FailedCount}";
}
catch (UnauthorizedAccessException)
{
StatusMessage = "登录已过期,请重新登录";
MessageBox.Show("登录已过期,请重新登录", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch (Exception ex)
{
StatusMessage = $"上传失败: {ex.Message}";
MessageBox.Show($"上传失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsBusy = false;
SyncProgress = "";
}
}
[RelayCommand]
private async Task SearchAsync()
{

View File

@@ -81,6 +81,12 @@
<Button Content="编辑货品编号" Command="{Binding StartEditItemCodeCommand}"
Width="100" Height="30" Margin="0,0,5,0"/>
<Button Content="上传到服务端" Command="{Binding UploadItemCodesToServerCommand}"
Width="100" Height="30" Margin="0,0,5,0"
Background="#FA8C16" Foreground="White" BorderThickness="0"
ToolTip="将本地货品编号同步到服务端"
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
</StackPanel>
<!-- 产品列表 -->