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:
@@ -264,18 +264,39 @@ namespace PackagingMallShipper.Models
|
|||||||
[JsonProperty("pics")]
|
[JsonProperty("pics")]
|
||||||
public List<string> Pics { get; set; }
|
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>
|
/// <summary>
|
||||||
/// 转换为本地Product模型
|
/// 转换为本地Product模型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Product ToProduct(Product existingProduct = null)
|
public Product ToProduct(Product existingProduct = null)
|
||||||
{
|
{
|
||||||
|
// 优先使用本地已有的货品编号,其次使用服务端的扩展属性
|
||||||
|
var itemCode = existingProduct?.ItemCode;
|
||||||
|
if (string.IsNullOrEmpty(itemCode))
|
||||||
|
{
|
||||||
|
itemCode = GetItemCode();
|
||||||
|
}
|
||||||
|
|
||||||
return new Product
|
return new Product
|
||||||
{
|
{
|
||||||
Id = Id,
|
Id = Id,
|
||||||
Name = Name,
|
Name = Name,
|
||||||
BarCode = BarCode,
|
BarCode = BarCode,
|
||||||
YyId = YyId,
|
YyId = YyId,
|
||||||
ItemCode = existingProduct?.ItemCode,
|
ItemCode = itemCode,
|
||||||
MinPrice = MinPrice,
|
MinPrice = MinPrice,
|
||||||
OriginalPrice = OriginalPrice,
|
OriginalPrice = OriginalPrice,
|
||||||
Stores = Stores,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,19 @@ namespace PackagingMallShipper.Services
|
|||||||
public interface IProductSyncService
|
public interface IProductSyncService
|
||||||
{
|
{
|
||||||
Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental);
|
Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental);
|
||||||
|
Task<UploadResult> UploadItemCodesToServerAsync(List<Product> products);
|
||||||
event Action<int, int> OnSyncProgress;
|
event Action<int, int> OnSyncProgress;
|
||||||
event Action<string> OnSyncMessage;
|
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>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,13 @@ namespace PackagingMallShipper.Services
|
|||||||
var isNew = !exists;
|
var isNew = !exists;
|
||||||
var picsJson = dto.Pics != null ? JsonConvert.SerializeObject(dto.Pics) : "";
|
var picsJson = dto.Pics != null ? JsonConvert.SerializeObject(dto.Pics) : "";
|
||||||
|
|
||||||
|
// 优先使用本地已有的货品编号,其次使用服务端 extJson 中的
|
||||||
|
var itemCode = existingItemCode;
|
||||||
|
if (string.IsNullOrEmpty(itemCode))
|
||||||
|
{
|
||||||
|
itemCode = dto.GetItemCode() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
var sql = isNew
|
var sql = isNew
|
||||||
? @"INSERT INTO products_cache
|
? @"INSERT INTO products_cache
|
||||||
(id, name, bar_code, yy_id, item_code, min_price, original_price, stores, status,
|
(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("@name", dto.Name ?? "");
|
||||||
cmd.Parameters.AddWithValue("@barCode", dto.BarCode ?? "");
|
cmd.Parameters.AddWithValue("@barCode", dto.BarCode ?? "");
|
||||||
cmd.Parameters.AddWithValue("@yyId", dto.YyId ?? "");
|
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("@minPrice", dto.MinPrice);
|
||||||
cmd.Parameters.AddWithValue("@originalPrice", dto.OriginalPrice);
|
cmd.Parameters.AddWithValue("@originalPrice", dto.OriginalPrice);
|
||||||
cmd.Parameters.AddWithValue("@stores", dto.Stores);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
[RelayCommand]
|
||||||
private async Task SearchAsync()
|
private async Task SearchAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,6 +81,12 @@
|
|||||||
|
|
||||||
<Button Content="编辑货品编号" Command="{Binding StartEditItemCodeCommand}"
|
<Button Content="编辑货品编号" Command="{Binding StartEditItemCodeCommand}"
|
||||||
Width="100" Height="30" Margin="0,0,5,0"/>
|
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>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 产品列表 -->
|
<!-- 产品列表 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user