This commit is contained in:
zpc 2025-09-26 20:39:27 +08:00
parent 57300de4e1
commit fcb057ee5f
5 changed files with 288 additions and 137 deletions

View File

@ -128,9 +128,9 @@ namespace CoreCms.Net.Model.Entities
public System.String important_data { get; set; }
/// <summary>
/// 是否需要退款。0 无需退款未交押金1 待支付 2 已支付 9退款失败
/// 是否需要退款。0 无需退款未交押金1 待支付 2 已支付 3 发起退款4退款中5退款完成。9退款失败
/// </summary>
[Display(Name = "是否需要退款。0 无需退款未交押金1 待支付 2 已支付 9退款失败")]
[Display(Name = "是否需要退款。0 无需退款未交押金1 待支付 2 已支付 3 发起退款4退款中5退款完成。 9退款失败")]
public System.Int32 is_refund { get; set; }
/// <summary>

View File

@ -66,8 +66,10 @@ namespace CoreCms.Net.Task
//定时刷新获取微信AccessToken
RecurringJob.AddOrUpdate<RefreshWeChatAccessTokenJob>(s => s.Execute(), "0 0/4 * * * ? ", TimeZoneInfo.Local); // 每2分钟刷新获取微信AccessToken
// 预约状态处理(每分钟执行一次)
// 预约状态处理(每分钟执行一次)
RecurringJob.AddOrUpdate<SQReservationJob>(s => s.Execute(), "0 0/1 * * * ? ", TimeZoneInfo.Local);
// 预约退款处理
//RecurringJob.AddOrUpdate<SQReservationRefundJob>(s => s.Execute(), "0 0/1 * * * ? ", TimeZoneInfo.Local);
}

View File

@ -1,6 +1,7 @@
using CoreCms.Net.Caching.AutoMate.RedisCache;
using CoreCms.Net.Configuration;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using DotLiquid.Tags;
@ -17,149 +18,158 @@ using System.Threading.Tasks;
using static SKIT.FlurlHttpClient.Wechat.Api.Models.CgibinUserInfoBatchGetRequest.Types;
namespace CoreCms.Net.Task
namespace CoreCms.Net.Task;
/// <summary>
/// 预约定时清理
/// </summary>
public class SQReservationJob
{
/// <summary>
/// 预约定时清理
/// </summary>
public class SQReservationJob
private readonly ISQReservationsServices _reservationsServices;
private readonly ISQReservationParticipantsServices _participantsServices;
private readonly IRedisOperationRepository _redisOperationRepository;
public SQReservationJob(ISQReservationsServices reservationsServices,
ISQReservationParticipantsServices participantsServices,
IRedisOperationRepository redisOperationRepository)
{
private readonly ISQReservationsServices _reservationsServices;
private readonly ISQReservationParticipantsServices _participantsServices;
private readonly IRedisOperationRepository _redisOperationRepository;
public SQReservationJob(ISQReservationsServices reservationsServices,
ISQReservationParticipantsServices participantsServices,
IRedisOperationRepository redisOperationRepository)
{
_reservationsServices = reservationsServices;
_participantsServices = participantsServices;
_redisOperationRepository = redisOperationRepository;
}
_reservationsServices = reservationsServices;
_participantsServices = participantsServices;
_redisOperationRepository = redisOperationRepository;
}
public async System.Threading.Tasks.Task Execute()
public async System.Threading.Tasks.Task Execute()
{
Console.WriteLine($"[SQReservationJob] 执行开始 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
var now = DateTime.Now;
Console.WriteLine("[SQReservationJob] 步骤1将已到结束时间的预约置为已结束3");
// 1) 将已到结束时间的预约置为已结束3
var endedList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 2 && r.end_time <= now,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤1查询到待结束记录数量 = {endedList?.Count ?? 0}");
if (endedList != null && endedList.Count > 0)
{
Console.WriteLine($"[SQReservationJob] 执行开始 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
var now = DateTime.Now;
Console.WriteLine("[SQReservationJob] 步骤1将已到结束时间的预约置为已结束3");
// 1) 将已到结束时间的预约置为已结束3
var endedList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 2 && r.end_time <= now,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤1查询到待结束记录数量 = {endedList?.Count ?? 0}");
if (endedList != null && endedList.Count > 0)
var changedCount = 0;
foreach (var item in endedList)
{
var changedCount = 0;
foreach (var item in endedList)
Console.WriteLine($"[SQReservationJob] 步骤1预约ID={item.id} 从状态2->3end_time={item.end_time:yyyy-MM-dd HH:mm:ss}");
item.status = 3;
item.updated_at = now;
changedCount++;
}
Console.WriteLine($"[SQReservationJob] 步骤1批量更新记录数 = {changedCount}");
await _reservationsServices.UpdateAsync(endedList);
}
//Console.WriteLine("[SQReservationJob] 步骤2将开始时间已到且未结束的预约置为进行中2");
//// 2) 将开始时间已到且未结束的预约置为进行中2
//var startedList = await _reservationsServices.QueryListByClauseAsync(
// r => r.status == 1 && r.start_time <= now && r.end_time > now,
// r => r.id, OrderByType.Asc);
//Console.WriteLine($"[SQReservationJob] 步骤2查询到待置为进行中记录数量 = {startedList?.Count ?? 0}");
//if (startedList != null && startedList.Count > 0)
//{
// var changedCount = 0;
// foreach (var item in startedList)
// {
// Console.WriteLine($"[SQReservationJob] 步骤2预约ID={item.id} 从状态1->2start_time={item.start_time:yyyy-MM-dd HH:mm:ss} end_time={item.end_time:yyyy-MM-dd HH:mm:ss}");
// item.status = 2;
// item.updated_at = now;
// changedCount++;
// }
// Console.WriteLine($"[SQReservationJob] 步骤2批量更新记录数 = {changedCount}");
// await _reservationsServices.UpdateAsync(startedList);
//}
// 2.5) 到达开始时间但人数未满的预约置为取消4
Console.WriteLine("[SQReservationJob] 步骤2.5到达开始时间但人数未满的预约置为取消4");
var toCancelList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 0 && r.start_time <= now && r.end_time > now,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤2.5:查询到候选取消记录数量 = {toCancelList?.Count ?? 0}");
if (toCancelList != null && toCancelList.Count > 0)
{
var examineCount = 0;
foreach (var item in toCancelList)
{
var count = await _participantsServices.GetCountAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 参与人数={count}/{item.player_count}start_time={item.start_time:yyyy-MM-dd HH:mm:ss}");
if (count < item.player_count)
{
Console.WriteLine($"[SQReservationJob] 步骤1预约ID={item.id} 从状态2->3end_time={item.end_time:yyyy-MM-dd HH:mm:ss}");
item.status = 3;
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 人数未满,置为取消(4)");
item.status = 4;
item.updated_at = now;
changedCount++;
}
Console.WriteLine($"[SQReservationJob] 步骤1批量更新记录数 = {changedCount}");
await _reservationsServices.UpdateAsync(endedList);
examineCount++;
}
//Console.WriteLine("[SQReservationJob] 步骤2将开始时间已到且未结束的预约置为进行中2");
//// 2) 将开始时间已到且未结束的预约置为进行中2
//var startedList = await _reservationsServices.QueryListByClauseAsync(
// r => r.status == 1 && r.start_time <= now && r.end_time > now,
// r => r.id, OrderByType.Asc);
//Console.WriteLine($"[SQReservationJob] 步骤2查询到待置为进行中记录数量 = {startedList?.Count ?? 0}");
//if (startedList != null && startedList.Count > 0)
//{
// var changedCount = 0;
// foreach (var item in startedList)
// {
// Console.WriteLine($"[SQReservationJob] 步骤2预约ID={item.id} 从状态1->2start_time={item.start_time:yyyy-MM-dd HH:mm:ss} end_time={item.end_time:yyyy-MM-dd HH:mm:ss}");
// item.status = 2;
// item.updated_at = now;
// changedCount++;
// }
// Console.WriteLine($"[SQReservationJob] 步骤2批量更新记录数 = {changedCount}");
// await _reservationsServices.UpdateAsync(startedList);
//}
// 2.5) 到达开始时间但人数未满的预约置为取消4
Console.WriteLine("[SQReservationJob] 步骤2.5到达开始时间但人数未满的预约置为取消4");
var toCancelList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 0 && r.start_time <= now && r.end_time > now,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤2.5:查询到候选取消记录数量 = {toCancelList?.Count ?? 0}");
if (toCancelList != null && toCancelList.Count > 0)
var toCancelChanged = toCancelList.Where(x => x.status == 4).ToList();
Console.WriteLine($"[SQReservationJob] 步骤2.5:需要更新为取消的记录数 = {toCancelChanged.Count}");
if (toCancelChanged.Count > 0)
{
var examineCount = 0;
foreach (var item in toCancelList)
await _reservationsServices.UpdateAsync(toCancelChanged);
Console.WriteLine($"[SQReservationJob] 步骤2.5:已更新为取消的记录数 = {toCancelChanged.Count}");
foreach (var item in toCancelChanged)
{
var count = await _participantsServices.GetCountAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 参与人数={count}/{item.player_count}start_time={item.start_time:yyyy-MM-dd HH:mm:ss}");
if (count < item.player_count)
{
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 人数未满,置为取消(4)");
item.status = 4;
item.updated_at = now;
}
examineCount++;
}
var toCancelChanged = toCancelList.Where(x => x.status == 4).ToList();
Console.WriteLine($"[SQReservationJob] 步骤2.5:需要更新为取消的记录数 = {toCancelChanged.Count}");
if (toCancelChanged.Count > 0)
{
await _reservationsServices.UpdateAsync(toCancelChanged);
Console.WriteLine($"[SQReservationJob] 步骤2.5:已更新为取消的记录数 = {toCancelChanged.Count}");
foreach (var item in toCancelChanged)
{
Console.WriteLine($"[SQReservationJob] 步骤2.5准备通知预约ID={item.id} 的参与用户");
var userList = await _participantsServices.QueryListByClauseAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 需通知用户数量 = {userList.Count}");
await _reservationsServices.NotifyReservationFailedAsync(item, userList);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 组局失败通知入队完成");
}
Console.WriteLine($"[SQReservationJob] 步骤2.5准备通知预约ID={item.id} 的参与用户");
var userList = await _participantsServices.QueryListByClauseAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 需通知用户数量 = {userList.Count}");
await _reservationsServices.NotifyReservationFailedAsync(item, userList);
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 组局失败通知入队完成");
// 解散(取消)的预约,标记需要退款押金的参与者为“待退款(3)”
// 条件:未退出(status=0) 且 押金已支付(is_refund=2) 且 有有效支付单号
await _participantsServices.UpdateAsync(
it => new SQReservationParticipants
{
is_refund = 3
},
it => it.reservation_id == item.id && it.status == 0 && it.is_refund == 2 && it.paymentId != null && it.paymentId != "");
Console.WriteLine($"[SQReservationJob] 步骤2.5预约ID={item.id} 已标记需要退款的参与者为待退款(3)");
}
}
// 3) 开始前30分钟内人数已凑齐则置为锁定1
Console.WriteLine("[SQReservationJob] 步骤3开始前30分钟内人数已凑齐则置为锁定1");
var threshold = now.AddMinutes(30);
var toLockList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 0 && r.start_time > now && r.start_time <= threshold,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤3查询到候选锁定记录数量 = {toLockList?.Count ?? 0},阈值时间={threshold:yyyy-MM-dd HH:mm:ss}");
if (toLockList != null && toLockList.Count > 0)
{
foreach (var item in toLockList)
{
// 当前有效参与人数(未退出)
var count = await _participantsServices.GetCountAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 参与人数={count}/{item.player_count}start_time={item.start_time:yyyy-MM-dd HH:mm:ss}");
if (count >= item.player_count)
{
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 人数已满,置为锁定(1)");
item.status = 1;
item.updated_at = now;
}
}
// 仅更新被修改的项
var changed = toLockList.Where(x => x.status == 1).ToList();
Console.WriteLine($"[SQReservationJob] 步骤3需要更新为锁定的记录数 = {changed.Count}");
if (changed.Count > 0)
{
await _reservationsServices.UpdateAsync(changed);
Console.WriteLine($"[SQReservationJob] 步骤3已更新为锁定的记录数 = {changed.Count}");
foreach (var item in changed)
{
Console.WriteLine($"[SQReservationJob] 步骤3准备通知预约ID={item.id} 的参与用户");
var userList = await _participantsServices.QueryListByClauseAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 需通知用户数量 = {userList.Count}");
await _reservationsServices.NotifyReservationSuccessAsync(item, userList);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 组局成功通知入队完成");
}
}
}
Console.WriteLine($"[SQReservationJob] 执行结束 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
// 3) 开始前30分钟内人数已凑齐则置为锁定1
Console.WriteLine("[SQReservationJob] 步骤3开始前30分钟内人数已凑齐则置为锁定1");
var threshold = now.AddMinutes(30);
var toLockList = await _reservationsServices.QueryListByClauseAsync(
r => r.status == 0 && r.start_time > now && r.start_time <= threshold,
r => r.id, OrderByType.Asc);
Console.WriteLine($"[SQReservationJob] 步骤3查询到候选锁定记录数量 = {toLockList?.Count ?? 0},阈值时间={threshold:yyyy-MM-dd HH:mm:ss}");
if (toLockList != null && toLockList.Count > 0)
{
foreach (var item in toLockList)
{
// 当前有效参与人数(未退出)
var count = await _participantsServices.GetCountAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 参与人数={count}/{item.player_count}start_time={item.start_time:yyyy-MM-dd HH:mm:ss}");
if (count >= item.player_count)
{
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 人数已满,置为锁定(1)");
item.status = 1;
item.updated_at = now;
}
}
// 仅更新被修改的项
var changed = toLockList.Where(x => x.status == 1).ToList();
Console.WriteLine($"[SQReservationJob] 步骤3需要更新为锁定的记录数 = {changed.Count}");
if (changed.Count > 0)
{
await _reservationsServices.UpdateAsync(changed);
Console.WriteLine($"[SQReservationJob] 步骤3已更新为锁定的记录数 = {changed.Count}");
foreach (var item in changed)
{
Console.WriteLine($"[SQReservationJob] 步骤3准备通知预约ID={item.id} 的参与用户");
var userList = await _participantsServices.QueryListByClauseAsync(p => p.reservation_id == item.id && p.status == 0);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 需通知用户数量 = {userList.Count}");
await _reservationsServices.NotifyReservationSuccessAsync(item, userList);
Console.WriteLine($"[SQReservationJob] 步骤3预约ID={item.id} 组局成功通知入队完成");
}
}
}
Console.WriteLine($"[SQReservationJob] 执行结束 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
}

View File

@ -0,0 +1,113 @@
using CoreCms.Net.Caching.AutoMate.RedisCache;
using CoreCms.Net.IServices;
using CoreCms.Net.Model.Entities;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoreCms.Net.Task;
public class SQReservationRefundJob
{
private readonly ISQReservationsServices _reservationsServices;
private readonly ISQReservationParticipantsServices _participantsServices;
private readonly IRedisOperationRepository _redisOperationRepository;
private readonly ICoreCmsBillPaymentsServices _billPaymentsServices;
private readonly ICoreCmsBillRefundServices _billRefundServices;
public SQReservationRefundJob(ISQReservationsServices reservationsServices,
ISQReservationParticipantsServices participantsServices,
IRedisOperationRepository redisOperationRepository,
ICoreCmsBillPaymentsServices billPaymentsServices,
ICoreCmsBillRefundServices billRefundServices)
{
_reservationsServices = reservationsServices;
_participantsServices = participantsServices;
_redisOperationRepository = redisOperationRepository;
_billPaymentsServices = billPaymentsServices;
_billRefundServices = billRefundServices;
}
public async System.Threading.Tasks.Task Execute()
{
Console.WriteLine($"[SQReservationRefundJob] 执行开始 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
// 扫描需要退款的参与者is_refund=3发起退款、未退出(status=0)、有有效支付单号
var needRefundList = await _participantsServices.QueryListByClauseAsync(
p => p.is_refund == 3 && p.paymentId != null && p.paymentId != "",
p => p.id, OrderByType.Asc);
if (needRefundList == null || needRefundList.Count == 0)
{
Console.WriteLine("[SQReservationRefundJob] 无需处理的记录");
return;
}
foreach (var participant in needRefundList)
{
try
{
// 查询预约获取押金
var reservation = await _reservationsServices.QueryByClauseAsync(r => r.id == participant.reservation_id);
var deposit = reservation?.deposit_fee ?? 0m;
if (deposit <= 0)
{
// 无需退款,置为已完成
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 4 }, it => it.id == participant.id);
continue;
}
// 获取支付单
var payInfoRes = await _billPaymentsServices.GetInfo(participant.paymentId, participant.user_id);
if (payInfoRes.status == false || payInfoRes.data is not CoreCmsBillPayments payInfo)
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 9 }, it => it.id == participant.id);
continue;
}
// 退款金额:押金与实付金额取最小
var refundMoney = deposit > payInfo.money ? payInfo.money : deposit;
if (refundMoney <= 0)
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 4 }, it => it.id == participant.id);
continue;
}
// 创建退款单
var addRefundRes = await _billRefundServices.ToAdd(participant.user_id, payInfo.sourceId, payInfo.type, refundMoney, "");
if (addRefundRes.status == false)
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 9 }, it => it.id == participant.id);
continue;
}
// 查询退款单并执行原路退款
var refundInfo = await _billRefundServices.QueryByClauseAsync(p =>
p.userId == participant.user_id && p.sourceId == payInfo.sourceId && p.type == payInfo.type && p.money == refundMoney && p.status == 1);
if (refundInfo == null)
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 9 }, it => it.id == participant.id);
continue;
}
var doRefundRes = await _billRefundServices.ToRefund(refundInfo.refundId, 2, "");
if (doRefundRes.status)
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 4 }, it => it.id == participant.id);
}
else
{
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 9 }, it => it.id == participant.id);
}
}
catch (Exception ex)
{
Console.WriteLine($"[SQReservationRefundJob] 处理失败 participantId={participant.id}, ex={ex.Message}");
await _participantsServices.UpdateAsync(it => new SQReservationParticipants { is_refund = 9 }, it => it.id == participant.id);
}
}
Console.WriteLine($"[SQReservationRefundJob] 执行结束 @ {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
}

View File

@ -1036,7 +1036,19 @@ public class SQController : ControllerBase
};
}
// 7.2发起者取消整个预约,所有参与人都标记为退出
// 7.2 有押金时,已支付押金(is_refund=2)的参与者为发起退款(3)
if (reservation.deposit_fee.HasValue && reservation.deposit_fee.Value > 0)
{
await _SQReservationParticipantsServices.UpdateAsync(
it => new SQReservationParticipants
{
is_refund = 3
},
it => it.reservation_id == dto.reservation_id && it.status == 0 && it.is_refund == 2 && it.paymentId != null && it.paymentId != "");
}
// 7.3发起者取消整个预约,所有参与人都标记为退出
await _SQReservationParticipantsServices.UpdateAsync(
it => new SQReservationParticipants
{
@ -1061,6 +1073,16 @@ public class SQController : ControllerBase
quit_time = DateTime.Now
},
it => it.reservation_id == dto.reservation_id && it.user_id == userId);
// 有押金且已支付押金时,标记该参与者为发起退款(3)
if (reservation.deposit_fee.HasValue && reservation.deposit_fee.Value > 0)
{
await _SQReservationParticipantsServices.UpdateAsync(
it => new SQReservationParticipants
{
is_refund = 3
},
it => it.reservation_id == dto.reservation_id && it.user_id == userId && it.is_refund == 2 && it.paymentId != null && it.paymentId != "");
}
}
return new WebApiDto()
@ -1208,9 +1230,13 @@ public class SQController : ControllerBase
if (reservation.deposit_fee > 0)
{
//有押金,押金原路退回
// 有押金,标记到场人员为“发起退款(3)”,由定时任务统一处理实际退款
await _SQReservationParticipantsServices.UpdateAsync(
it => new SQReservationParticipants
{
is_refund = 3
},
it => it.reservation_id == dto.reservation_id && it.status == 0 && it.is_arrive == 1 && it.is_refund == 2 && it.paymentId != null && it.paymentId != "");
}
_dbBase.Ado.CommitTran();