electricity_bill_calc_service/excel/abstract.go

200 lines
5.7 KiB
Go

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 {
actualField.Set(reflect.ValueOf(decimalValue))
}
}
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
}