自动费用模板后台任务

dev
嵇文龙 1 month ago
parent 6a9e2de59c
commit e90e2c5997

@ -40,6 +40,15 @@ public interface IFeeRecordService
/// <returns></returns>
Task<DataResult> SaveAsync(IEnumerable<FeeRecord> items, bool excludeZeroFee = false, bool useTransaction = true);
/// <summary>
/// 费用保存后提交审核
/// </summary>
/// <param name="items">要提交的费用记录</param>
/// <param name="excludeZeroFee">是否排除总价为零的费用</param>
/// <param name="useTransaction">是否使用事务</param>
/// <returns></returns>
Task<DataResult> SaveAndSubmitAsync(IEnumerable<FeeRecord> items, bool excludeZeroFee = false, bool useTransaction = true);
/// <summary>
/// 根据费用模板引入
/// </summary>

@ -323,6 +323,27 @@ namespace DS.WMS.Core.Fee.Method
}
}
/// <summary>
/// 费用保存后提交审核
/// </summary>
/// <param name="items">要提交的费用记录</param>
/// <param name="excludeZeroFee">是否排除总价为零的费用</param>
/// <param name="useTransaction">是否使用事务</param>
/// <returns></returns>
public async Task<DataResult> SaveAndSubmitAsync(IEnumerable<FeeRecord> items, bool excludeZeroFee = false, bool useTransaction = true)
{
var result = await SaveAsync(items, excludeZeroFee, useTransaction);
if (!result.Succeeded)
return result;
var ids = items.Select(x => x.Id).ToArray();
ids = await TenantDb.Queryable<FeeRecord>().Where(x => ids.Contains(x.Id) && x.FeeStatus == FeeStatus.Entering).Select(x => x.Id).ToArrayAsync();
if (ids.Length > 0)
result = await SubmitForApprovalAsync(TaskBaseTypeEnum.FEE_AUDIT, string.Empty, ids);
return result;
}
/// <summary>
/// 根据模板ID创建
/// </summary>

@ -1,13 +1,9 @@
using DS.Module.Core;
using DS.Module.Core.Condition;
using DS.Module.Core.Data;
using DS.WMS.Core.Fee.Dtos;
using DS.WMS.Core.Fee.Entity;
using System.Net.Http.Headers;
using DS.Module.Core;
using DS.WMS.Core.HangfireJob.Interface;
using DS.WMS.Core.Op.Dtos;
using DS.WMS.Core.Op.Entity;
using DS.WMS.Core.Sys.Entity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using SqlSugar;
namespace DS.WMS.Core.HangfireJob.Method
@ -17,7 +13,14 @@ namespace DS.WMS.Core.HangfireJob.Method
/// </summary>
public class FeeCustTemplateJobService : IFeeCustTemplateJobService
{
static readonly ApiFox api;
ISqlSugarClient? db;
IConfiguration configuration;
static FeeCustTemplateJobService()
{
api = new ApiFox();
}
/// <summary>
/// 初始化
@ -26,6 +29,7 @@ namespace DS.WMS.Core.HangfireJob.Method
public FeeCustTemplateJobService(IServiceProvider serviceProvider)
{
db = serviceProvider.GetRequiredService<ISqlSugarClient>();
configuration = serviceProvider.GetRequiredService<IConfiguration>();
}
/// <summary>
@ -34,502 +38,71 @@ namespace DS.WMS.Core.HangfireJob.Method
/// <returns></returns>
public async Task GenerateFeesAsync()
{
db.QueryFilter.Clear();
var dbLinks = await db.Queryable<Module.SqlSugar.SysTenantLink>().ToListAsync();
SqlSugarClient? tenantDb = null;
try
{
foreach (var dbLink in dbLinks)
{
string configId = dbLinks.IndexOf(dbLink).ToString();
//if (db.IsAnyConnection(configId))
//{
// var config = new ConnectionConfig
// {
// ConfigId = configId,
// DbType = dbLink.DbType,
// IsAutoCloseConnection = true,
// ConnectionString = dbLink.Connection
// };
// db.AddConnection(config);
//}
tenantDb = new SqlSugarClient(new ConnectionConfig
{
ConfigId = configId,
DbType = dbLink.DbType,
IsAutoCloseConnection = true,
ConnectionString = dbLink.Connection
});
await GenerateFeesCoreAsync(tenantDb, DateTime.Now.Date);
}
}
finally
{
tenantDb?.Dispose();
}
}
/// <summary>
/// 根据开船日生成费用
/// </summary>
/// <param name="tenantDb"></param>
/// <param name="etd">开船日</param>
/// <returns></returns>
internal async Task GenerateFeesCoreAsync(SqlSugarClient tenantDb, DateTime etd)
{
DateTime dt = DateTime.Now;
var list = await tenantDb.Queryable<FeeCustTemplate>()
.Where(x => !x.IsDisabled && SqlFunc.Between(dt, x.StartTime, x.EndTime) && (x.IsShared || x.CustomerId != null))
.Select(x => new FeeCustTemplate
{
Id = x.Id,
CustomerId = x.CustomerId,
FeeType = x.FeeType,
FeeCategoryId = x.FeeCategoryId,
FeeCategoryName = x.FeeCategoryName,
Priority = x.Priority,
IsShared = x.IsShared,
POLCode = x.POLCode,
PODCode = x.PODCode,
LaneId = x.LaneId,
SourceId = x.SourceId,
CarrierId = x.CarrierId,
ForwarderId = x.ForwarderId,
MBLFrtCode = x.MBLFrtCode,
Condition = x.Condition
}).ToListAsync();
if (list.Count == 0)
return;
var tids = list.Select(x => x.Id);
var orders = await tenantDb.Queryable<SeaExport>().Where(x => SqlFunc.DateIsSame(x.ETD, etd) &&
SqlFunc.Subqueryable<FeeCustTemplateRecord>().Where(y => y.BusinessId == x.Id && y.BusinessType == BusinessType.OceanShippingExport && tids.Contains(y.TemplateId)).NotAny())
.Select<SeaExportRes>().ToListAsync();
if (orders.Count == 0)
return;
List<FeeRecord> feeList = [];
await tenantDb.Ado.BeginTranAsync();
try
{
foreach (var order in orders)
{
var custList = list.Where(x => x.CustomerId == order.CustomerId).OrderBy(x => x.Priority);
foreach (var template in custList) //遍历客户费用模板,查找匹配项
{
var fees = await CreateFeesIfMatchAsync(tenantDb, order, template);
if (fees != null)
var adminUser = await db.Queryable<SysUser>()
.Where(x => x.TenantId == dbLink.TenantId && x.Status == 0 && x.UserType == 1)
.OrderByDescending(x => x.CreateTime)
.Select(x => new
{
feeList.AddRange(fees);
var record = new FeeCustTemplateRecord
{
BusinessId = order.CustomerId,
BusinessType = BusinessType.OceanShippingExport,
CreateTime = dt,
FeeCategoryId = template.FeeCategoryId,
FeeType = template.FeeType,
TemplateId = template.Id
};
await tenantDb.Insertable(record).ExecuteCommandAsync();
}
}
//未找到客户模板,开始匹配共享模板
if (feeList.Count == 0)
x.Id,
x.UserName,
x.Password,
x.DefaultOrgId,
x.DefaultOrgName,
x.TenantId,
x.TenantName
}).FirstAsync();
if (adminUser == null)
{
var sharedList = list.Where(x => x.IsShared).OrderBy(x => x.Priority).ToList();
foreach (var template in sharedList)
{
var fees = await CreateFeesIfMatchAsync(tenantDb, order, template);
if (fees != null)
{
feeList.AddRange(fees);
var record = new FeeCustTemplateRecord
{
BusinessId = order.CustomerId,
BusinessType = BusinessType.OceanShippingExport,
CreateTime = dt,
FeeCategoryId = template.FeeCategoryId,
FeeType = template.FeeType,
TemplateId = template.Id
};
await tenantDb.Insertable(record).ExecuteCommandAsync();
}
}
Console.WriteLine($"未能获取租户系统管理员租户ID{dbLink.TenantId}");
continue;
}
//写入当前业务的费用
if (feeList.Count > 0)
var tokenModel = new JwtHelper.JwtTokenModel
{
await SaveAsync(tenantDb, feeList);
feeList.Clear();
}
}
await tenantDb.Ado.CommitTranAsync();
}
catch (Exception ex)
{
await tenantDb.Ado.RollbackTranAsync();
await ex.LogAsync(db);
}
}
/// <summary>
/// 费用保存
/// </summary>
/// <param name="tenantDb"></param>
/// <param name="items">要提交的费用记录</param>
/// <returns></returns>
static async Task SaveAsync(SqlSugarClient tenantDb, List<FeeRecord> items)
{
var first = items.Select(x => new { x.BusinessType, x.BusinessId }).FirstOrDefault();
//if (await IsFeeLockedAsync(first.BusinessId, first.BusinessType))
// return DataResult.FailedWithDesc(nameof(MultiLanguageConst.FeeLocked));
var order = await tenantDb.Queryable<SeaExport>().Where(x => x.Id == first.BusinessId).Select(x => new
{
x.TEU,
x.KGS, //毛重
x.PKGS, //件数
x.CBM //尺码
}).FirstAsync();
if (order == null)
return;
//获取订单箱型箱量
string bsNo = first.BusinessId.ToString();
var ctns = await tenantDb.Queryable<OpCtn>().Where(y => y.BSNO == bsNo).Select(x => new
{
x.CtnCode,
x.CtnNum
}).ToListAsync();
items = items.FindAll(x => x.Amount != 0);
foreach (var item in items)
{
if (item.Quantity == 0)
{
//逐个判定处理标准
switch (item.Unit)
{
case "HOUR": //小时
goto case "P";
case "GE": //个
goto case "P";
case "DAY": //天
goto case "P";
//case "XX": //箱型
// goto default;
case "JJZL": //计价重量
goto case "Z";
case "ZJ": //总价
goto case "P";
case "JZ": //净重
item.Quantity = 0;
break;
case "TEU": //TEU
item.Quantity = order.TEU;
break;
case "JF": //计费吨
item.Quantity = order.KGS.GetValueOrDefault() / 1000 > order.CBM.GetValueOrDefault() ? order.KGS.GetValueOrDefault() : order.CBM.GetValueOrDefault();
break;
case "J": //件数
item.Quantity = order.PKGS.GetValueOrDefault();
break;
case "C": //尺码
item.Quantity = order.CBM.GetValueOrDefault();
break;
case "Z": //重量
item.Quantity = order.KGS.GetValueOrDefault();
break;
case "P": //单票
item.Quantity = 1;
break;
default: //查找箱型标准
var ctn = ctns.Find(x => x.CtnCode == item.Unit);
item.Quantity = ctn == null ? 0 : ctn.CtnNum.GetValueOrDefault();
break;
}
}
//计算税费
item.SetTax();
}
//若计价货币单位不等于本位币则尝试获取最新汇率
await FetchExchangeRateAsync(tenantDb, items);
//写入费用
await tenantDb.Insertable(items).ExecuteCommandAsync();
}
static async Task FetchExchangeRateAsync(SqlSugarClient tenantDb, List<FeeRecord> items)
{
var exRecords = items.Where(x => x.Currency != x.LocalCurrency && x.ExchangeRate == null);
if (exRecords.Any())
{
var exchanges = exRecords.GroupBy(x => new
{
x.Currency,
x.LocalCurrency,
x.FeeType,
}).Select(x => new ExchangeRate
{
CurrencyFrom = x.Key.Currency,
CurrencyTo = x.Key.LocalCurrency,
FeeType = x.Key.FeeType
});
List<ExchangeRate> exchangeRates = [];
foreach (var item in exchanges)
{
var result = await GetExchangeRateAsync(tenantDb, item);
if (result.Succeeded && result.Data != null)
exchangeRates.Add(result.Data);
}
foreach (var item in items)
{
item.ExchangeRate = exchangeRates.Find(x => x.CurrencyFrom == item.Currency &&
x.CurrencyTo == item.LocalCurrency && x.FeeType == item.FeeType)?.Rate;
Uid = adminUser.Id.ToString(),
Name = adminUser.UserName,
OrgId = adminUser.DefaultOrgId.ToString(),
TenantId = adminUser.TenantId.ToString(),
TenantName = adminUser.TenantName
};
var token = JwtHelper.Encrypt(tokenModel, false, true);
await SendRequestAsync(token);
}
}
}
static async Task<DataResult<ExchangeRate>> GetExchangeRateAsync(SqlSugarClient tenantDb, ExchangeRate exchange)
{
if (exchange.CurrencyFrom == exchange.CurrencyTo)
{
exchange.ReverseRate = exchange.Rate = 1m;
return DataResult<ExchangeRate>.Success(exchange);
}
//获取本位币,默认=人民币
string localCurrency = FeeCurrency.RMB_CODE;
if (string.IsNullOrWhiteSpace(exchange.CurrencyTo))
{
exchange.CurrencyTo = localCurrency;
}
//需要做二次转换
if (exchange.CurrencyFrom != localCurrency && exchange.CurrencyTo != localCurrency)
{
//获取中间汇率
var middleRate = await GetLocalRateAsync(tenantDb, exchange.CurrencyFrom, localCurrency, exchange.FeeType);
if (middleRate == null)
return DataResult<ExchangeRate>.FailedData(exchange, message: $"{MultiLanguageConst.FeeCurrencyNotFound}{exchange.CurrencyFrom}");
var rate = await GetLocalRateAsync(tenantDb, exchange.CurrencyTo, localCurrency, exchange.FeeType);
exchange.Rate = Math.Round(1 / (rate ?? 1m) * middleRate.GetValueOrDefault(), 4, MidpointRounding.AwayFromZero);
exchange.ReverseRate = Math.Round(1 / exchange.Rate, 4, MidpointRounding.AwayFromZero);
}
else
finally
{
string currency = exchange.CurrencyFrom == FeeCurrency.RMB_CODE ? exchange.CurrencyTo : exchange.CurrencyFrom;
var rate = await GetLocalRateAsync(tenantDb, currency, localCurrency, exchange.FeeType);
if (currency == exchange.CurrencyFrom)
{
exchange.Rate = rate ?? 1m;
exchange.ReverseRate = Math.Round(1 / exchange.Rate, 4, MidpointRounding.AwayFromZero);
}
else
{
exchange.ReverseRate = rate ?? 1m;
exchange.Rate = Math.Round(1 / exchange.ReverseRate, 4, MidpointRounding.AwayFromZero);
}
tenantDb?.Dispose();
}
return DataResult<ExchangeRate>.Success(exchange);
}
static async Task<decimal?> GetLocalRateAsync(SqlSugarClient tenantDb, string currency, string localCurrency, FeeType? type)
internal async Task SendRequestAsync(string token)
{
var entity = await tenantDb.Queryable<FeeCurrency>().Where(x => x.CodeName == currency).Includes(x => x.Exchanges).FirstAsync();
if (entity == null)
return null;
var rate = entity.DefaultRate;
DateTime dtNow = DateTime.Now;
if (type.HasValue && entity.Exchanges != null && entity.Exchanges.Count > 0)
if (api.BaseUri == null)
{
//取当前时间范围内的最新一条,优先获取本位币
var item = entity.Exchanges.FindAll(x => x.Status == StatusEnum.Enable && x.LocalCurrency == localCurrency &&
x.StartDate >= dtNow && x.EndDate <= dtNow).OrderByDescending(x => x.CreateTime).FirstOrDefault();
var baseUrl = configuration["AutoFeeTemplate:BaseUrl"];
if (string.IsNullOrEmpty(baseUrl))
throw new ApplicationException("未配置自动费用模板请求基础URL");
item ??= entity.Exchanges.FindAll(x => x.Status == StatusEnum.Enable &&
x.StartDate >= dtNow && x.EndDate <= dtNow).OrderByDescending(x => x.CreateTime).FirstOrDefault();
if (item != null)
rate = type.Value == FeeType.Receivable ? item.DRValue : item.CRValue;
api.BaseUri = new Uri(baseUrl, UriKind.Absolute);
}
return rate;
}
static async Task<List<FeeRecord>?> CreateFeesIfMatchAsync(SqlSugarClient tenantDb, SeaExportRes order, FeeCustTemplate template)
{
if (!string.IsNullOrEmpty(template.POLCode) && template.POLCode != order.LoadPortCode)
return null;
if (!string.IsNullOrEmpty(template.PODCode) && template.PODCode != order.DischargePortCode)
return null;
if (!string.IsNullOrEmpty(template.MBLFrtCode) && template.MBLFrtCode != order.MBLFrtCode)
return null;
if (template.LaneId.HasValue && template.LaneId != order.LaneId)
return null;
if (template.CarrierId.HasValue && template.CarrierId != order.CarrierId)
return null;
if (template.SourceId.HasValue && template.SourceId != order.SourceId)
return null;
if (template.ForwarderId.HasValue && template.ForwarderId != order.ForwarderId)
return null;
if (!string.IsNullOrEmpty(template.Condition)) //设置了自定义匹配条件
api.DefaultHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await api.SendRequestAsync(HttpMethod.Post, configuration["AutoFeeTemplate:GenerateFeesUrl"]!, new
{
var conditionModel = JsonConvert.DeserializeObject<ConditionContent>(template.Condition);
if (!IsMatch(order, conditionModel))
return null;
}
if (await tenantDb.Queryable<FeeCustTemplateRecord>().AnyAsync(x => x.BusinessId == order.Id && x.BusinessType == BusinessType.OceanShippingExport &&
x.FeeType == template.FeeType && x.FeeCategoryId == template.FeeCategoryId))
return null;
var details = await tenantDb.Queryable<FeeCustTemplateDetail>().Where(y => y.TemplateId == template.Id)
.Select(x => new FeeRecord
{
BusinessId = order.Id,
BusinessType = BusinessType.OceanShippingExport,
FeeType = template.FeeType,
FeeId = x.FeeId,
FeeCode = x.FeeCode,
FeeName = x.FeeName,
CustomerId = x.CustomerId,
CustomerName = x.CustomerName,
CustomerType = x.CustomerType,
Unit = x.Unit,
UnitPrice = SqlFunc.IsNull(x.UnitPrice.Value, 0),
//Quantity = x.IsCtn ? 1 : 0,
Currency = x.Currency,
ExchangeRate = x.ExchangeRate == null ? 1 : x.ExchangeRate,
TaxRate = SqlFunc.IsNull(x.TaxRate.Value, 0),
AccTaxRate = SqlFunc.IsNull(x.AccTaxRate.Value, 0),
Tax = SqlFunc.IsNull(x.Tax.Value, 0),
TaxUnitPrice = SqlFunc.IsNull(x.TaxUnitPrice.Value, 0),
IsInvoice = x.IsInvoice,
IsAdvancedPay = x.IsAdvancedPay,
LocalCurrency = FeeCurrency.RMB_CODE,
Remark = template.FeeCategoryName,
TemplateId = x.TemplateId,
InputMethod = InputMethod.Automatic
}).ToListAsync();
foreach (var detail in details)
{
if (detail.CustomerId == 0)
{
switch (detail.CustomerType)
{
case "controller":
detail.CustomerId = order.CustomerId;
detail.CustomerName = order.CustomerName;
break;
case "yard":
detail.CustomerId = order.YardId;
detail.CustomerName = order.Yard;
break;
case "custom":
detail.CustomerId = order.CustomserId;
detail.CustomerName = order.Customser;
break;
case "contract":
detail.CustomerId = order.ContractClientId;
detail.CustomerName = order.ContractClientName;
break;
case "shipagency":
detail.CustomerId = order.ShipAgencyId;
detail.CustomerName = order.ShipAgency;
break;
case "shipper":
detail.CustomerId = order.ShipperId.GetValueOrDefault();
detail.CustomerName = order.Shipper;
break;
case "truck":
detail.CustomerId = order.TruckerId;
detail.CustomerName = order.Trucker;
break;
case "booking":
detail.CustomerId = order.ForwarderId;
detail.CustomerName = order.Forwarder;
break;
case "carrier":
detail.CustomerId = order.CarrierId;
detail.CustomerName = order.Carrier;
break;
case "wareHouse":
detail.CustomerId = order.WareHouseId;
detail.CustomerName = order.WareHouse;
break;
case "shippercn":
detail.CustomerId = order.ShipperCnId.GetValueOrDefault();
detail.CustomerName = order.ShipperCn;
break;
case "agent":
detail.CustomerId = order.AgentId.GetValueOrDefault();
detail.CustomerName = order.Agent;
break;
}
}
}
return details;
}
static bool IsMatch(object source, ConditionContent condition)
{
if (source == null || condition == null)
return false;
TaskFlowDataContext dataContext = new((TaskFlowDataNameConst.Business, source));
if (string.IsNullOrEmpty(condition.SourceName))
condition.SourceName = TaskFlowDataNameConst.Business;
etd = DateTime.Now.Date
});
return ConditionHelper.IsPass(condition, dataContext);
if (!response.IsSuccessStatusCode)
throw new ApplicationException("自动费用模板生成费用失败,详情查看日志");
}
}
}

@ -79,14 +79,14 @@ namespace DS.WMS.FeeApi.Controllers
}
/// <summary>
/// 提交费用
/// 保存费用
/// </summary>
/// <param name="recordSubmit">费用提交参数</param>
/// <param name="recordSubmit"></param>
/// <returns></returns>
[HttpPost, Route("Submit")]
public async Task<DataResult> SubmitAsync([FromBody] FeeRecordSubmit recordSubmit)
{
if (recordSubmit == null || recordSubmit.Items == null)
if (recordSubmit.Items == null)
return DataResult.Failed("参数无效", MultiLanguageConst.IllegalRequest);
if (recordSubmit.Items.Any(x => x.FeeStatus != FeeStatus.Entering && x.FeeStatus != FeeStatus.RejectSubmission))
@ -101,6 +101,29 @@ namespace DS.WMS.FeeApi.Controllers
return await _feeService.SaveAsync(recordSubmit.Items);
}
/// <summary>
/// 保存费用并提交审核
/// </summary>
/// <param name="recordSubmit"></param>
/// <returns></returns>
[HttpPost, Route("SubmitWithAudit")]
public async Task<DataResult> SubmitWithAuditAsync([FromBody] FeeRecordSubmit recordSubmit)
{
if (recordSubmit.Items == null)
return DataResult.Failed("参数无效", MultiLanguageConst.IllegalRequest);
if (recordSubmit.Items.Any(x => x.FeeStatus != FeeStatus.Entering && x.FeeStatus != FeeStatus.RejectSubmission))
return DataResult.Failed("只能提交状态为‘录入’或‘驳回提交’的费用", MultiLanguageConst.IllegalRequest);
foreach (var item in recordSubmit.Items)
{
item.BusinessId = recordSubmit.BusinessId;
item.BusinessType = recordSubmit.BusinessType;
item.FeeStatus = FeeStatus.Entering;
}
return await _feeService.SaveAndSubmitAsync(recordSubmit.Items);
}
/// <summary>
/// 根据一组模板ID创建费用记录引入模板
/// </summary>

@ -63,10 +63,14 @@
"UserSecret": "",
"BaseUrl": "http://47.105.115.105:26650"
},
"AutoFeeTemplate": {
"BaseUrl": "http://localhost:5295",
"GenerateFeesUrl": "/feeApi/FeeCustTemplate/GenerateFees"
},
"HangfireSettings": {
"DbString": "server=rm-m5e06xxqpa68a68ry5o.mysql.rds.aliyuncs.com;port=3306;uid=rulesengine_admin;pwd=Rule1qaz2wsx!QAZ;database=shippingweb8_hangfire;Allow User Variables=true",
"WorkerCount": 10,
"ServerName": "OpApi",
"Queues": "op"
"ServerName": "FeeApi",
"Queues": "fee"
}
}

Loading…
Cancel
Save