API 查詢同步改寫 .Contains()、.OrderBy()、複雜 GroupBy/Math.Round,必要時 materialize 或加 HasValue。 Participation rate / kind breakdown 改在記憶體計算,同時檢查整數陣列 .Contains() 的型別安全性。
1160 lines
49 KiB
C#
1160 lines
49 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Web.Http;
|
||
using PagedList;
|
||
using Newtonsoft.Json;
|
||
using System.Collections;
|
||
using System.Data.Entity;
|
||
|
||
// api/pivot
|
||
[ezAuthorize]
|
||
public class pivotController : ApiController
|
||
{
|
||
private Model.ezEntities _db = new Model.ezEntities();
|
||
|
||
#region 報表查詢 API
|
||
|
||
/// <summary>
|
||
/// 報名明細查詢 - 對應 Excel 中的視圖結構
|
||
/// GET api/pivot/registration_details
|
||
/// </summary>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="activityNum">法會編號</param>
|
||
/// <param name="followerNum">信眾編號</param>
|
||
/// <param name="page">頁碼</param>
|
||
/// <param name="pageSize">每頁筆數</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/registration_details")]
|
||
public IHttpActionResult GetRegistrationDetails(string startDate = null, string endDate = null,
|
||
int? activityNum = null, int? followerNum = null, int page = 1, int pageSize = 1000)
|
||
{
|
||
try
|
||
{
|
||
// 對應 README.md 中的視圖查詢結構,按顏色分類欄位
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
select new
|
||
{
|
||
// 🟠 橘色欄位 - 法會基礎資料 (法會相關的直接來源)
|
||
法會ID = act.num,
|
||
法會名稱 = act.subject,
|
||
開始日期 = act.startDate_solar,
|
||
結束日期 = act.endDate_solar,
|
||
|
||
// 🔵 藍色欄位 - 信眾基礎資料 (信眾相關的直接來源)
|
||
信眾編號 = f.f_number,
|
||
信眾姓名 = f.u_name,
|
||
報名編號 = po.order_no,
|
||
報名日期 = po.up_time,
|
||
|
||
// 🟢綠色欄位 - 功德資訊欄位 (功德相關資訊)
|
||
功德主 = pod.parent_num != null ? "是" : "否",
|
||
功德類型 = aik.kind,
|
||
功德名稱 = ai.subject,
|
||
|
||
// 🟣 紫色欄位 - 計算欄位 (需要計算)
|
||
數量 = pod.qty,
|
||
金額 = pod.price,
|
||
已收 = 0, // 業務邏輯:固定為 0
|
||
未收 = pod.price * pod.qty - 0, // 計算未收金額
|
||
|
||
// 額外提供原始資料供進階計算
|
||
_metadata = new
|
||
{
|
||
parent_num = pod.parent_num,
|
||
unit_price = pod.price,
|
||
quantity = pod.qty,
|
||
total_amount = pod.price * pod.qty,
|
||
kind_num = aik.num,
|
||
item_num = ai.num,
|
||
activity_num = act.num,
|
||
follower_num = f.num
|
||
}
|
||
};
|
||
|
||
// 日期篩選
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.開始日期 >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.結束日期 <= end);
|
||
}
|
||
|
||
// 法會篩選
|
||
if (activityNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.法會ID == activityNum.Value);
|
||
}
|
||
|
||
// 信眾篩選
|
||
if (followerNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.信眾編號 == followerNum.ToString());
|
||
}
|
||
|
||
// 分頁處理
|
||
var totalCount = query.Count();
|
||
var pagedQuery = query.OrderByDescending(x => x.報名日期)
|
||
.ThenByDescending(x => x.法會ID)
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.ToList();
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
list = pagedQuery,
|
||
total = totalCount,
|
||
page = page,
|
||
pageSize = pageSize,
|
||
totalPages = (int)Math.Ceiling((double)totalCount / pageSize)
|
||
},
|
||
message = "查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 報名明細查詢 - Excel 匯出格式
|
||
/// GET api/pivot/registration_details_export
|
||
/// </summary>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="activityNum">法會編號</param>
|
||
/// <param name="followerNum">信眾編號</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/registration_details_export")]
|
||
public IHttpActionResult GetRegistrationDetailsForExport(string startDate = null, string endDate = null,
|
||
int? activityNum = null, int? followerNum = null)
|
||
{
|
||
try
|
||
{
|
||
// 完全對應 Excel 中的欄位結構
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
select new
|
||
{
|
||
// 完全對應 Excel 欄位名稱
|
||
法會ID = act.num,
|
||
法會名稱 = act.subject,
|
||
開始日期 = act.startDate_solar,
|
||
結束日期 = act.endDate_solar,
|
||
信眾編號 = f.f_number,
|
||
信眾姓名 = f.u_name,
|
||
報名編號 = po.order_no,
|
||
報名日期 = po.up_time,
|
||
功德主 = pod.parent_num != null ? "是" : "否",
|
||
功德類型 = aik.kind,
|
||
功德名稱 = ai.subject,
|
||
數量 = pod.qty,
|
||
金額 = pod.price,
|
||
已收 = 0,
|
||
未收 = pod.price * pod.qty
|
||
};
|
||
|
||
// 套用篩選條件
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.開始日期 >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.結束日期 <= end);
|
||
}
|
||
if (activityNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.法會ID == activityNum.Value);
|
||
}
|
||
if (followerNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.信眾編號 == followerNum.ToString());
|
||
}
|
||
|
||
var exportData = query.OrderByDescending(x => x.報名日期)
|
||
.ThenByDescending(x => x.法會ID)
|
||
.ToList()
|
||
.Select(x => new
|
||
{
|
||
x.法會ID,
|
||
x.法會名稱,
|
||
開始日期 = x.開始日期.HasValue ? x.開始日期.Value.ToString("yyyy/MM/dd") : "",
|
||
結束日期 = x.結束日期.HasValue ? x.結束日期.Value.ToString("yyyy/MM/dd") : "",
|
||
x.信眾編號,
|
||
x.信眾姓名,
|
||
x.報名編號,
|
||
報名日期 = x.報名日期.HasValue ? x.報名日期.Value.ToString("yyyy/MM/dd HH:mm") : "",
|
||
x.功德主,
|
||
x.功德類型,
|
||
x.功德名稱,
|
||
x.數量,
|
||
x.金額,
|
||
x.已收,
|
||
x.未收
|
||
})
|
||
.ToList();
|
||
|
||
// 統計資訊
|
||
var summary = new
|
||
{
|
||
總筆數 = exportData.Count,
|
||
總金額 = exportData.Sum(x => x.金額 * x.數量),
|
||
參與法會數 = exportData.Select(x => x.法會ID).Distinct().Count(),
|
||
參與信眾數 = exportData.Select(x => x.信眾編號).Distinct().Count(),
|
||
功德主數量 = exportData.Count(x => x.功德主 == "是")
|
||
};
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
details = exportData,
|
||
summary = summary,
|
||
export_time = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"),
|
||
filters = new
|
||
{
|
||
startDate,
|
||
endDate,
|
||
activityNum,
|
||
followerNum
|
||
}
|
||
},
|
||
message = "匯出資料準備完成"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"匯出失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取得法會報名統計
|
||
/// GET api/pivot/activity_stats
|
||
/// </summary>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="activityNum">法會編號</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/activity_stats")]
|
||
public IHttpActionResult GetActivityStats(string startDate = null, string endDate = null, int? activityNum = null)
|
||
{
|
||
try
|
||
{
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
select new
|
||
{
|
||
ActivityNum = act.num,
|
||
ActivityName = act.subject,
|
||
StartDate = act.startDate_solar,
|
||
EndDate = act.endDate_solar,
|
||
FollowerNum = f.num,
|
||
FollowerName = f.u_name,
|
||
OrderNo = po.order_no,
|
||
OrderDate = po.up_time,
|
||
KindName = aik.kind,
|
||
ItemName = ai.subject,
|
||
Qty = pod.qty,
|
||
Price = pod.price,
|
||
Amount = pod.qty * pod.price
|
||
};
|
||
|
||
// 日期篩選
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.StartDate >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.EndDate <= end);
|
||
}
|
||
|
||
// 法會篩選
|
||
if (activityNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.ActivityNum == activityNum.Value);
|
||
}
|
||
|
||
// 統計資料
|
||
var stats = query.GroupBy(x => new { x.ActivityNum, x.ActivityName, x.StartDate, x.EndDate })
|
||
.Select(g => new
|
||
{
|
||
activity_num = g.Key.ActivityNum,
|
||
activity_name = g.Key.ActivityName,
|
||
start_date = g.Key.StartDate,
|
||
end_date = g.Key.EndDate,
|
||
total_orders = g.Select(x => x.OrderNo).Distinct().Count(),
|
||
total_followers = g.Select(x => x.FollowerNum).Distinct().Count(),
|
||
total_amount = g.Sum(x => x.Amount),
|
||
total_qty = g.Sum(x => x.Qty),
|
||
// ⚠️ 複雜聚合:巢狀 GroupBy → Average → Sum,EF 6.4.4 可轉換
|
||
// 如遇到 NotSupportedException,改為在 .ToList() 後計算
|
||
avg_amount_per_follower = g.GroupBy(x => x.FollowerNum).Average(fg => fg.Sum(x => x.Amount))
|
||
})
|
||
.OrderByDescending(x => x.start_date)
|
||
.ToList();
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = stats,
|
||
message = "查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取得信眾參與分析
|
||
/// GET api/pivot/follower_analysis
|
||
/// </summary>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="followerNum">信眾編號</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/follower_analysis")]
|
||
public IHttpActionResult GetFollowerAnalysis(string startDate = null, string endDate = null, int? followerNum = null)
|
||
{
|
||
try
|
||
{
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
select new
|
||
{
|
||
FollowerNum = f.num,
|
||
FollowerName = f.u_name,
|
||
FollowerCode = f.f_number,
|
||
ActivityNum = act.num,
|
||
ActivityName = act.subject,
|
||
StartDate = act.startDate_solar,
|
||
EndDate = act.endDate_solar,
|
||
OrderDate = po.up_time,
|
||
KindName = aik.kind,
|
||
ItemName = ai.subject,
|
||
Amount = pod.qty * pod.price,
|
||
HasParent = pod.parent_num != null
|
||
};
|
||
|
||
// 日期篩選
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.StartDate >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.EndDate <= end);
|
||
}
|
||
|
||
// 信眾篩選
|
||
if (followerNum.HasValue)
|
||
{
|
||
query = query.Where(x => x.FollowerNum == followerNum.Value);
|
||
}
|
||
|
||
// ✅ 優化:先取得總活動數,避免在投影中跨表查詢
|
||
var totalActivitiesCount = _db.activities.Count();
|
||
|
||
// 信眾統計分析
|
||
var followerStats = query.GroupBy(x => new { x.FollowerNum, x.FollowerName, x.FollowerCode })
|
||
.Select(g => new
|
||
{
|
||
follower_num = g.Key.FollowerNum,
|
||
follower_name = g.Key.FollowerName,
|
||
follower_code = g.Key.FollowerCode,
|
||
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
||
total_amount = g.Sum(x => x.Amount),
|
||
total_orders = g.Select(x => x.OrderDate).Count(),
|
||
// ⚠️ 複雜聚合:巢狀 GroupBy → Average → Sum,EF 6.4.4 可轉換
|
||
// 如遇到 NotSupportedException,改為在 .ToList() 後計算
|
||
avg_amount_per_activity = g.GroupBy(x => x.ActivityNum).Average(ag => ag.Sum(x => x.Amount)),
|
||
is_patron_count = g.Count(x => x.HasParent),
|
||
favorite_kinds = g.GroupBy(x => x.KindName)
|
||
.OrderByDescending(kg => kg.Sum(x => x.Amount))
|
||
.Take(3)
|
||
.Select(kg => new { kind = kg.Key, amount = kg.Sum(x => x.Amount) })
|
||
.ToList(),
|
||
recent_activities = g.OrderByDescending(x => x.OrderDate)
|
||
.Take(5)
|
||
.Select(x => new {
|
||
activity_name = x.ActivityName,
|
||
order_date = x.OrderDate,
|
||
amount = x.Amount
|
||
})
|
||
.ToList()
|
||
})
|
||
.OrderByDescending(x => x.total_amount)
|
||
.ToList()
|
||
// ✅ 優化:在記憶體中計算 participation_rate,避免 Math.Round 在 LINQ to Entities 投影中
|
||
.Select(x => new
|
||
{
|
||
x.follower_num,
|
||
x.follower_name,
|
||
x.follower_code,
|
||
x.total_activities,
|
||
x.total_amount,
|
||
x.total_orders,
|
||
x.avg_amount_per_activity,
|
||
x.is_patron_count,
|
||
participation_rate = Math.Round((double)x.total_activities / totalActivitiesCount * 100, 2),
|
||
x.favorite_kinds,
|
||
x.recent_activities
|
||
})
|
||
.ToList();
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = followerStats,
|
||
message = "查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取得收入統計
|
||
/// GET api/pivot/income_stats
|
||
/// </summary>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="groupBy">分組方式 (monthly/yearly/activity)</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/income_stats")]
|
||
public IHttpActionResult GetIncomeStats(string startDate = null, string endDate = null, string groupBy = "monthly")
|
||
{
|
||
try
|
||
{
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
select new
|
||
{
|
||
ActivityNum = act.num,
|
||
ActivityName = act.subject,
|
||
StartDate = act.startDate_solar,
|
||
OrderDate = po.up_time,
|
||
KindNum = aik.num,
|
||
KindName = aik.kind,
|
||
ItemName = ai.subject,
|
||
Amount = pod.qty * pod.price
|
||
};
|
||
|
||
// 日期篩選
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.StartDate >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.StartDate <= end);
|
||
}
|
||
|
||
object stats = null;
|
||
|
||
switch (groupBy?.ToLower())
|
||
{
|
||
case "monthly":
|
||
stats = query.GroupBy(x => new {
|
||
Year = x.StartDate.Value.Year,
|
||
Month = x.StartDate.Value.Month
|
||
})
|
||
.Select(g => new
|
||
{
|
||
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
||
year = g.Key.Year,
|
||
month = g.Key.Month,
|
||
total_amount = g.Sum(x => x.Amount),
|
||
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
||
kind_breakdown = g.GroupBy(x => x.KindName)
|
||
.Select(kg => new {
|
||
kind = kg.Key,
|
||
amount = kg.Sum(x => x.Amount)
|
||
}).ToList()
|
||
})
|
||
.OrderBy(x => x.year).ThenBy(x => x.month)
|
||
.ToList();
|
||
break;
|
||
|
||
case "yearly":
|
||
stats = query.GroupBy(x => x.StartDate.Value.Year)
|
||
.Select(g => new
|
||
{
|
||
period = g.Key.ToString(),
|
||
year = g.Key,
|
||
total_amount = g.Sum(x => x.Amount),
|
||
total_activities = g.Select(x => x.ActivityNum).Distinct().Count(),
|
||
kind_breakdown = g.GroupBy(x => x.KindName)
|
||
.Select(kg => new {
|
||
kind = kg.Key,
|
||
amount = kg.Sum(x => x.Amount)
|
||
}).ToList(),
|
||
monthly_trend = g.GroupBy(x => x.StartDate.Value.Month)
|
||
.Select(mg => new {
|
||
month = mg.Key,
|
||
amount = mg.Sum(x => x.Amount)
|
||
}).OrderBy(x => x.month).ToList()
|
||
})
|
||
.OrderBy(x => x.year)
|
||
.ToList();
|
||
break;
|
||
|
||
case "activity":
|
||
// ✅ 優化:一次性取出所有資料,避免 N+1 查詢
|
||
var activityData = query.ToList();
|
||
|
||
stats = activityData.GroupBy(x => new { x.ActivityNum, x.ActivityName, x.StartDate })
|
||
.Select(g => new
|
||
{
|
||
activity_num = g.Key.ActivityNum,
|
||
activity_name = g.Key.ActivityName,
|
||
start_date = g.Key.StartDate,
|
||
total_amount = g.Sum(x => x.Amount),
|
||
// ✅ 優化:在記憶體中分組,避免子查詢
|
||
kind_breakdown = g.GroupBy(x => x.KindName)
|
||
.Select(kg => new
|
||
{
|
||
kind = kg.Key,
|
||
amount = kg.Sum(x => x.Amount),
|
||
// ✅ 優化:在記憶體中計算百分比
|
||
percentage = g.Sum(x => x.Amount) > 0
|
||
? Math.Round((double)kg.Sum(x => x.Amount) / (double)g.Sum(x => x.Amount) * 100, 2)
|
||
: 0
|
||
})
|
||
.OrderByDescending(x => x.amount)
|
||
.ToList()
|
||
})
|
||
.OrderByDescending(x => x.start_date)
|
||
.ToList();
|
||
break;
|
||
|
||
default:
|
||
return BadRequest("不支援的分組方式,請使用 monthly, yearly 或 activity");
|
||
}
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = stats,
|
||
groupBy = groupBy,
|
||
message = "查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 數據分析 API
|
||
|
||
/// <summary>
|
||
/// 樞紐分析
|
||
/// POST api/pivot/pivot_analysis
|
||
/// </summary>
|
||
/// <param name="request">分析請求參數</param>
|
||
/// <returns></returns>
|
||
[HttpPost]
|
||
[Route("api/pivot/pivot_analysis")]
|
||
public IHttpActionResult PostPivotAnalysis(dynamic request)
|
||
{
|
||
try
|
||
{
|
||
// TODO: 實作樞紐分析邏輯
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
rows = new List<object>(),
|
||
columns = new List<object>(),
|
||
values = new List<object>()
|
||
},
|
||
message = "分析完成"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"分析失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 趨勢分析
|
||
/// GET api/pivot/trend_analysis
|
||
/// </summary>
|
||
/// <param name="metric">指標名稱</param>
|
||
/// <param name="startDate">開始日期</param>
|
||
/// <param name="endDate">結束日期</param>
|
||
/// <param name="interval">時間間隔</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/trend_analysis")]
|
||
public IHttpActionResult GetTrendAnalysis(string metric, string startDate = null, string endDate = null, string interval = "monthly")
|
||
{
|
||
try
|
||
{
|
||
if (string.IsNullOrEmpty(metric))
|
||
{
|
||
return BadRequest("請指定分析指標");
|
||
}
|
||
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
select new
|
||
{
|
||
ActivityDate = act.startDate_solar,
|
||
OrderDate = po.up_time,
|
||
Amount = pod.qty * pod.price,
|
||
FollowerNum = f.num,
|
||
ActivityNum = act.num
|
||
};
|
||
|
||
// 日期篩選
|
||
if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out DateTime start))
|
||
{
|
||
query = query.Where(x => x.ActivityDate >= start);
|
||
}
|
||
if (!string.IsNullOrEmpty(endDate) && DateTime.TryParse(endDate, out DateTime end))
|
||
{
|
||
query = query.Where(x => x.ActivityDate <= end);
|
||
}
|
||
|
||
object trendData = null;
|
||
string trendDirection = "stable";
|
||
|
||
switch (metric?.ToLower())
|
||
{
|
||
case "income": // 收入趨勢
|
||
if (interval == "monthly")
|
||
{
|
||
var monthlyIncome = query.GroupBy(x => new {
|
||
Year = x.ActivityDate.Value.Year,
|
||
Month = x.ActivityDate.Value.Month
|
||
})
|
||
.Select(g => new
|
||
{
|
||
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
||
value = g.Sum(x => x.Amount),
|
||
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
||
})
|
||
.OrderBy(x => x.date)
|
||
.ToList();
|
||
|
||
// 計算趨勢方向
|
||
if (monthlyIncome.Count >= 2)
|
||
{
|
||
var firstHalf = monthlyIncome.Take(monthlyIncome.Count / 2).Average(x => (double)x.value);
|
||
var secondHalf = monthlyIncome.Skip(monthlyIncome.Count / 2).Average(x => (double)x.value);
|
||
trendDirection = secondHalf > firstHalf * 1.05 ? "up" : (secondHalf < firstHalf * 0.95 ? "down" : "stable");
|
||
}
|
||
|
||
trendData = new
|
||
{
|
||
labels = monthlyIncome.Select(x => x.period).ToList(),
|
||
values = monthlyIncome.Select(x => x.value).ToList(),
|
||
growth_rates = monthlyIncome.Select((x, i) => i == 0 ? 0.0 :
|
||
Math.Round((double)(x.value - monthlyIncome[i-1].value) / (double)monthlyIncome[i-1].value * 100, 2))
|
||
.ToList()
|
||
};
|
||
}
|
||
break;
|
||
|
||
case "followers": // 參與信眾數趨勢
|
||
if (interval == "monthly")
|
||
{
|
||
var monthlyFollowers = query.GroupBy(x => new {
|
||
Year = x.ActivityDate.Value.Year,
|
||
Month = x.ActivityDate.Value.Month
|
||
})
|
||
.Select(g => new
|
||
{
|
||
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
||
value = g.Select(x => x.FollowerNum).Distinct().Count(),
|
||
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
||
})
|
||
.OrderBy(x => x.date)
|
||
.ToList();
|
||
|
||
// 計算趨勢方向
|
||
if (monthlyFollowers.Count >= 2)
|
||
{
|
||
var firstHalf = monthlyFollowers.Take(monthlyFollowers.Count / 2).Average(x => (double)x.value);
|
||
var secondHalf = monthlyFollowers.Skip(monthlyFollowers.Count / 2).Average(x => (double)x.value);
|
||
trendDirection = secondHalf > firstHalf * 1.05 ? "up" : (secondHalf < firstHalf * 0.95 ? "down" : "stable");
|
||
}
|
||
|
||
trendData = new
|
||
{
|
||
labels = monthlyFollowers.Select(x => x.period).ToList(),
|
||
values = monthlyFollowers.Select(x => x.value).ToList(),
|
||
growth_rates = monthlyFollowers.Select((x, i) => i == 0 ? 0.0 :
|
||
Math.Round((double)(x.value - monthlyFollowers[i-1].value) / (double)monthlyFollowers[i-1].value * 100, 2))
|
||
.ToList()
|
||
};
|
||
}
|
||
break;
|
||
|
||
case "activities": // 法會數量趨勢
|
||
var monthlyActivities = query.GroupBy(x => new {
|
||
Year = x.ActivityDate.Value.Year,
|
||
Month = x.ActivityDate.Value.Month
|
||
})
|
||
.Select(g => new
|
||
{
|
||
period = $"{g.Key.Year}/{g.Key.Month:00}",
|
||
value = g.Select(x => x.ActivityNum).Distinct().Count(),
|
||
date = new DateTime(g.Key.Year, g.Key.Month, 1)
|
||
})
|
||
.OrderBy(x => x.date)
|
||
.ToList();
|
||
|
||
trendData = new
|
||
{
|
||
labels = monthlyActivities.Select(x => x.period).ToList(),
|
||
values = monthlyActivities.Select(x => x.value).ToList()
|
||
};
|
||
break;
|
||
|
||
default:
|
||
return BadRequest("不支援的指標,請使用 income, followers 或 activities");
|
||
}
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
metric = metric,
|
||
interval = interval,
|
||
trend = trendDirection,
|
||
chart_data = trendData
|
||
},
|
||
message = "分析完成"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"分析失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 對比分析
|
||
/// GET api/pivot/comparative_analysis
|
||
/// </summary>
|
||
/// <param name="compareType">對比類型</param>
|
||
/// <param name="period1">期間1</param>
|
||
/// <param name="period2">期間2</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/comparative_analysis")]
|
||
public IHttpActionResult GetComparativeAnalysis(string compareType, string period1, string period2)
|
||
{
|
||
try
|
||
{
|
||
// TODO: 實作對比分析邏輯
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
period1_data = new List<object>(),
|
||
period2_data = new List<object>(),
|
||
comparison = new
|
||
{
|
||
growth_rate = 0.0,
|
||
difference = 0.0
|
||
}
|
||
},
|
||
message = "對比完成"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"對比失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 報表管理 API
|
||
|
||
/// <summary>
|
||
/// 取得自訂報表清單
|
||
/// GET api/pivot/custom_reports
|
||
/// </summary>
|
||
/// <param name="page">頁碼</param>
|
||
/// <param name="pageSize">每頁筆數</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/custom_reports")]
|
||
public IHttpActionResult GetCustomReports(int page = 1, int pageSize = 10)
|
||
{
|
||
try
|
||
{
|
||
// TODO: 實作自訂報表查詢邏輯
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
list = new List<object>(),
|
||
total = 0,
|
||
page = page,
|
||
pageSize = pageSize
|
||
},
|
||
message = "查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 建立自訂報表
|
||
/// POST api/pivot/custom_reports
|
||
/// </summary>
|
||
/// <param name="request">報表設定</param>
|
||
/// <returns></returns>
|
||
[HttpPost]
|
||
[Route("api/pivot/custom_reports")]
|
||
public IHttpActionResult PostCustomReport(dynamic request)
|
||
{
|
||
try
|
||
{
|
||
// TODO: 實作自訂報表建立邏輯
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new { id = 1 },
|
||
message = "報表建立成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"建立失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 匯出報表
|
||
/// GET api/pivot/export/{reportId}
|
||
/// </summary>
|
||
/// <param name="reportId">報表編號</param>
|
||
/// <param name="format">匯出格式 (excel/pdf/csv)</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/export/{reportId}")]
|
||
public IHttpActionResult GetReportExport(int reportId, string format = "excel")
|
||
{
|
||
try
|
||
{
|
||
// TODO: 實作報表匯出邏輯
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
download_url = $"/admin/pivot/downloads/report_{reportId}.{format}",
|
||
file_name = $"report_{reportId}_{DateTime.Now:yyyyMMdd}.{format}"
|
||
},
|
||
message = "匯出成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"匯出失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Excel 數據連接 API
|
||
|
||
/// <summary>
|
||
/// Excel 數據連接專用 - 簡化格式
|
||
/// GET api/pivot/excel_data
|
||
/// </summary>
|
||
/// <param name="format">回傳格式 (json/csv)</param>
|
||
/// <param name="limit">限制筆數</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/excel_data")]
|
||
[AllowAnonymous] // 允許 Excel 直接呼叫
|
||
public IHttpActionResult GetExcelData(string format = "json", int limit = 5000)
|
||
{
|
||
try
|
||
{
|
||
// 直接查詢,對應視圖結構
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
orderby po.up_time descending, act.num descending
|
||
select new
|
||
{
|
||
ActivityId = act.num,
|
||
ActivityName = act.subject,
|
||
StartDate = act.startDate_solar,
|
||
EndDate = act.endDate_solar,
|
||
FollowerCode = f.f_number,
|
||
FollowerName = f.u_name,
|
||
OrderNo = po.order_no,
|
||
OrderDate = po.up_time,
|
||
IsPatron = pod.parent_num != null ? "是" : "否",
|
||
KindName = aik.kind,
|
||
ItemName = ai.subject,
|
||
Quantity = pod.qty,
|
||
Price = pod.price,
|
||
Received = 0,
|
||
Outstanding = pod.price * pod.qty
|
||
};
|
||
|
||
var data = query.Take(limit).ToList();
|
||
|
||
if (format?.ToLower() == "csv")
|
||
{
|
||
// CSV 格式回傳
|
||
var csv = "法會ID,法會名稱,開始日期,結束日期,信眾編號,信眾姓名,報名編號,報名日期,功德主,功德類型,功德名稱,數量,金額,已收,未收\n";
|
||
foreach (var item in data)
|
||
{
|
||
csv += $"{item.ActivityId},{item.ActivityName},{item.StartDate:yyyy/MM/dd},{item.EndDate:yyyy/MM/dd}," +
|
||
$"{item.FollowerCode},{item.FollowerName},{item.OrderNo},{item.OrderDate:yyyy/MM/dd HH:mm}," +
|
||
$"{item.IsPatron},{item.KindName},{item.ItemName},{item.Quantity},{item.Price},{item.Received},{item.Outstanding}\n";
|
||
}
|
||
|
||
return Ok(csv);
|
||
}
|
||
|
||
// JSON 格式回傳 (預設)
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = data,
|
||
count = data.Count,
|
||
generated_at = DateTime.Now,
|
||
message = "資料查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"資料查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Excel 數據連接專用 - 按欄位顏色分類格式
|
||
/// GET api/pivot/excel_data_structured
|
||
/// </summary>
|
||
/// <param name="includeMetadata">是否包含元數據</param>
|
||
/// <param name="limit">限制筆數</param>
|
||
/// <returns></returns>
|
||
[HttpGet]
|
||
[Route("api/pivot/excel_data_structured")]
|
||
[AllowAnonymous]
|
||
public IHttpActionResult GetExcelDataStructured(bool includeMetadata = false, int limit = 5000)
|
||
{
|
||
try
|
||
{
|
||
var query = from pod in _db.pro_order_detail
|
||
join po in _db.pro_order on pod.order_no equals po.order_no
|
||
join ai in _db.actItems on pod.actItem_num equals ai.num
|
||
join act in _db.activities on po.activity_num equals act.num
|
||
join f in _db.followers on po.f_num equals f.num
|
||
join aik in _db.actItem_kind on ai.kind equals aik.num
|
||
orderby po.up_time descending, act.num descending
|
||
select new
|
||
{
|
||
// 🟠 橘色欄位 - 法會基礎資料
|
||
activity_fields = new
|
||
{
|
||
法會ID = act.num,
|
||
法會名稱 = act.subject,
|
||
開始日期 = act.startDate_solar,
|
||
結束日期 = act.endDate_solar
|
||
},
|
||
|
||
// 🔵 藍色欄位 - 信眾基礎資料
|
||
follower_fields = new
|
||
{
|
||
信眾編號 = f.f_number,
|
||
信眾姓名 = f.u_name,
|
||
報名編號 = po.order_no,
|
||
報名日期 = po.up_time
|
||
},
|
||
|
||
// 🟢 綠色欄位 - 功德資訊欄位
|
||
merit_info_fields = new
|
||
{
|
||
功德主 = pod.parent_num != null ? "是" : "否",
|
||
功德類型 = aik.kind,
|
||
功德名稱 = ai.subject
|
||
},
|
||
|
||
// 🟣 紫色欄位 - 計算欄位
|
||
calculated_fields = new
|
||
{
|
||
數量 = pod.qty,
|
||
金額 = pod.price,
|
||
已收 = 0,
|
||
未收 = pod.price * pod.qty,
|
||
小計 = pod.price * pod.qty
|
||
},
|
||
|
||
// 📊 元數據 (可選)
|
||
metadata = includeMetadata ? new
|
||
{
|
||
原始資料 = new
|
||
{
|
||
parent_num = pod.parent_num,
|
||
kind_num = aik.num,
|
||
item_num = ai.num,
|
||
activity_num = act.num,
|
||
follower_num = f.num
|
||
},
|
||
欄位分類說明 = new
|
||
{
|
||
橘色欄位_法會資料 = "法會相關的基礎資料,用於法會維度分析",
|
||
藍色欄位_信眾資料 = "信眾相關的基礎資料,用於信眾維度分析",
|
||
綠色欄位_功德資訊 = "功德相關的資訊欄位,包含功德主、功德類型和名稱",
|
||
紫色欄位_計算欄位 = "需要數學計算或統計的欄位"
|
||
}
|
||
} : null
|
||
};
|
||
|
||
var data = query.Take(limit).ToList();
|
||
|
||
var result = new
|
||
{
|
||
success = true,
|
||
data = new
|
||
{
|
||
records = data,
|
||
field_categories = new
|
||
{
|
||
橘色_法會基礎資料 = new[] { "法會ID", "法會名稱", "開始日期", "結束日期" },
|
||
藍色_信眾基礎資料 = new[] { "信眾編號", "信眾姓名", "報名編號", "報名日期" },
|
||
綠色_功德資訊欄位 = new[] { "功德主", "功德類型", "功德名稱" },
|
||
紫色_計算欄位 = new[] { "數量", "金額", "已收", "未收" }
|
||
},
|
||
usage_notes = new
|
||
{
|
||
橘色欄位說明 = "法會相關基礎資料,適合用於法會維度的篩選和分組",
|
||
藍色欄位說明 = "信眾相關基礎資料,適合用於信眾維度的篩選和分組",
|
||
綠色欄位說明 = "功德資訊欄位,包含功德主判斷、功德類型和名稱,適合用於功德維度的篩選和分組",
|
||
紫色欄位說明 = "計算統計欄位,適合用於樞紐分析的值區域進行加總",
|
||
Excel樞紐分析建議 = new
|
||
{
|
||
篩選器 = "橘色(法會名稱) + 藍色(報名日期) + 綠色(功德類型)",
|
||
列標籤 = "藍色(信眾姓名) + 綠色(功德主)",
|
||
欄標籤 = "橘色(法會名稱) 或時間維度",
|
||
值區域 = "紫色(金額加總、數量計數)"
|
||
}
|
||
}
|
||
},
|
||
count = data.Count,
|
||
generated_at = DateTime.Now,
|
||
message = "結構化資料查詢成功"
|
||
};
|
||
|
||
return Ok(result);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return BadRequest($"查詢失敗:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 工具方法
|
||
|
||
/// <summary>
|
||
/// 釋放資源
|
||
/// </summary>
|
||
/// <param name="disposing"></param>
|
||
protected override void Dispose(bool disposing)
|
||
{
|
||
if (disposing)
|
||
{
|
||
_db?.Dispose();
|
||
}
|
||
base.Dispose(disposing);
|
||
}
|
||
|
||
#endregion
|
||
} |