Files
PackagingMallShipper/PackagingMallShipper/Services/ProductSyncService.cs
Administrator 83030a779c 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>
2025-12-30 14:59:27 +08:00

263 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}