using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Mapster;
using Microsoft.AspNetCore.Mvc;
using NCC.ClayObject;
using NCC.Common.Configuration;
using NCC.Common.Core.Manager;
using NCC.Common.Enum;
using NCC.Common.Extension;
using NCC.Common.Filter;
using NCC.Common.Helper;
using NCC.Common.Model.NPOI;
using NCC.DataEncryption;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqYyjl;
using NCC.Extend.Entitys.Dto.LqYyjlPx;
using NCC.Extend.Entitys.Dto.LqYyjlPxJks;
using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_md_room;
using NCC.Extend.Entitys.lq_mdxx;
using NCC.Extend.Entitys.lq_xh_hyhk;
using NCC.Extend.Entitys.lq_yaoyjl;
using NCC.Extend.Entitys.lq_yyjl;
using NCC.Extend.Entitys.lq_yyjl_px;
using NCC.Extend.Entitys.lq_yyjl_px_jks;
using NCC.Extend.Interfaces.LqYyjl;
using NCC.FriendlyException;
using NCC.JsonSerialization;
using NCC.System.Entitys.Permission;
using SqlSugar;
using Yitter.IdGenerator;
namespace NCC.Extend.LqYyjl
{
///
/// 预约记录服务
///
[ApiDescriptionSettings(Tag = "绿纤预约记录服务", Name = "LqYyjl", Order = 200)]
[Route("api/Extend/[controller]")]
public class LqYyjlService : ILqYyjlService, IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository _lqYyjlRepository;
private readonly SqlSugarScope _db;
private readonly IUserManager _userManager;
///
/// 初始化一个类型的新实例
///
public LqYyjlService(ISqlSugarRepository lqYyjlRepository, IUserManager userManager)
{
_lqYyjlRepository = lqYyjlRepository;
_db = _lqYyjlRepository.Context;
_userManager = userManager;
}
#region 预约记录
///
/// 获取预约记录
///
/// 参数
///
[HttpGet("{id}")]
public async Task GetInfo(string id)
{
var entity = await _db.Queryable().FirstAsync(p => p.Id == id);
var output = entity.Adapt();
// 获取门店名称
if (!string.IsNullOrEmpty(entity.Djmd))
{
var store = await _db.Queryable().Where(u => u.Id == entity.Djmd).FirstAsync();
output.djmdName = store?.Dm;
}
// 获取预约人姓名
if (!string.IsNullOrEmpty(entity.Yyr))
{
var user = await _db.Queryable().Where(u => u.Id == entity.Yyr).FirstAsync();
output.yyrName = user?.RealName;
}
// 获取预约健康师姓名
if (!string.IsNullOrEmpty(entity.Yyjks))
{
var healthCoach = await _db.Queryable().Where(u => u.Id == entity.Yyjks).FirstAsync();
output.yyjksName = healthCoach?.RealName;
}
//获取沟通记录
if (!string.IsNullOrEmpty(entity.InviteId))
{
var invite = await _db.Queryable().Where(u => u.Id == entity.InviteId).FirstAsync();
output.InviteTime = invite?.Yysj;
}
if (!string.IsNullOrEmpty(entity.RoomId))
{
output.roomName = entity.RoomName;
if (string.IsNullOrEmpty(output.roomName))
{
output.roomName = await _db.Queryable()
.Where(r => r.Id == entity.RoomId)
.Select(r => r.RoomName)
.FirstAsync();
}
}
output.hasEffectiveConsume = await _db.Queryable()
.AnyAsync(x => x.AppointmentId == id && x.IsEffective == StatusEnum.有效.GetHashCode());
output.relatedConsumeList = await _db.Queryable()
.Where(x => x.AppointmentId == id && x.IsEffective == StatusEnum.有效.GetHashCode())
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.Select(x => new LqYyjlRelatedConsumeOutput
{
id = x.Id,
hksj = x.Hksj,
xfje = x.Xfje,
hymc = x.Hymc,
})
.ToListAsync();
await ResolveYyjlRelatedConsumeAsync(output, entity);
var pxEntities = await _db.Queryable()
.Where(x => x.YyjlId == id && x.IsEffective == 1)
.OrderBy(x => x.SortNo)
.OrderBy(x => x.CreateTime)
.ToListAsync();
output.lqYyjlPxList = pxEntities.Adapt>();
var jksEntities = await _db.Queryable()
.Where(x => x.YyjlId == id && x.IsEffective == 1)
.OrderBy(x => x.YyjlPxId)
.OrderBy(x => x.SortNo)
.OrderBy(x => x.CreateTime)
.ToListAsync();
var jksByPxId = jksEntities
.GroupBy(x => x.YyjlPxId)
.ToDictionary(
g => g.Key,
g => g.Select(e => new LqYyjlPxJksOutput
{
id = e.Id,
yyjlId = e.YyjlId,
yyjlPxId = e.YyjlPxId,
jks = e.Jks,
jksxm = e.Jksxm,
sortNo = e.SortNo,
isEffective = e.IsEffective,
}).ToList());
foreach (var pxOut in output.lqYyjlPxList)
{
pxOut.jksList = jksByPxId.TryGetValue(pxOut.id, out var jl)
? jl
: new List();
}
return output;
}
#endregion
#region 预约记录列表
///
/// 获取预约记录列表
///
/// 请求参数
///
[HttpGet("")]
public async Task GetList([FromQuery] LqYyjlListQueryInput input)
{
var sidx = input.sidx == null ? "id" : input.sidx;
var (startCzsj, endCzsj) = TryParseTsRange(input.czsj);
var (startYysj, endYysj) = TryParseTsRange(input.yysj);
var (startYyjs, endYyjs) = TryParseTsRange(input.yyjs);
var hasCzsj = startCzsj.HasValue && endCzsj.HasValue;
var hasYysj = startYysj.HasValue && endYysj.HasValue;
var hasYyjs = startYyjs.HasValue && endYyjs.HasValue;
var data = await _db.Queryable()
.WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
.WhereIF(!string.IsNullOrEmpty(input.djmd), p => p.Djmd.Equals(input.djmd))
.WhereIF(!string.IsNullOrEmpty(input.yyr), p => p.Yyr.Equals(input.yyr))
.WhereIF(!string.IsNullOrEmpty(input.gklx), p => p.Gklx.Contains(input.gklx))
.WhereIF(!string.IsNullOrEmpty(input.yytyxm), p => p.Yytyxm.Equals(input.yytyxm))
.WhereIF(!string.IsNullOrEmpty(input.czr), p => p.Czr.Equals(input.czr))
.WhereIF(hasCzsj, p => p.Czsj >= new DateTime(startCzsj.Value.Year, startCzsj.Value.Month, startCzsj.Value.Day, 0, 0, 0))
.WhereIF(hasCzsj, p => p.Czsj <= new DateTime(endCzsj.Value.Year, endCzsj.Value.Month, endCzsj.Value.Day, 23, 59, 59))
.WhereIF(!string.IsNullOrEmpty(input.gk), p => p.Gk.Equals(input.gk))
.WhereIF(!string.IsNullOrEmpty(input.gkxm), p => p.Gkxm.Contains(input.gkxm))
.WhereIF(!string.IsNullOrEmpty(input.yyjks), p => p.Yyjks.Equals(input.yyjks))
.WhereIF(hasYysj, p => p.Yysj >= new DateTime(startYysj.Value.Year, startYysj.Value.Month, startYysj.Value.Day, 0, 0, 0))
.WhereIF(hasYysj, p => p.Yysj <= new DateTime(endYysj.Value.Year, endYysj.Value.Month, endYysj.Value.Day, 23, 59, 59))
.WhereIF(hasYyjs, p => p.Yyjs >= new DateTime(startYyjs.Value.Year, startYyjs.Value.Month, startYyjs.Value.Day, 0, 0, 0))
.WhereIF(hasYyjs, p => p.Yyjs <= new DateTime(endYyjs.Value.Year, endYyjs.Value.Month, endYyjs.Value.Day, 23, 59, 59))
.WhereIF(!string.IsNullOrEmpty(input.F_Status), p => p.F_Status.Equals(input.F_Status))
.Select(it => new LqYyjlListOutput
{
id = it.Id,
djmd = it.Djmd,
yyr = it.Yyr,
gklx = it.Gklx,
yytyxm = it.Yytyxm,
czr = it.Czr,
czsj = it.Czsj,
gk = it.Gk,
gkxm = it.Gkxm,
yyjks = it.Yyjks,
yysj = it.Yysj,
yyjs = it.Yyjs,
F_Status = it.F_Status,
yyrName = SqlFunc.Subqueryable().Where(u => u.Id == it.Yyr).Select(u => u.RealName),
yyjksName = SqlFunc.Subqueryable().Where(u => u.Id == it.Yyjks).Select(u => u.RealName),
djmdName = SqlFunc.Subqueryable().Where(u => u.Id == it.Djmd).Select(u => u.Dm),
NoDealRemark = it.NoDealRemark,
InviteId = it.InviteId,
InviteTime = SqlFunc.Subqueryable().Where(u => u.Id == it.InviteId).Select(u => SqlFunc.ToDate(u.Yysj)),
roomId = it.RoomId,
roomName = it.RoomName,
serviceStartTime = it.ServiceStartTime,
serviceEndTime = it.ServiceEndTime,
remark = it.Remark,
consumeId = it.ConsumeId,
hasEffectiveConsume = SqlFunc.Subqueryable()
.Where(h => h.AppointmentId == it.Id && h.IsEffective == StatusEnum.有效.GetHashCode())
.Any(),
})
.MergeTable()
.OrderBy(sidx + " " + input.sort)
.ToPagedListAsync(input.currentPage, input.pageSize);
await FillYyjlProjectSummaryAsync(data.list);
await FillYyjlConsumeIdAsync(data.list);
return PageResult.SqlSugarPageResult(data);
}
///
/// 填充列表行的预约计划品项摘要
///
private async Task FillYyjlProjectSummaryAsync(IEnumerable rows)
{
if (rows == null || !rows.Any()) return;
var ids = rows.Select(x => x.id).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
if (ids.Count == 0) return;
var pxList = await _db.Queryable()
.Where(x => ids.Contains(x.YyjlId) && x.IsEffective == 1)
.OrderBy(x => x.SortNo)
.OrderBy(x => x.CreateTime)
.ToListAsync();
var summaryMap = pxList
.GroupBy(x => x.YyjlId)
.ToDictionary(
g => g.Key,
g => string.Join("、", g.Select(FormatYyjlPxSummary).Where(s => !string.IsNullOrEmpty(s))));
foreach (var row in rows)
{
if (summaryMap.TryGetValue(row.id, out var summary) && !string.IsNullOrWhiteSpace(summary))
row.projectSummary = summary;
}
}
private static string FormatYyjlPxSummary(LqYyjlPxEntity px)
{
if (px == null) return string.Empty;
var name = (px.Pxmc ?? string.Empty).Trim();
if (string.IsNullOrEmpty(name)) return string.Empty;
var num = px.ProjectNumber.HasValue && px.ProjectNumber.Value > 0
? (int)px.ProjectNumber.Value
: 1;
return $"{name}×{num}";
}
///
/// 列表行补全/校验关联耗卡编号(剔除无效 ConsumeId,并按 AppointmentId 反查)
///
private async Task FillYyjlConsumeIdAsync(IEnumerable rows)
{
if (rows == null || !rows.Any()) return;
var rowList = rows as IList ?? rows.ToList();
var listedConsumeIds = rowList
.Where(r => !string.IsNullOrEmpty(r.consumeId))
.Select(r => r.consumeId)
.Distinct()
.ToList();
if (listedConsumeIds.Count > 0)
{
var existingConsumeIds = await _db.Queryable()
.Where(x => listedConsumeIds.Contains(x.Id))
.Select(x => x.Id)
.ToListAsync();
var existingSet = new HashSet(existingConsumeIds);
foreach (var row in rowList)
{
if (!string.IsNullOrEmpty(row.consumeId) && !existingSet.Contains(row.consumeId))
row.consumeId = null;
}
}
var needAppointmentIds = rowList
.Where(r => string.IsNullOrEmpty(r.consumeId) && IsTransferredConsumeStatus(r.F_Status))
.Select(r => r.id)
.Where(id => !string.IsNullOrEmpty(id))
.Distinct()
.ToList();
if (needAppointmentIds.Count == 0) return;
var consumeRows = await _db.Queryable()
.Where(x => needAppointmentIds.Contains(x.AppointmentId))
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.Select(x => new { x.Id, x.AppointmentId, x.IsEffective })
.ToListAsync();
var consumeMap = consumeRows
.GroupBy(x => x.AppointmentId)
.ToDictionary(
g => g.Key,
g =>
{
var effective = g.FirstOrDefault(x => x.IsEffective == StatusEnum.有效.GetHashCode());
return (effective ?? g.First()).Id;
});
foreach (var row in rowList)
{
if (!string.IsNullOrEmpty(row.consumeId)) continue;
if (consumeMap.TryGetValue(row.id, out var consumeId))
row.consumeId = consumeId;
}
}
///
/// 预约详情:校验 ConsumeId 是否真实存在,并合并关联耗卡列表
///
private async Task ResolveYyjlRelatedConsumeAsync(LqYyjlInfoOutput output, LqYyjlEntity entity)
{
var related = output.relatedConsumeList ?? new List();
var relatedIds = new HashSet(related.Select(x => x.id));
if (!string.IsNullOrEmpty(entity.ConsumeId))
{
var linked = await _db.Queryable()
.Where(x => x.Id == entity.ConsumeId)
.FirstAsync();
if (linked != null)
{
if (!relatedIds.Contains(linked.Id))
{
related.Insert(0, new LqYyjlRelatedConsumeOutput
{
id = linked.Id,
hksj = linked.Hksj,
xfje = linked.Xfje,
hymc = linked.Hymc,
});
}
output.consumeId = linked.Id;
}
else
{
output.consumeId = null;
}
}
else if (related.Count > 0)
{
output.consumeId = related[0].id;
}
output.relatedConsumeList = related;
}
private static bool IsTransferredConsumeStatus(string status)
{
var s = (status ?? string.Empty).Trim();
return s == LqYyjlStatusTexts.TransferredToConsume || s == LqYyjlStatusTexts.Completed;
}
///
/// 解析「时间戳,时间戳」范围参数,非法值(空/NaN/非数字)返回 (null,null)。
///
private static (DateTime? start, DateTime? end) TryParseTsRange(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return (null, null);
var parts = raw.Split(',');
if (parts.Length < 2) return (null, null);
var s = (parts[0] ?? string.Empty).Trim();
var e = (parts[parts.Length - 1] ?? string.Empty).Trim();
if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(e)) return (null, null);
if (!long.TryParse(s, out _) || !long.TryParse(e, out _)) return (null, null);
try
{
return (Ext.GetDateTime(s), Ext.GetDateTime(e));
}
catch
{
return (null, null);
}
}
#endregion
#region 新建预约记录
///
/// 新建预约记录
///
/// 参数
///
[HttpPost("")]
public async Task Create([FromBody] LqYyjlCrInput input)
{
var entity = input.Adapt();
entity.Id = YitIdHelper.NextId().ToString();
entity.Czr = _userManager.UserId;
entity.Czsj = DateTime.Now;
entity.CreateTime = DateTime.Now;
ApplyPrimaryYyjksFromFirstPxJksList(entity, input.lqYyjlPxList);
if (string.IsNullOrWhiteSpace(entity.F_Status))
entity.F_Status = LqYyjlStatusTexts.Booked;
//判断沟通记录是否存在
if (!string.IsNullOrEmpty(input.InviteId))
{
var invite = await _db.Queryable().Where(u => u.Id == input.InviteId).FirstAsync();
if (invite == null)
{
throw NCCException.Oh("沟通记录不存在");
}
}
if (!string.IsNullOrEmpty(entity.RoomId) && !string.IsNullOrEmpty(entity.Djmd))
{
var room = await _db.Queryable()
.Where(r => r.Id == entity.RoomId && r.StoreId == entity.Djmd && r.Enabled == 1)
.FirstAsync();
entity.RoomName = room?.RoomName ?? entity.RoomName;
}
try
{
_db.BeginTran();
var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync();
if (!(isOk > 0))
throw NCCException.Oh(ErrorCode.COM1000);
await InsertEffectiveYyjlPxListAsync(entity.Id, input.lqYyjlPxList);
_db.CommitTran();
}
catch (Exception)
{
_db.RollbackTran();
throw;
}
}
#endregion
#region 预约记录无分页列表
///
/// 获取预约记录无分页列表
///
/// 请求参数
///
[NonAction]
public async Task GetNoPagingList([FromQuery] LqYyjlListQueryInput input)
{
var sidx = input.sidx == null ? "id" : input.sidx;
var (startCzsj, endCzsj) = TryParseTsRange(input.czsj);
var (startYysj, endYysj) = TryParseTsRange(input.yysj);
var (startYyjs, endYyjs) = TryParseTsRange(input.yyjs);
var hasCzsj = startCzsj.HasValue && endCzsj.HasValue;
var hasYysj = startYysj.HasValue && endYysj.HasValue;
var hasYyjs = startYyjs.HasValue && endYyjs.HasValue;
var data = await _db.Queryable()
.WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
.WhereIF(!string.IsNullOrEmpty(input.djmd), p => p.Djmd.Equals(input.djmd))
.WhereIF(!string.IsNullOrEmpty(input.yyr), p => p.Yyr.Equals(input.yyr))
.WhereIF(!string.IsNullOrEmpty(input.gklx), p => p.Gklx.Contains(input.gklx))
.WhereIF(!string.IsNullOrEmpty(input.yytyxm), p => p.Yytyxm.Equals(input.yytyxm))
.WhereIF(!string.IsNullOrEmpty(input.czr), p => p.Czr.Equals(input.czr))
.WhereIF(hasCzsj, p => p.Czsj >= new DateTime(startCzsj.Value.Year, startCzsj.Value.Month, startCzsj.Value.Day, 0, 0, 0))
.WhereIF(hasCzsj, p => p.Czsj <= new DateTime(endCzsj.Value.Year, endCzsj.Value.Month, endCzsj.Value.Day, 23, 59, 59))
.WhereIF(!string.IsNullOrEmpty(input.gk), p => p.Gk.Equals(input.gk))
.WhereIF(!string.IsNullOrEmpty(input.gkxm), p => p.Gkxm.Contains(input.gkxm))
.WhereIF(!string.IsNullOrEmpty(input.yyjks), p => p.Yyjks.Equals(input.yyjks))
.WhereIF(hasYysj, p => p.Yysj >= new DateTime(startYysj.Value.Year, startYysj.Value.Month, startYysj.Value.Day, 0, 0, 0))
.WhereIF(hasYysj, p => p.Yysj <= new DateTime(endYysj.Value.Year, endYysj.Value.Month, endYysj.Value.Day, 23, 59, 59))
.WhereIF(hasYyjs, p => p.Yyjs >= new DateTime(startYyjs.Value.Year, startYyjs.Value.Month, startYyjs.Value.Day, 0, 0, 0))
.WhereIF(hasYyjs, p => p.Yyjs <= new DateTime(endYyjs.Value.Year, endYyjs.Value.Month, endYyjs.Value.Day, 23, 59, 59))
.WhereIF(!string.IsNullOrEmpty(input.F_Status), p => p.F_Status.Equals(input.F_Status))
.Select(it => new LqYyjlListOutput
{
id = it.Id,
djmd = it.Djmd,
yyr = it.Yyr,
gklx = it.Gklx,
yytyxm = it.Yytyxm,
czr = it.Czr,
czsj = it.Czsj,
gk = it.Gk,
gkxm = it.Gkxm,
yyjks = it.Yyjks,
yysj = it.Yysj,
yyjs = it.Yyjs,
F_Status = it.F_Status,
yyrName = SqlFunc.Subqueryable().Where(u => u.Id == it.Yyr).Select(u => u.RealName),
yyjksName = SqlFunc.Subqueryable().Where(u => u.Id == it.Yyjks).Select(u => u.RealName),
djmdName = SqlFunc.Subqueryable().Where(u => u.Id == it.Djmd).Select(u => u.Dm),
})
.MergeTable()
.OrderBy(sidx + " " + input.sort)
.ToListAsync();
return data;
}
#endregion
#region 导出预约记录
///
/// 导出预约记录
///
/// 请求参数
///
[HttpGet("Actions/Export")]
public async Task Export([FromQuery] LqYyjlListQueryInput input)
{
var userInfo = await _userManager.GetUserInfo();
var exportData = new List();
if (input.dataType == 0)
{
var data = Clay.Object(await this.GetList(input));
exportData = data.Solidify>().list;
}
else
{
exportData = await this.GetNoPagingList(input);
}
List paramList =
"[{\"value\":\"预约编号\",\"field\":\"id\"},{\"value\":\"单据门店\",\"field\":\"djmd\"},{\"value\":\"沟通人\",\"field\":\"yyr\"},{\"value\":\"顾客姓名\",\"field\":\"gkxm\"},{\"value\":\"操作人\",\"field\":\"czr\"},{\"value\":\"操作时间\",\"field\":\"czsj\"},{\"value\":\"预约体验项目\",\"field\":\"yytyxm\"},{\"value\":\"顾客类型\",\"field\":\"gklx\"},{\"value\":\"预约健康师\",\"field\":\"yyjks\"},{\"value\":\"预约开始时间\",\"field\":\"yysj\"},{\"value\":\"预约结束时间\",\"field\":\"yyjs\"},{\"value\":\"顾客\",\"field\":\"gk\"},{\"value\":\"预约状态\",\"field\":\"F_Status\"},]".ToList();
ExcelConfig excelconfig = new ExcelConfig();
excelconfig.FileName = "预约记录.xls";
excelconfig.HeadFont = "微软雅黑";
excelconfig.HeadPoint = 10;
excelconfig.IsAllSizeColumn = true;
excelconfig.ColumnModel = new List();
List selectKeyList = input.selectKey.Split(',').ToList();
foreach (var item in selectKeyList)
{
var isExist = paramList.Find(p => p.field == item);
if (isExist != null)
{
excelconfig.ColumnModel.Add(new ExcelColumnModel() { Column = isExist.field, ExcelColumn = isExist.value });
}
}
var addPath = FileVariable.TemporaryFilePath + excelconfig.FileName;
ExcelExportHelper.Export(exportData, excelconfig, addPath);
var fileName = _userManager.UserId + "|" + addPath + "|xls";
var output = new { name = excelconfig.FileName, url = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC") };
return output;
}
#endregion
#region 批量删除预约记录
///
/// 批量删除预约记录
///
/// 主键数组
///
[HttpPost("batchRemove")]
public async Task BatchRemove([FromBody] List ids)
{
var entitys = await _db.Queryable().In(it => it.Id, ids).ToListAsync();
if (entitys.Count > 0)
{
try
{
//开启事务
_db.BeginTran();
//批量删除预约记录
await _db.Deleteable().In(d => d.Id, ids).ExecuteCommandAsync();
//关闭事务
_db.CommitTran();
}
catch (Exception)
{
//回滚事务
_db.RollbackTran();
throw NCCException.Oh(ErrorCode.COM1002);
}
}
}
#endregion
#region 更新预约记录
///
/// 更新预约记录
///
/// 主键
/// 参数
///
[HttpPut("{id}")]
public async Task Update(string id, [FromBody] LqYyjlUpInput input)
{
var existing = await _db.Queryable().FirstAsync(p => p.Id == id);
_ = existing ?? throw NCCException.Oh(ErrorCode.COM1005);
if (LqYyjlStatusHelper.IsBlockedForMasterEdit(existing.F_Status))
throw NCCException.Oh("当前预约状态不允许修改");
var entity = input.Adapt();
entity.Id = id;
ApplyPrimaryYyjksFromFirstPxJksList(entity, input.lqYyjlPxList);
if (!string.IsNullOrEmpty(entity.RoomId) && !string.IsNullOrEmpty(entity.Djmd))
{
var room = await _db.Queryable()
.Where(r => r.Id == entity.RoomId && r.StoreId == entity.Djmd && r.Enabled == 1)
.FirstAsync();
entity.RoomName = room?.RoomName ?? entity.RoomName;
}
try
{
_db.BeginTran();
var isOk = await _db.Updateable(entity).Where(x => x.Id == id).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
if (!(isOk > 0))
throw NCCException.Oh(ErrorCode.COM1001);
await _db.Updateable()
.SetColumns(x => new LqYyjlPxJksEntity { IsEffective = 0, LastModifyTime = DateTime.Now })
.Where(x => x.YyjlId == id && x.IsEffective == 1)
.ExecuteCommandAsync();
await _db.Updateable()
.SetColumns(x => new LqYyjlPxEntity { IsEffective = 0, LastModifyTime = DateTime.Now })
.Where(x => x.YyjlId == id && x.IsEffective == 1)
.ExecuteCommandAsync();
await InsertEffectiveYyjlPxListAsync(id, input.lqYyjlPxList);
_db.CommitTran();
}
catch (Exception)
{
_db.RollbackTran();
throw;
}
}
#endregion
#region 删除预约记录
///
/// 删除预约记录
///
///
[HttpDelete("{id}")]
public async Task Delete(string id)
{
var entity = await _db.Queryable().FirstAsync(p => p.Id == id);
_ = entity ?? throw NCCException.Oh(ErrorCode.COM1005);
var isOk = await _db.Deleteable().Where(d => d.Id == id).ExecuteCommandAsync();
if (!(isOk > 0))
throw NCCException.Oh(ErrorCode.COM1002);
}
#endregion
#region 添加未成交说明
///
/// 添加未成交说明
///
/// 参数
///
[HttpPost("AddNoDealRemark")]
public async Task AddNoDealRemark([FromBody] LqYyjlAddNoDealRemarkInput input)
{
var entity = await _db.Queryable().FirstAsync(p => p.Id == input.id);
_ = entity ?? throw NCCException.Oh(ErrorCode.COM1005);
entity.NoDealRemark = input.noDealRemark;
var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
if (!(isOk > 0))
throw NCCException.Oh(ErrorCode.COM1001);
}
#endregion
#region 预约动作(开始服务 / 取消 / 完成)
///
/// 开始服务:已预约 → 服务中,写入服务开始时间。
///
/// 预约主键
[HttpPost("{id}/StartService")]
public async Task StartService(string id)
{
var yyjl = await _db.Queryable().FirstAsync(p => p.Id == id);
_ = yyjl ?? throw NCCException.Oh(ErrorCode.COM1005);
if (!LqYyjlStatusHelper.CanStartService(yyjl.F_Status))
throw NCCException.Oh("当前预约状态不允许开始服务");
yyjl.F_Status = LqYyjlStatusTexts.InService;
yyjl.ServiceStartTime = DateTime.Now;
var ok = await _db.Updateable(yyjl)
.UpdateColumns(x => new { x.F_Status, x.ServiceStartTime })
.ExecuteCommandAsync();
if (!(ok > 0))
throw NCCException.Oh(ErrorCode.COM1001);
}
///
/// 取消预约:已预约 / 服务中 → 已取消;已转耗卡需先作废耗卡;已完成不可取消。
///
/// 预约主键
/// 可选未成交说明
[HttpPost("{id}/Cancel")]
public async Task Cancel(string id, [FromBody] LqYyjlCancelInput input)
{
var yyjl = await _db.Queryable().FirstAsync(p => p.Id == id);
_ = yyjl ?? throw NCCException.Oh(ErrorCode.COM1005);
var normalized = LqYyjlStatusHelper.NormalizeStored(yyjl.F_Status);
if (normalized == LqYyjlStatusTexts.TransferredToConsume)
throw NCCException.Oh("请先作废耗卡");
if (normalized == LqYyjlStatusTexts.Completed)
throw NCCException.Oh("已完成不可取消");
if (!LqYyjlStatusHelper.CanCancel(yyjl.F_Status))
throw NCCException.Oh("当前预约状态不允许取消");
yyjl.F_Status = LqYyjlStatusTexts.Cancelled;
if (!string.IsNullOrWhiteSpace(input?.noDealRemark))
yyjl.NoDealRemark = input.noDealRemark;
var ok = await _db.Updateable(yyjl)
.UpdateColumns(x => new { x.F_Status, x.NoDealRemark })
.ExecuteCommandAsync();
if (!(ok > 0))
throw NCCException.Oh(ErrorCode.COM1001);
}
///
/// 完成预约:已转耗卡 → 已完成;要求已有关联有效耗卡。
///
/// 预约主键
[HttpPost("{id}/Complete")]
public async Task Complete(string id)
{
var yyjl = await _db.Queryable().FirstAsync(p => p.Id == id);
_ = yyjl ?? throw NCCException.Oh(ErrorCode.COM1005);
if (!LqYyjlStatusHelper.CanComplete(yyjl.F_Status))
throw NCCException.Oh("当前预约状态不允许完成");
if (string.IsNullOrWhiteSpace(yyjl.ConsumeId))
throw NCCException.Oh("未关联有效耗卡,无法完成");
var consumeOk = await _db.Queryable()
.AnyAsync(x => x.Id == yyjl.ConsumeId && x.IsEffective == StatusEnum.有效.GetHashCode());
if (!consumeOk)
throw NCCException.Oh("关联耗卡无效或已作废,无法完成");
yyjl.F_Status = LqYyjlStatusTexts.Completed;
var ok = await _db.Updateable(yyjl).UpdateColumns(x => new { x.F_Status }).ExecuteCommandAsync();
if (!(ok > 0))
throw NCCException.Oh(ErrorCode.COM1001);
}
#endregion
#region 门店排班一览
///
/// 门店员工排班:按门店与预约开始时间范围返回预约块(含多健康师、品项摘要)
///
/// 门店ID
/// 预约开始时间范围,格式:毫秒时间戳,毫秒时间戳
/// 排班预约列表
[HttpGet("StoreSchedule")]
public async Task> GetStoreSchedule([FromQuery] string djmd, [FromQuery] string yysj)
{
if (string.IsNullOrWhiteSpace(djmd))
{
throw NCCException.Oh("门店ID不能为空");
}
var (startYysj, endYysj) = TryParseTsRange(yysj);
if (!startYysj.HasValue || !endYysj.HasValue)
{
throw NCCException.Oh("预约时间范围无效,请使用毫秒时间戳,毫秒时间戳");
}
var dayStart = new DateTime(startYysj.Value.Year, startYysj.Value.Month, startYysj.Value.Day, 0, 0, 0);
var dayEnd = new DateTime(endYysj.Value.Year, endYysj.Value.Month, endYysj.Value.Day, 23, 59, 59);
var entities = await _db.Queryable()
.Where(x => x.Djmd == djmd)
.Where(x => x.F_Status != "已取消")
.Where(x => x.Yysj >= dayStart && x.Yysj <= dayEnd)
.OrderBy(x => x.Yysj)
.ToListAsync();
if (entities == null || entities.Count == 0)
{
return new List();
}
var yyjlIds = entities.Select(x => x.Id).Distinct().ToList();
var jksEntities = await _db.Queryable()
.Where(x => yyjlIds.Contains(x.YyjlId) && x.IsEffective == 1)
.OrderBy(x => x.SortNo)
.ToListAsync();
var jksByYyjl = jksEntities
.GroupBy(x => x.YyjlId)
.ToDictionary(g => g.Key, g => g.ToList());
var pxEntities = await _db.Queryable()
.Where(x => yyjlIds.Contains(x.YyjlId) && x.IsEffective == 1)
.OrderBy(x => x.SortNo)
.ToListAsync();
var pxByYyjl = pxEntities
.GroupBy(x => x.YyjlId)
.ToDictionary(g => g.Key, g => g.ToList());
var coachIdSet = new HashSet(StringComparer.Ordinal);
foreach (var j in jksEntities)
{
if (!string.IsNullOrWhiteSpace(j.Jks)) coachIdSet.Add(j.Jks);
}
foreach (var e in entities)
{
if (!string.IsNullOrWhiteSpace(e.Yyjks)) coachIdSet.Add(e.Yyjks);
}
var coachNameMap = new Dictionary(StringComparer.Ordinal);
if (coachIdSet.Count > 0)
{
var coachIds = coachIdSet.ToList();
var coaches = await _db.Queryable()
.Where(u => coachIds.Contains(u.Id))
.Select(u => new { u.Id, u.RealName })
.ToListAsync();
coachNameMap = coaches.ToDictionary(k => k.Id, v => v.RealName ?? v.Id);
}
var result = new List();
foreach (var entity in entities)
{
var therapistIds = new List();
var therapistNames = new List();
if (jksByYyjl.TryGetValue(entity.Id, out var jksList) && jksList != null && jksList.Count > 0)
{
foreach (var j in jksList)
{
if (string.IsNullOrWhiteSpace(j.Jks)) continue;
if (!therapistIds.Contains(j.Jks))
{
therapistIds.Add(j.Jks);
var nm = !string.IsNullOrWhiteSpace(j.Jksxm)
? j.Jksxm
: (coachNameMap.TryGetValue(j.Jks, out var cn) ? cn : j.Jks);
therapistNames.Add(nm);
}
}
}
if (therapistIds.Count == 0 && !string.IsNullOrWhiteSpace(entity.Yyjks))
{
therapistIds.Add(entity.Yyjks);
therapistNames.Add(coachNameMap.TryGetValue(entity.Yyjks, out var cn) ? cn : entity.Yyjks);
}
var itemLabels = new List();
if (pxByYyjl.TryGetValue(entity.Id, out var pxList) && pxList != null)
{
foreach (var px in pxList)
{
var pn = px.ProjectNumber > 0 ? px.ProjectNumber : 1;
var name = string.IsNullOrWhiteSpace(px.Pxmc) ? px.Px : px.Pxmc;
if (!string.IsNullOrWhiteSpace(name))
{
itemLabels.Add($"{name}×{pn}");
}
}
}
result.Add(new LqYyjlScheduleItemOutput
{
id = entity.Id,
gk = entity.Gk,
gkxm = entity.Gkxm,
yysj = entity.Yysj,
yyjs = entity.Yyjs,
F_Status = entity.F_Status,
roomId = entity.RoomId,
roomName = entity.RoomName,
remark = entity.Remark,
therapistIds = therapistIds,
therapistNames = therapistNames,
itemLabels = itemLabels
});
}
return result;
}
#endregion
#region 我的今日预约
///
/// 获取我的今日预约
///
///
/// 查询当前登录用户今天预约的客户列表
///
/// 查询逻辑:
/// 1. 查询当前用户(yyr字段)的预约记录
/// 2. 筛选预约开始时间(yysj)在今天范围内的记录
/// 3. 关联查询客户、健康师、门店等信息
/// 4. 按预约时间排序返回
///
/// 今日预约客户列表
/// 查询成功
/// 服务器错误
[HttpGet("GetMyTodayAppointments")]
public async Task> GetMyTodayAppointmentsAsync()
{
try
{
var currentUserId = _userManager.UserId;
var today = DateTime.Now.Date;
// 查询当前用户今天的预约记录
var appointments = await _db.Queryable()
.Where(x => x.Yyr == currentUserId)
.Where(x => x.Yysj != null && x.Yysj.Value.Date == today)
.OrderBy(x => x.Yysj)
.ToListAsync();
if (!appointments.Any())
{
return new List();
}
// 获取门店信息
var storeIds = appointments.Where(x => !string.IsNullOrEmpty(x.Djmd)).Select(x => x.Djmd).Distinct().ToList();
var stores = new Dictionary();
if (storeIds.Any())
{
var storeList = await _db.Queryable()
.Where(x => storeIds.Contains(x.Id))
.Select(x => new { x.Id, x.Dm })
.ToListAsync();
stores = storeList.ToDictionary(k => k.Id, v => v.Dm);
}
// 获取健康师信息
var healthCoachIds = appointments.Where(x => !string.IsNullOrEmpty(x.Yyjks)).Select(x => x.Yyjks).Distinct().ToList();
var healthCoaches = new Dictionary();
if (healthCoachIds.Any())
{
var healthCoachList = await _db.Queryable()
.Where(x => healthCoachIds.Contains(x.Id))
.Select(x => new { x.Id, x.RealName })
.ToListAsync();
healthCoaches = healthCoachList.ToDictionary(k => k.Id, v => v.RealName);
}
// 构建返回结果
var result = appointments.Select(x =>
{
var startTime = x.Yysj?.ToString("HH:mm") ?? "";
var endTime = x.Yyjs?.ToString("HH:mm") ?? "";
var timeDisplay = !string.IsNullOrEmpty(endTime) ? $"{startTime}-{endTime}" : startTime;
return new MyTodayAppointmentOutput
{
AppointmentId = x.Id,
CustomerId = x.Gk,
CustomerName = x.Gkxm,
CustomerType = x.Gklx,
ProjectName = x.Yytyxm,
AppointmentStartTime = x.Yysj,
AppointmentEndTime = x.Yyjs,
HealthCoachId = x.Yyjks,
HealthCoachName = !string.IsNullOrEmpty(x.Yyjks) && healthCoaches.ContainsKey(x.Yyjks) ? healthCoaches[x.Yyjks] : null,
Status = x.F_Status,
StoreId = x.Djmd,
StoreName = !string.IsNullOrEmpty(x.Djmd) && stores.ContainsKey(x.Djmd) ? stores[x.Djmd] : null,
AppointmentTimeDisplay = timeDisplay
};
}).ToList();
return result;
}
catch (Exception ex)
{
throw NCCException.Oh($"获取我的今日预约失败:{ex.Message}");
}
}
#endregion
///
/// 若首行品项存在计划健康师列表,则同步主表预约健康师为首条(兼容旧端:无列表则不覆盖)。
///
private static void ApplyPrimaryYyjksFromFirstPxJksList(LqYyjlEntity entity, List pxRows)
{
if (pxRows == null || pxRows.Count == 0)
return;
var row0 = pxRows[0];
if (row0?.jksList == null || row0.jksList.Count == 0)
return;
var head = row0.jksList[0];
if (!string.IsNullOrWhiteSpace(head?.jks))
entity.Yyjks = head.jks;
}
///
/// 插入预约计划品项(仅包含有品项编码的行,标记有效);同步写入品项-计划健康师子表。
///
private async Task InsertEffectiveYyjlPxListAsync(string yyjlId, List rows)
{
if (rows == null || !rows.Any())
return;
var now = DateTime.Now;
var pxList = new List();
var jksAll = new List();
foreach (var row in rows.Where(r => r != null && !string.IsNullOrWhiteSpace(r.px)))
{
if (row.jksList != null && row.jksList.Any())
{
if (row.jksList.Select(x => x.jks).Distinct().Count() != row.jksList.Count)
throw NCCException.Oh("同一品项不可重复添加同一健康师");
}
var pxId = YitIdHelper.NextId().ToString();
LqYyjlPxJksCrInput firstPlan = null;
if (row.jksList != null && row.jksList.Count > 0)
firstPlan = row.jksList[0];
pxList.Add(
new LqYyjlPxEntity
{
Id = pxId,
YyjlId = yyjlId,
Px = row.px,
Pxmc = row.pxmc,
Pxjg = row.pxjg,
ProjectNumber = row.projectNumber,
SourceType = row.sourceType,
BillingItemId = row.billingItemId,
Jks = firstPlan != null ? firstPlan.jks : row.jks,
Jksxm = firstPlan != null ? firstPlan.jksxm : row.jksxm,
Remark = row.remark,
SortNo = row.sortNo,
IsEffective = 1,
CreateTime = now,
});
if (row.jksList != null && row.jksList.Count > 0)
{
for (var i = 0; i < row.jksList.Count; i++)
{
var j = row.jksList[i];
if (string.IsNullOrWhiteSpace(j.jks))
throw NCCException.Oh("健康师不能为空");
jksAll.Add(
new LqYyjlPxJksEntity
{
Id = YitIdHelper.NextId().ToString(),
YyjlId = yyjlId,
YyjlPxId = pxId,
Jks = j.jks,
Jksxm = j.jksxm,
SortNo = j.sortNo != 0 ? j.sortNo : i,
IsEffective = 1,
CreateTime = now,
});
}
}
}
if (pxList.Any())
await _db.Insertable(pxList).ExecuteCommandAsync();
if (jksAll.Any())
await _db.Insertable(jksAll).ExecuteCommandAsync();
}
}
}