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")]
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 产品列表 -->
|
||||
|
||||
Reference in New Issue
Block a user