package excel import ( "electricity_bill_calc/model" "electricity_bill_calc/tools" "encoding/json" "errors" "fmt" "io" "reflect" "strconv" "strings" "github.com/samber/lo" "github.com/shopspring/decimal" "github.com/xuri/excelize/v2" ) type ExcelTemplateGenerator interface { Close() WriteTo(w io.Writer) (int64, error) WriteMeterData(meters []model.EndUserDetail) error } type ColumnRecognizer struct { Pattern []string Tag string MatchIndex int MustFill bool } type ExcelAnalyzer[T any] struct { File *excelize.File ActivedSheet string Regconizers []*ColumnRecognizer } type AnalysisError struct { Err error } type ExcelAnalysisError struct { Row int `json:"row"` Col int `json:"col"` Err AnalysisError `json:"error"` } func NewColumnRecognizer(tag string, patterns ...string) ColumnRecognizer { return ColumnRecognizer{ Pattern: patterns, Tag: tag, MatchIndex: -1, } } func (e AnalysisError) MarshalJSON() ([]byte, error) { return json.Marshal(e.Err.Error()) } func (e AnalysisError) Error() string { return e.Err.Error() } func (e ExcelAnalysisError) Error() string { return e.Err.Error() } func (r *ColumnRecognizer) Recognize(cellValue string) bool { matches := make([]bool, 0) for _, p := range r.Pattern { matches = append(matches, strings.Contains(cellValue, p)) } return lo.Reduce(matches, func(acc, elem bool, index int) bool { return acc && elem }, true) } func NewExcelAnalyzer[T any](file io.Reader, recognizers []*ColumnRecognizer) (*ExcelAnalyzer[T], error) { excelFile, err := excelize.OpenReader(file) if err != nil { return nil, err } sheets := excelFile.GetSheetList() return &ExcelAnalyzer[T]{ File: excelFile, ActivedSheet: sheets[0], Regconizers: recognizers, }, nil } func (a *ExcelAnalyzer[T]) AddRecognizer(recognizer *ColumnRecognizer) { a.Regconizers = append(a.Regconizers, recognizer) } func (a *ExcelAnalyzer[T]) analysisTitleRow(cells []string) { for col, content := range cells { for _, recognizer := range a.Regconizers { if recognizer.Recognize(content) { recognizer.MatchIndex = col break } } } } func (a *ExcelAnalyzer[T]) Analysis(bean T) ([]T, []ExcelAnalysisError) { errs := make([]ExcelAnalysisError, 0) rows, err := a.File.GetRows(a.ActivedSheet) if err != nil { errs = append(errs, ExcelAnalysisError{Row: 0, Col: 0, Err: AnalysisError{Err: err}}) return make([]T, 0), errs } elementType := reflect.TypeOf(bean) collections := reflect.MakeSlice(reflect.SliceOf(elementType), 0, 0) for rowIndex, cols := range rows { // 标题行,需要完成识别动作 if rowIndex == 0 { a.analysisTitleRow(cols) continue } // 非标题行,创建一个需要收集的目标实例,然后逐识别器开始赋值 instance := reflect.New(elementType) for i := 0; i < elementType.NumField(); i++ { field := elementType.Field(i) // 循环目标实例中的字段,与识别器匹配以后,取出指定列的值 if alias, ok := field.Tag.Lookup("excel"); ok { for _, recognizer := range a.Regconizers { if alias == recognizer.Tag && recognizer.MatchIndex != -1 { var matchValue string actualField := instance.Elem().FieldByName(field.Name) if recognizer.MatchIndex > len(cols)-1 { if recognizer.MustFill { errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: errors.New("单元格内不能没有内容。")}}) continue } else { matchValue = "" } } else { matchValue = cols[recognizer.MatchIndex] } switch field.Type.String() { case "string": actualField.Set(reflect.ValueOf(matchValue)) case "*string": if len(matchValue) > 0 { actualField.Set(reflect.ValueOf(&matchValue)) } case "decimal.Decimal": decimalValue, err := decimal.NewFromString(matchValue) if err != nil { errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为纯数字内容。%w", err)}}) actualField.Set(reflect.ValueOf(decimal.Zero)) } else { actualField.Set(reflect.ValueOf(decimalValue)) } case "decimal.NullDecimal": nullValue := decimal.NewNullDecimal(decimal.Zero) nullValue.Valid = false if len(matchValue) == 0 { actualField.Set(reflect.ValueOf(nullValue)) } else { decimalValue, err := decimal.NewFromString(matchValue) if err != nil { errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为纯数字内容。%w", err)}}) actualField.Set(reflect.ValueOf((nullValue))) } else { value := decimal.NewNullDecimal(decimalValue) value.Valid = true actualField.Set(reflect.ValueOf(value)) } } case "int64", "int": if len(matchValue) == 0 { actualField.SetInt(0) } else { v, err := strconv.Atoi(matchValue) if err != nil { errs = append(errs, ExcelAnalysisError{Row: rowIndex + 1, Col: recognizer.MatchIndex + 1, Err: AnalysisError{Err: fmt.Errorf("单元格内容应为不带小数的整数。%w", err)}}) actualField.SetInt(0) } else { actualField.SetInt(int64(v)) } } case "bool": if tools.ContainsInsensitive(matchValue, []string{"是", "yes", "y", "true", "t"}) { actualField.SetBool(true) } else { actualField.SetBool(false) } } } } } } collections = reflect.Append(collections, instance.Elem()) } return collections.Interface().([]T), errs }