Files
qhmes/yy-admin-master/YY.Admin.Services/Service/MixerMaterial/MixerMaterialService.cs

375 lines
16 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 Microsoft.Extensions.Configuration;
using SqlSugar;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
namespace YY.Admin.Services.Service.MixerMaterial;
public class MixerMaterialService : IMixerMaterialService, ISingletonDependency
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly ISqlSugarClient _dbContext;
private readonly ILoggerService _logger;
private readonly SemaphoreSlim _syncLock = new(1, 1);
private readonly object _cacheLock = new();
private readonly string _cacheFilePath;
private List<MesMixerMaterial> _localCache = [];
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new NullableDateTimeJsonConverter() }
};
public MixerMaterialService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ISqlSugarClient dbContext,
ILoggerService logger)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_dbContext = dbContext;
_logger = logger;
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YY.Admin", "sync-cache");
Directory.CreateDirectory(appDataDir);
_cacheFilePath = Path.Combine(appDataDir, "mixer-material-cache.json");
_ = LoadCacheAsync();
}
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
private async Task LoadCacheAsync()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
var json = await File.ReadAllTextAsync(_cacheFilePath).ConfigureAwait(false);
var list = JsonSerializer.Deserialize<List<MesMixerMaterial>>(json, _jsonOpts);
if (list != null)
lock (_cacheLock) { _localCache = list; }
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 加载本地缓存失败:{ex.Message}");
}
}
private async Task SaveCacheAsync(List<MesMixerMaterial> list)
{
try
{
var json = JsonSerializer.Serialize(list, _jsonOpts);
await File.WriteAllTextAsync(_cacheFilePath, json).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 保存本地缓存失败:{ex.Message}");
}
}
public async Task SyncFromRemoteAsync(CancellationToken ct = default)
{
// 等待当前正在进行的同步结束再执行本次同步WaitAsync(0) 会静默跳过,导致 STOMP 通知丢失)
await _syncLock.WaitAsync(ct).ConfigureAwait(false);
try
{
var all = new List<MesMixerMaterial>();
var pageNo = 1;
const int pageSize = 500;
while (true)
{
var qs = HttpUtility.ParseQueryString(string.Empty);
qs["pageNo"] = pageNo.ToString();
qs["pageSize"] = pageSize.ToString();
// mes_mixer_material 不在多租户隔离表中,不传 tenantId 可拉取全部记录
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/list?{qs}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
{
_logger.Warning($"[密炼物料] 同步请求失败,状态码:{(int)resp.StatusCode}");
break;
}
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("result", out var resultEl)) break;
var records = resultEl.TryGetProperty("records", out var recEl)
? recEl.Deserialize<List<MesMixerMaterial>>(_jsonOpts) ?? []
: [];
all.AddRange(records);
if (records.Count < pageSize) break;
pageNo++;
}
if (all.Count > 0)
{
lock (_cacheLock) { _localCache = all; }
await SaveCacheAsync(all).ConfigureAwait(false);
_logger.Information($"[密炼物料] 同步完成,共 {all.Count} 条");
}
else
{
_logger.Warning("[密炼物料] 同步返回 0 条,可能是 tenantId 配置有误或网络异常,保留原缓存");
}
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 远程同步失败:{ex.Message}");
}
finally
{
_syncLock.Release();
}
}
public Task<MixerMaterialPageResult> PageAsync(
int pageNo, int pageSize,
string? materialCode = null, string? materialName = null, string? erpCode = null,
string? majorCategoryId = null, string? minorCategoryId = null,
CancellationToken ct = default)
{
List<MesMixerMaterial> cache;
lock (_cacheLock) { cache = _localCache; }
if (cache.Count > 0)
return Task.FromResult(PageFromCache(cache, pageNo, pageSize,
materialCode, materialName, erpCode, majorCategoryId, minorCategoryId));
return PageFromRemoteAsync(pageNo, pageSize,
materialCode, materialName, erpCode, majorCategoryId, minorCategoryId, ct);
}
private static MixerMaterialPageResult PageFromCache(
List<MesMixerMaterial> cache, int pageNo, int pageSize,
string? materialCode, string? materialName, string? erpCode,
string? majorCategoryId, string? minorCategoryId)
{
var q = cache.AsEnumerable();
if (!string.IsNullOrWhiteSpace(materialCode))
q = q.Where(x => x.MaterialCode?.Contains(materialCode, StringComparison.OrdinalIgnoreCase) == true);
if (!string.IsNullOrWhiteSpace(materialName))
q = q.Where(x => x.MaterialName?.Contains(materialName, StringComparison.OrdinalIgnoreCase) == true);
if (!string.IsNullOrWhiteSpace(erpCode))
q = q.Where(x => x.ErpCode?.Contains(erpCode, StringComparison.OrdinalIgnoreCase) == true);
if (!string.IsNullOrWhiteSpace(majorCategoryId))
q = q.Where(x => x.MajorCategoryId == majorCategoryId);
if (!string.IsNullOrWhiteSpace(minorCategoryId))
q = q.Where(x => x.MinorCategoryId == minorCategoryId);
var filtered = q.ToList();
var records = filtered.Skip((pageNo - 1) * pageSize).Take(pageSize).ToList();
return new MixerMaterialPageResult(records, filtered.Count, pageNo, pageSize);
}
private async Task<MixerMaterialPageResult> PageFromRemoteAsync(
int pageNo, int pageSize,
string? materialCode, string? materialName, string? erpCode,
string? majorCategoryId, string? minorCategoryId,
CancellationToken ct)
{
var qs = HttpUtility.ParseQueryString(string.Empty);
qs["pageNo"] = pageNo.ToString();
qs["pageSize"] = pageSize.ToString();
if (!string.IsNullOrWhiteSpace(materialCode)) qs["materialCode"] = materialCode;
if (!string.IsNullOrWhiteSpace(materialName)) qs["materialName"] = materialName;
if (!string.IsNullOrWhiteSpace(erpCode)) qs["erpCode"] = erpCode;
if (!string.IsNullOrWhiteSpace(majorCategoryId)) qs["majorCategoryId"] = majorCategoryId;
if (!string.IsNullOrWhiteSpace(minorCategoryId)) qs["minorCategoryId"] = minorCategoryId;
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/list?{qs}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var result = doc.RootElement.GetProperty("result");
var records = result.GetProperty("records").Deserialize<List<MesMixerMaterial>>(_jsonOpts) ?? [];
var total = result.TryGetProperty("total", out var totalEl) ? totalEl.GetInt64() : records.Count;
return new MixerMaterialPageResult(records, total, pageNo, pageSize);
}
public async Task<MesMixerMaterial?> GetByIdAsync(string id, CancellationToken ct = default)
{
List<MesMixerMaterial> cache;
lock (_cacheLock) { cache = _localCache; }
var local = cache.FirstOrDefault(x => x.Id == id);
if (local != null) return local;
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/queryById?id={Uri.EscapeDataString(id)}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) return null;
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("result", out var resultEl)) return null;
return resultEl.Deserialize<MesMixerMaterial>(_jsonOpts);
}
public Task<bool> AddAsync(MesMixerMaterial material, CancellationToken ct = default)
=> PostJsonAsync($"{BaseUrl}/mes/material/mixerMaterial/anon/add", material, ct);
public Task<bool> EditAsync(MesMixerMaterial material, CancellationToken ct = default)
=> PostJsonAsync($"{BaseUrl}/mes/material/mixerMaterial/anon/edit", material, ct);
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
{
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/delete?id={Uri.EscapeDataString(id)}";
using var client = CreateClient();
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
public async Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default)
{
var url = $"{BaseUrl}/mes/material/mixerMaterial/anon/deleteBatch?ids={Uri.EscapeDataString(ids)}";
using var client = CreateClient();
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
public async Task<List<KeyValuePair<string, string>>> GetMajorCategoryOptionsAsync(CancellationToken ct = default)
{
try
{
var root = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => x.Code == "XSLMES_MATERIAL").FirstAsync();
if (root == null) return [];
var majors = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => x.Pid == root.Id).OrderBy(x => x.Code).ToListAsync();
return majors.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.Select(x => new KeyValuePair<string, string>(x.Name ?? x.Code ?? x.Id, x.Id))
.ToList();
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 加载大类选项失败:{ex.Message}");
return [];
}
}
public async Task<List<KeyValuePair<string, string>>> GetMinorCategoryOptionsAsync(string majorCategoryId, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(majorCategoryId)) return [];
try
{
var minors = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => x.Pid == majorCategoryId).OrderBy(x => x.Code).ToListAsync();
return minors.Where(x => !string.IsNullOrWhiteSpace(x.Id))
.Select(x => new KeyValuePair<string, string>(x.Name ?? x.Code ?? x.Id, x.Id))
.ToList();
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 加载小类选项失败:{ex.Message}");
return [];
}
}
public async Task<List<MaterialCategoryNode>> GetMaterialCategoryTreeAsync(CancellationToken ct = default)
{
try
{
var root = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => x.Code == "XSLMES_MATERIAL").FirstAsync();
if (root == null) return [];
var majors = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => x.Pid == root.Id).OrderBy(x => x.Code).ToListAsync();
if (majors.Count == 0) return [];
var majorIds = majors.Select(x => x.Id).ToList();
var allMinors = await _dbContext.Queryable<JeecgSysCategoryItem>()
.ClearFilter().Where(x => majorIds.Contains(x.Pid!)).OrderBy(x => x.Code).ToListAsync();
return majors.Select(major =>
{
var minorNodes = allMinors
.Where(m => m.Pid == major.Id)
.Select(m => new MaterialCategoryNode(m.Id, m.Name, m.Code, []))
.ToList();
return new MaterialCategoryNode(major.Id, major.Name, major.Code, minorNodes);
}).ToList();
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料] 加载分类树失败:{ex.Message}");
return [];
}
}
private async Task<bool> PostJsonAsync(string url, object body, CancellationToken ct)
{
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private static async Task<bool> IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct)
{
try
{
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200;
if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean();
return true;
}
catch { return true; }
}
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
{
private static readonly string[] Formats =
[
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss.fffZ"
];
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType == JsonTokenType.String)
{
var raw = reader.GetString();
if (string.IsNullOrWhiteSpace(raw)) return null;
if (DateTime.TryParseExact(raw, Formats, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeLocal, out var exact)) return exact;
if (DateTime.TryParse(raw, out var fallback)) return fallback;
}
throw new JsonException($"无法将 JSON 值转换为 DateTime?token={reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
else writer.WriteNullValue();
}
}
}