Implement referral, points, coupons, commission, member levels, and order integration with migration SQL and background jobs. Co-authored-by: Cursor <cursoragent@cursor.com>
367 lines
13 KiB
C#
367 lines
13 KiB
C#
using DocumentFormat.OpenXml;
|
|
using DocumentFormat.OpenXml.Packaging;
|
|
using DocumentFormat.OpenXml.Spreadsheet;
|
|
|
|
namespace Oms.Application.Services.Orders.Templates;
|
|
|
|
/// <summary>
|
|
/// 为标准模板 Excel 增加表头必填标黄、枚举下拉等 OpenXML 增强。
|
|
/// </summary>
|
|
internal static class OrderTemplateExcelOpenXmlEnhancer
|
|
{
|
|
private const uint DataValidationStartRow = 2;
|
|
private const uint DataValidationEndRow = 5000;
|
|
private const string RequiredHeaderFillRgb = "FFFFE5E5";
|
|
private const string RequiredHeaderFontRgb = "FFC00000";
|
|
|
|
public static byte[] Enhance(
|
|
byte[] workbookBytes,
|
|
IReadOnlyList<OrderTemplateFieldDefinition> fields,
|
|
IReadOnlyList<OrderTemplateDictOption> dictOptions)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
stream.Write(workbookBytes, 0, workbookBytes.Length);
|
|
stream.Position = 0;
|
|
|
|
using (var document = SpreadsheetDocument.Open(stream, true))
|
|
{
|
|
var workbookPart = document.WorkbookPart
|
|
?? throw new InvalidOperationException("工作簿无效");
|
|
|
|
var requiredHeaderStyleIndex = EnsureRequiredHeaderStyle(workbookPart);
|
|
var dataSheetPart = GetWorksheetPart(workbookPart, OrderTemplateExcelGenerator.DataSheetName)
|
|
?? throw new InvalidOperationException("未找到下单数据工作表");
|
|
|
|
ApplyRequiredHeaderHighlight(dataSheetPart.Worksheet, fields, requiredHeaderStyleIndex);
|
|
ApplyColumnValidations(workbookPart, dataSheetPart, fields, dictOptions);
|
|
|
|
dataSheetPart.Worksheet.Save();
|
|
workbookPart.Workbook.Save();
|
|
}
|
|
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static void ApplyRequiredHeaderHighlight(
|
|
Worksheet worksheet,
|
|
IReadOnlyList<OrderTemplateFieldDefinition> fields,
|
|
uint styleIndex)
|
|
{
|
|
var sheetData = worksheet.GetFirstChild<SheetData>()
|
|
?? throw new InvalidOperationException("工作表数据为空");
|
|
|
|
var headerRow = sheetData.Elements<Row>().FirstOrDefault(x => x.RowIndex?.Value == 1)
|
|
?? throw new InvalidOperationException("未找到表头行");
|
|
|
|
for (var columnIndex = 0; columnIndex < fields.Count; columnIndex++)
|
|
{
|
|
if (!fields[columnIndex].Required)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var cellReference = $"{ToColumnName(columnIndex)}1";
|
|
var cell = headerRow.Elements<Cell>().FirstOrDefault(x => x.CellReference?.Value == cellReference)
|
|
?? CreateCell(headerRow, cellReference, fields[columnIndex].Label);
|
|
cell.StyleIndex = styleIndex;
|
|
}
|
|
}
|
|
|
|
private static void ApplyColumnValidations(
|
|
WorkbookPart workbookPart,
|
|
WorksheetPart dataSheetPart,
|
|
IReadOnlyList<OrderTemplateFieldDefinition> fields,
|
|
IReadOnlyList<OrderTemplateDictOption> dictOptions)
|
|
{
|
|
var dictRangeByType = BuildDictTypeRanges(workbookPart, dictOptions);
|
|
var validations = dataSheetPart.Worksheet.GetFirstChild<DataValidations>();
|
|
if (validations == null)
|
|
{
|
|
validations = new DataValidations();
|
|
dataSheetPart.Worksheet.Append(validations);
|
|
}
|
|
|
|
for (var columnIndex = 0; columnIndex < fields.Count; columnIndex++)
|
|
{
|
|
var field = fields[columnIndex];
|
|
var formula = ResolveValidationFormula(field, dictRangeByType);
|
|
if (string.IsNullOrWhiteSpace(formula))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var columnName = ToColumnName(columnIndex);
|
|
var validation = new DataValidation
|
|
{
|
|
Type = DataValidationValues.List,
|
|
AllowBlank = !field.Required,
|
|
ShowInputMessage = true,
|
|
ShowErrorMessage = true,
|
|
ErrorTitle = "输入无效",
|
|
Error = field.Required ? "请从下拉列表选择或填写有效值" : "请填写有效值",
|
|
PromptTitle = field.Label,
|
|
Prompt = BuildValidationPrompt(field),
|
|
SequenceOfReferences = new ListValue<StringValue>
|
|
{
|
|
InnerText = $"{columnName}{DataValidationStartRow}:{columnName}{DataValidationEndRow}"
|
|
}
|
|
};
|
|
validation.Append(new Formula1(formula));
|
|
validations.Append(validation);
|
|
}
|
|
|
|
validations.Count = (uint)validations.ChildElements.Count;
|
|
}
|
|
|
|
private static string? ResolveValidationFormula(
|
|
OrderTemplateFieldDefinition field,
|
|
IReadOnlyDictionary<string, string> dictRangeByType)
|
|
{
|
|
if (string.Equals(field.DataType, "boolean", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "\"true,false\"";
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(field.DictType))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (dictRangeByType.TryGetValue(field.DictType, out var rangeFormula))
|
|
{
|
|
return rangeFormula;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string BuildValidationPrompt(OrderTemplateFieldDefinition field)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(field.Hint))
|
|
{
|
|
return field.Hint;
|
|
}
|
|
|
|
if (string.Equals(field.DataType, "boolean", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "填写 true 或 false";
|
|
}
|
|
|
|
return field.Required ? "必填" : "选填";
|
|
}
|
|
|
|
private static Dictionary<string, string> BuildDictTypeRanges(
|
|
WorkbookPart workbookPart,
|
|
IReadOnlyList<OrderTemplateDictOption> dictOptions)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (dictOptions.Count == 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var dictSheetPart = GetWorksheetPart(workbookPart, OrderTemplateExcelGenerator.DictSheetName);
|
|
if (dictSheetPart?.Worksheet == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var grouped = dictOptions
|
|
.Where(x => !string.IsNullOrWhiteSpace(x.DictType) && !string.IsNullOrWhiteSpace(x.Code))
|
|
.GroupBy(x => x.DictType, StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
var sheetData = dictSheetPart.Worksheet.GetFirstChild<SheetData>();
|
|
sheetData?.RemoveAllChildren<Row>();
|
|
sheetData ??= new SheetData();
|
|
if (dictSheetPart.Worksheet.GetFirstChild<SheetData>() == null)
|
|
{
|
|
dictSheetPart.Worksheet.Append(sheetData);
|
|
}
|
|
|
|
var headerRow = new Row { RowIndex = 1 };
|
|
sheetData.InsertAt(headerRow, 0);
|
|
|
|
var columnIndex = 0;
|
|
foreach (var group in grouped)
|
|
{
|
|
var columnName = ToColumnName(columnIndex);
|
|
SetCellText(headerRow, $"{columnName}1", group.Key);
|
|
|
|
var rowIndex = 2U;
|
|
foreach (var option in group.OrderBy(x => x.Code, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
var row = sheetData.Elements<Row>().FirstOrDefault(x => x.RowIndex?.Value == rowIndex)
|
|
?? CreateRow(sheetData, rowIndex);
|
|
SetCellText(row, $"{columnName}{rowIndex}", option.Code);
|
|
rowIndex++;
|
|
}
|
|
|
|
if (rowIndex == 2)
|
|
{
|
|
columnIndex++;
|
|
continue;
|
|
}
|
|
|
|
var lastRow = rowIndex - 1;
|
|
var escapedSheetName = OrderTemplateExcelGenerator.DictSheetName.Replace("'", "''");
|
|
result[group.Key] = $"'{escapedSheetName}'!${columnName}$2:${columnName}${lastRow}";
|
|
columnIndex++;
|
|
}
|
|
|
|
dictSheetPart.Worksheet.Save();
|
|
return result;
|
|
}
|
|
|
|
private static uint EnsureRequiredHeaderStyle(WorkbookPart workbookPart)
|
|
{
|
|
var stylesPart = workbookPart.WorkbookStylesPart ?? workbookPart.AddNewPart<WorkbookStylesPart>();
|
|
var stylesheet = stylesPart.Stylesheet;
|
|
if (stylesheet == null)
|
|
{
|
|
stylesheet = new Stylesheet
|
|
{
|
|
Fonts = new Fonts(new Font()),
|
|
Fills = new Fills(
|
|
new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } },
|
|
new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } }),
|
|
Borders = new Borders(new Border()),
|
|
CellStyleFormats = new CellStyleFormats(new CellFormat()),
|
|
CellFormats = new CellFormats(new CellFormat())
|
|
};
|
|
stylesheet.Fonts.Count = 1;
|
|
stylesheet.Fills.Count = 2;
|
|
stylesheet.Borders.Count = 1;
|
|
stylesheet.CellStyleFormats.Count = 1;
|
|
stylesheet.CellFormats.Count = 1;
|
|
stylesPart.Stylesheet = stylesheet;
|
|
}
|
|
|
|
var fills = stylesheet.Fills ?? new Fills { Count = 0 };
|
|
stylesheet.Fills = fills;
|
|
|
|
var fonts = stylesheet.Fonts ?? new Fonts { Count = 0 };
|
|
stylesheet.Fonts = fonts;
|
|
|
|
var borders = stylesheet.Borders ?? new Borders { Count = 0 };
|
|
stylesheet.Borders = borders;
|
|
|
|
var cellFormats = stylesheet.CellFormats ?? new CellFormats { Count = 0 };
|
|
stylesheet.CellFormats = cellFormats;
|
|
|
|
if (fills.Count?.Value is not > 0)
|
|
{
|
|
fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } });
|
|
fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } });
|
|
fills.Count = 2U;
|
|
}
|
|
|
|
if (fonts.Count?.Value is not > 0)
|
|
{
|
|
fonts.Append(new Font());
|
|
fonts.Count = 1U;
|
|
}
|
|
|
|
if (borders.Count?.Value is not > 0)
|
|
{
|
|
borders.Append(new Border());
|
|
borders.Count = 1U;
|
|
}
|
|
|
|
if (cellFormats.Count?.Value is not > 0)
|
|
{
|
|
cellFormats.Append(new CellFormat());
|
|
cellFormats.Count = 1U;
|
|
}
|
|
|
|
var requiredFill = new Fill
|
|
{
|
|
PatternFill = new PatternFill
|
|
{
|
|
PatternType = PatternValues.Solid,
|
|
ForegroundColor = new ForegroundColor { Rgb = RequiredHeaderFillRgb },
|
|
BackgroundColor = new BackgroundColor { Indexed = 64 }
|
|
}
|
|
};
|
|
fills.Append(requiredFill);
|
|
var requiredFillId = (uint)fills.ChildElements.Count - 1;
|
|
fills.Count = (uint)fills.ChildElements.Count;
|
|
|
|
var headerFont = new Font(
|
|
new Bold(),
|
|
new Color { Rgb = RequiredHeaderFontRgb });
|
|
fonts.Append(headerFont);
|
|
var headerFontId = (uint)fonts.ChildElements.Count - 1;
|
|
fonts.Count = (uint)fonts.ChildElements.Count;
|
|
|
|
var headerFormat = new CellFormat
|
|
{
|
|
FontId = headerFontId,
|
|
FillId = requiredFillId,
|
|
BorderId = 0,
|
|
ApplyFont = true,
|
|
ApplyFill = true
|
|
};
|
|
cellFormats.Append(headerFormat);
|
|
var headerFormatIndex = (uint)cellFormats.ChildElements.Count - 1;
|
|
cellFormats.Count = (uint)cellFormats.ChildElements.Count;
|
|
|
|
stylesheet.Save();
|
|
return headerFormatIndex;
|
|
}
|
|
|
|
private static WorksheetPart? GetWorksheetPart(WorkbookPart workbookPart, string sheetName)
|
|
{
|
|
var sheet = workbookPart.Workbook.Sheets?.Elements<Sheet>()
|
|
.FirstOrDefault(x => string.Equals(x.Name?.Value, sheetName, StringComparison.Ordinal));
|
|
if (sheet?.Id?.Value == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return workbookPart.GetPartById(sheet.Id.Value) as WorksheetPart;
|
|
}
|
|
|
|
private static Cell CreateCell(Row row, string cellReference, string text)
|
|
{
|
|
var cell = new Cell
|
|
{
|
|
CellReference = cellReference,
|
|
DataType = CellValues.String,
|
|
CellValue = new CellValue(text)
|
|
};
|
|
row.Append(cell);
|
|
return cell;
|
|
}
|
|
|
|
private static Row CreateRow(SheetData sheetData, uint rowIndex)
|
|
{
|
|
var row = new Row { RowIndex = rowIndex };
|
|
sheetData.Append(row);
|
|
return row;
|
|
}
|
|
|
|
private static void SetCellText(Row row, string cellReference, string text)
|
|
{
|
|
var cell = row.Elements<Cell>().FirstOrDefault(x => x.CellReference?.Value == cellReference)
|
|
?? CreateCell(row, cellReference, text);
|
|
cell.DataType = CellValues.String;
|
|
cell.CellValue = new CellValue(text);
|
|
}
|
|
|
|
private static string ToColumnName(int columnIndex)
|
|
{
|
|
var dividend = columnIndex + 1;
|
|
var columnName = string.Empty;
|
|
while (dividend > 0)
|
|
{
|
|
var modulo = (dividend - 1) % 26;
|
|
columnName = Convert.ToChar('A' + modulo) + columnName;
|
|
dividend = (dividend - modulo) / 26;
|
|
}
|
|
|
|
return columnName;
|
|
}
|
|
}
|