- 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>
263 lines
12 KiB
C#
263 lines
12 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data.SQLite;
|
||
using System.Diagnostics;
|
||
using System.Net.Http;
|
||
using System.Threading.Tasks;
|
||
using Newtonsoft.Json;
|
||
using PackagingMallShipper.Data;
|
||
using PackagingMallShipper.Models;
|
||
|
||
namespace PackagingMallShipper.Services
|
||
{
|
||
public class ProductSyncService : IProductSyncService
|
||
{
|
||
private readonly IAuthService _authService;
|
||
private readonly HttpClient _httpClient;
|
||
|
||
public event Action<int, int> OnSyncProgress;
|
||
public event Action<string> OnSyncMessage;
|
||
|
||
public ProductSyncService(IAuthService authService)
|
||
{
|
||
_authService = authService;
|
||
_httpClient = new HttpClient();
|
||
_httpClient.Timeout = TimeSpan.FromSeconds(60);
|
||
}
|
||
|
||
public async Task<SyncResult> SyncProductsAsync(SyncMode mode = SyncMode.Incremental)
|
||
{
|
||
var token = _authService.GetToken();
|
||
if (string.IsNullOrEmpty(token))
|
||
throw new UnauthorizedAccessException("未登录,请先登录");
|
||
|
||
var result = new SyncResult();
|
||
|
||
try
|
||
{
|
||
int page = 1;
|
||
int pageSize = 50;
|
||
bool hasMore = true;
|
||
int totalPages = 1;
|
||
|
||
while (hasMore)
|
||
{
|
||
OnSyncMessage?.Invoke($"正在同步产品第 {page}/{totalPages} 页...");
|
||
|
||
var url = $"https://user.api.it120.cc/user/apiExtShopGoods/list?page={page}&pageSize={pageSize}";
|
||
|
||
Debug.WriteLine($"[产品同步] URL: {url}");
|
||
|
||
_httpClient.DefaultRequestHeaders.Clear();
|
||
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
|
||
|
||
var response = await _httpClient.PostAsync(url, null);
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
|
||
Debug.WriteLine($"[产品同步] HTTP状态码: {response.StatusCode}");
|
||
Debug.WriteLine($"[产品同步] 响应长度: {json?.Length ?? 0}");
|
||
|
||
var data = JsonConvert.DeserializeObject<ApiResponse<ProductListData>>(json);
|
||
|
||
if (data?.Code != 0)
|
||
{
|
||
if (data?.Msg?.Contains("token") == true || data?.Msg?.Contains("登录") == true)
|
||
throw new UnauthorizedAccessException("当前登录token无效,请重新登录");
|
||
throw new Exception(data?.Msg ?? "获取产品列表失败");
|
||
}
|
||
|
||
var products = data.Data?.GetProducts() ?? new List<ProductDto>();
|
||
totalPages = data.Data?.TotalPage ?? 1;
|
||
if (totalPages == 0) totalPages = 1;
|
||
var totalRow = data.Data?.GetTotalRow() ?? 0;
|
||
|
||
Debug.WriteLine($"[产品同步] 产品数量: {products.Count}, 总记录: {totalRow}, 总页数: {totalPages}");
|
||
|
||
if (products.Count == 0) break;
|
||
|
||
foreach (var productDto in products)
|
||
{
|
||
var isNew = await SaveProductAsync(productDto);
|
||
if (isNew) result.NewCount++;
|
||
else result.UpdatedCount++;
|
||
}
|
||
|
||
result.TotalCount += products.Count;
|
||
OnSyncProgress?.Invoke(page, totalPages);
|
||
|
||
if (page >= totalPages) hasMore = false;
|
||
else page++;
|
||
}
|
||
|
||
OnSyncMessage?.Invoke($"产品同步完成!新增 {result.NewCount},更新 {result.UpdatedCount}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.WriteLine($"[产品同步异常] {ex.Message}");
|
||
throw;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private Task<bool> SaveProductAsync(ProductDto dto)
|
||
{
|
||
return Task.Run(() =>
|
||
{
|
||
using (var conn = SqliteHelper.GetConnection())
|
||
{
|
||
conn.Open();
|
||
|
||
// 检查是否存在,并获取现有的本地字段
|
||
string existingItemCode = null;
|
||
string existingLocalNote = null;
|
||
decimal? existingPickupPrice = null;
|
||
decimal? existingExpressPrice = null;
|
||
decimal? existingPackPrice = null;
|
||
int? existingPackQuantity = null;
|
||
string existingProductSeries = null;
|
||
string existingPlacementMethod = null;
|
||
string existingThickness = null;
|
||
string existingHoleSize = null;
|
||
string existingBoxSize = null;
|
||
string existingApplicableType = null;
|
||
bool exists = false;
|
||
|
||
var checkSql = "SELECT * FROM products_cache WHERE id = @id";
|
||
using (var cmd = new SQLiteCommand(checkSql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@id", dto.Id);
|
||
using (var reader = cmd.ExecuteReader())
|
||
{
|
||
if (reader.Read())
|
||
{
|
||
exists = true;
|
||
existingItemCode = reader.GetStringSafe("item_code");
|
||
existingLocalNote = reader.GetStringSafe("local_note");
|
||
existingPickupPrice = reader.GetDecimalNullable("pickup_price");
|
||
existingExpressPrice = reader.GetDecimalNullable("express_price");
|
||
existingPackPrice = reader.GetDecimalNullable("pack_price");
|
||
existingPackQuantity = reader.GetInt32Nullable("pack_quantity");
|
||
existingProductSeries = reader.GetStringSafe("product_series");
|
||
existingPlacementMethod = reader.GetStringSafe("placement_method");
|
||
existingThickness = reader.GetStringSafe("thickness");
|
||
existingHoleSize = reader.GetStringSafe("hole_size");
|
||
existingBoxSize = reader.GetStringSafe("box_size");
|
||
existingApplicableType = reader.GetStringSafe("applicable_type");
|
||
}
|
||
}
|
||
}
|
||
|
||
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,
|
||
category_id, unit, pics_json, is_local_only, synced_at, local_created_at)
|
||
VALUES
|
||
(@id, @name, @barCode, @yyId, @itemCode, @minPrice, @originalPrice, @stores, @status,
|
||
@categoryId, @unit, @picsJson, 0, @syncedAt, @syncedAt)"
|
||
: @"UPDATE products_cache SET
|
||
name = @name, bar_code = @barCode, yy_id = @yyId,
|
||
min_price = @minPrice, original_price = @originalPrice, stores = @stores,
|
||
status = @status, category_id = @categoryId, unit = @unit, pics_json = @picsJson,
|
||
synced_at = @syncedAt
|
||
WHERE id = @id";
|
||
|
||
using (var cmd = new SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@id", dto.Id);
|
||
cmd.Parameters.AddWithValue("@name", dto.Name ?? "");
|
||
cmd.Parameters.AddWithValue("@barCode", dto.BarCode ?? "");
|
||
cmd.Parameters.AddWithValue("@yyId", dto.YyId ?? "");
|
||
cmd.Parameters.AddWithValue("@itemCode", itemCode);
|
||
cmd.Parameters.AddWithValue("@minPrice", dto.MinPrice);
|
||
cmd.Parameters.AddWithValue("@originalPrice", dto.OriginalPrice);
|
||
cmd.Parameters.AddWithValue("@stores", dto.Stores);
|
||
cmd.Parameters.AddWithValue("@status", dto.Status);
|
||
cmd.Parameters.AddWithValue("@categoryId", (object)dto.CategoryId ?? DBNull.Value);
|
||
cmd.Parameters.AddWithValue("@unit", dto.Unit ?? "");
|
||
cmd.Parameters.AddWithValue("@picsJson", picsJson);
|
||
cmd.Parameters.AddWithValue("@syncedAt", DateTime.Now);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
|
||
return isNew;
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
}
|
||
}
|