feat: Migrate Excel plugin to built-in functionality
Move Excel plugin from external plugin to built-in functionality, including: - Core Excel file operations (open, close, save) - Sheet manipulation and cell operations - Row and column iterators - Process handlers for Excel operations - Comprehensive test coverage This change internalizes Excel manipulation capabilities: - Read/write operations on cells, rows and columns - Sheet management and styling - Cell merging and formula handling - Coordinate conversion utilities
This commit is contained in:
parent
211e523759
commit
f0da60f467
8 changed files with 1291 additions and 0 deletions
104
excel/each.go
Normal file
104
excel/each.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// Cols defines an iterator to a sheet
|
||||
type Cols struct {
|
||||
id string
|
||||
*excelize.Cols
|
||||
create int64
|
||||
}
|
||||
|
||||
// Rows defines an iterator to a sheet
|
||||
type Rows struct {
|
||||
id string
|
||||
*excelize.Rows
|
||||
create int64
|
||||
}
|
||||
|
||||
var openCols = sync.Map{}
|
||||
var openRows = sync.Map{}
|
||||
|
||||
// OpenRow each row of the sheet
|
||||
func (excel *Excel) OpenRow(sheet string) (string, error) {
|
||||
id := uuid.NewString()
|
||||
rows, err := excel.Rows(sheet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
openRows.Store(id, &Rows{id: id, Rows: rows, create: time.Now().Unix()})
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// NextRow next row of the sheet
|
||||
func NextRow(id string) ([]string, error) {
|
||||
value, ok := openRows.Load(id)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rows %s not found", id)
|
||||
}
|
||||
|
||||
if value.(*Rows).Next() {
|
||||
row, err := value.(*Rows).Columns()
|
||||
// fmt.Printf("DEBUG: %#v %v %v\n", row, err, row == nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if row == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CloseRow done the sheet
|
||||
func CloseRow(id string) {
|
||||
openRows.Delete(id)
|
||||
}
|
||||
|
||||
// OpenColumn each cols of the sheet
|
||||
func (excel *Excel) OpenColumn(sheet string) (string, error) {
|
||||
id := uuid.NewString()
|
||||
cols, err := excel.Cols(sheet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
openCols.Store(id, &Cols{id: id, Cols: cols, create: time.Now().Unix()})
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// NextColumn next col of the sheet
|
||||
func NextColumn(id string) ([]string, error) {
|
||||
value, ok := openCols.Load(id)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cols %s not found", id)
|
||||
}
|
||||
|
||||
if value.(*Cols).Next() {
|
||||
col, err := value.(*Cols).Rows()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if col == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return col, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CloseColumn done the sheet
|
||||
func CloseColumn(id string) {
|
||||
openCols.Delete(id)
|
||||
}
|
||||
64
excel/each_test.go
Normal file
64
excel/each_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEachCols(t *testing.T) {
|
||||
files := testFiles(t)
|
||||
h1, err := Open(files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close(h1)
|
||||
|
||||
xls, err := Get(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := xls.OpenColumn("供销存管理表格")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer CloseColumn(id)
|
||||
|
||||
res := []string{}
|
||||
for col, err := NextColumn(id); err == nil && col != nil; col, err = NextColumn(id) {
|
||||
res = append(res, col...)
|
||||
}
|
||||
|
||||
assert.Contains(t, strings.Join(res, ""), "供销存管理表格产品查询")
|
||||
assert.Contains(t, strings.Join(res, ""), "刘大大")
|
||||
}
|
||||
|
||||
func TestEachRows(t *testing.T) {
|
||||
files := testFiles(t)
|
||||
h1, err := Open(files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close(h1)
|
||||
|
||||
xls, err := Get(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := xls.OpenRow("供销存管理表格")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer CloseRow(id)
|
||||
|
||||
res := []string{}
|
||||
for row, err := NextRow(id); err == nil && row != nil; row, err = NextRow(id) {
|
||||
res = append(res, row...)
|
||||
}
|
||||
|
||||
assert.Contains(t, strings.Join(res, ""), "供销存管理表格产品查询")
|
||||
assert.Contains(t, strings.Join(res, ""), "刘大大")
|
||||
}
|
||||
102
excel/excel.go
Normal file
102
excel/excel.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"github.com/yaoapp/yao/config"
|
||||
)
|
||||
|
||||
// Excel the excel file
|
||||
type Excel struct {
|
||||
id string
|
||||
path string
|
||||
create int64
|
||||
*excelize.File
|
||||
}
|
||||
|
||||
// openFiles the open files
|
||||
var openFiles = sync.Map{}
|
||||
|
||||
// Open open the excel file
|
||||
func Open(path string, writable bool) (string, error) {
|
||||
|
||||
excel := &Excel{path: path}
|
||||
// GET DATA ROOT
|
||||
root := config.Conf.DataRoot
|
||||
path, err := filepath.Abs(filepath.Join(root, path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if writable {
|
||||
|
||||
// if the file not exists, create it
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
create := excelize.NewFile()
|
||||
err := create.SaveAs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
create.Close()
|
||||
}
|
||||
|
||||
excelFile, err := excelize.OpenFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := uuid.NewString()
|
||||
excel.File = excelFile
|
||||
excel.id = id
|
||||
excel.create = time.Now().Unix()
|
||||
openFiles.Store(id, excel)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file %s failed: %w", path, err)
|
||||
}
|
||||
|
||||
excelFile, err := excelize.OpenReader(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id := uuid.NewString()
|
||||
excel.File = excelFile
|
||||
excel.id = id
|
||||
excel.create = time.Now().Unix()
|
||||
openFiles.Store(id, excel)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Close close the excel file
|
||||
func Close(handler string) error {
|
||||
excel, ok := openFiles.Load(handler)
|
||||
if !ok {
|
||||
return fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
err := excel.(*Excel).Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
openFiles.Delete(handler)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get get the excel file
|
||||
func Get(handler string) (*Excel, error) {
|
||||
excel, ok := openFiles.Load(handler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s not found", handler)
|
||||
}
|
||||
return excel.(*Excel), nil
|
||||
}
|
||||
252
excel/excel_test.go
Normal file
252
excel/excel_test.go
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestOpenClose(t *testing.T) {
|
||||
files := testFiles(t)
|
||||
|
||||
h1, err := Open(files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, ok := openFiles.Load(h1); !ok {
|
||||
t.Fatal("open file failed")
|
||||
}
|
||||
|
||||
h2, err := Open(files["test-02"], true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := openFiles.Load(h2); !ok {
|
||||
t.Fatal("open file failed")
|
||||
}
|
||||
|
||||
h3, err := Open(files["test-03"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := openFiles.Load(h3); !ok {
|
||||
t.Fatal("open file failed")
|
||||
}
|
||||
|
||||
_, err = Open(files["test-04"], false)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = Close(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := openFiles.Load(h1); ok {
|
||||
t.Fatal("close file failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSheetList(t *testing.T) {
|
||||
|
||||
files := testFiles(t)
|
||||
h1, err := Open(files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close(h1)
|
||||
|
||||
xls, err := Get(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sheets := xls.GetSheetList()
|
||||
assert.Equal(t, []string{"供销存管理表格", "使用说明"}, sheets)
|
||||
|
||||
_, err = Get("not found")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestOpenInvalidFile(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Create an invalid excel file in the data root
|
||||
root := "excel"
|
||||
invalidFile := filepath.Join(root, "invalid.xlsx")
|
||||
|
||||
// Ensure cleanup after test
|
||||
defer func() {
|
||||
if err := os.Remove(filepath.Join(config.Conf.DataRoot, invalidFile)); err != nil {
|
||||
t.Logf("Failed to cleanup test file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err := os.WriteFile(filepath.Join(config.Conf.DataRoot, invalidFile), []byte("invalid content"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = Open(invalidFile, false)
|
||||
assert.Error(t, err, "should fail to open invalid excel file")
|
||||
}
|
||||
|
||||
func TestCloseErrors(t *testing.T) {
|
||||
// Test closing non-existent handler
|
||||
err := Close("non-existent-handler")
|
||||
assert.Error(t, err, "should fail to close non-existent file")
|
||||
|
||||
// Test double close
|
||||
files := testFiles(t)
|
||||
h1, err := Open(files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First close
|
||||
err = Close(h1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second close should fail
|
||||
err = Close(h1)
|
||||
assert.Error(t, err, "should fail on second close")
|
||||
}
|
||||
|
||||
func TestOpenWithInvalidPath(t *testing.T) {
|
||||
// Test with invalid path
|
||||
_, err := Open("../invalid/path/file.xlsx", false)
|
||||
assert.Error(t, err, "should fail with invalid path")
|
||||
|
||||
// Test with path trying to escape data root
|
||||
_, err = Open("../../../../etc/file.xlsx", false)
|
||||
assert.Error(t, err, "should fail with path trying to escape data root")
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Create a new writable file for testing
|
||||
root := "excel"
|
||||
testFile := filepath.Join(root, "write-test.xlsx")
|
||||
|
||||
// Ensure cleanup after test
|
||||
defer func() {
|
||||
if err := os.Remove(filepath.Join(config.Conf.DataRoot, testFile)); err != nil {
|
||||
t.Logf("Failed to cleanup test file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
h1, err := Open(testFile, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close(h1)
|
||||
|
||||
xls, err := Get(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test WriteCell
|
||||
err = xls.WriteCell("Sheet1", "A1", "Hello")
|
||||
assert.NoError(t, err)
|
||||
err = xls.WriteCell("Sheet1", "B1", 123)
|
||||
assert.NoError(t, err)
|
||||
err = xls.WriteCell("Sheet1", "C1", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test WriteRow
|
||||
row := []interface{}{"Row1", 456, false}
|
||||
err = xls.WriteRow("Sheet1", "A2", row)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test WriteColumn
|
||||
col := []interface{}{"Col1", 789, true}
|
||||
err = xls.WriteColumn("Sheet1", "D1", col)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test WriteAll
|
||||
data := [][]interface{}{
|
||||
{"Name", "Age", "City"},
|
||||
{"John", 30, "New York"},
|
||||
{"Alice", 25, "London"},
|
||||
}
|
||||
err = xls.WriteAll("Sheet2", "A1", data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test error cases
|
||||
// Invalid cell reference
|
||||
err = xls.WriteCell("Sheet1", "invalid", "test")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Save the file to verify changes
|
||||
err = xls.SaveAs(filepath.Join(config.Conf.DataRoot, testFile))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify written data
|
||||
val, err := xls.GetCellValue("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello", val)
|
||||
|
||||
val, err = xls.GetCellValue("Sheet2", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Name", val)
|
||||
}
|
||||
|
||||
func TestSetSheet(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
root := "excel"
|
||||
testFile := filepath.Join(root, "sheet-test.xlsx")
|
||||
|
||||
// Ensure cleanup after test
|
||||
defer func() {
|
||||
if err := os.Remove(filepath.Join(config.Conf.DataRoot, testFile)); err != nil {
|
||||
t.Logf("Failed to cleanup test file: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
h1, err := Open(testFile, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer Close(h1)
|
||||
|
||||
xls, err := Get(h1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test creating new sheet
|
||||
idx, err := xls.SetSheet("NewSheet")
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, idx, 0)
|
||||
|
||||
// Test getting existing sheet
|
||||
idx2, err := xls.SetSheet("NewSheet")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, idx, idx2)
|
||||
|
||||
// Verify sheet exists
|
||||
sheets := xls.GetSheetList()
|
||||
assert.Contains(t, sheets, "NewSheet")
|
||||
}
|
||||
|
||||
func testFiles(t *testing.T) map[string]string {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// test data root path
|
||||
root := "excel"
|
||||
return map[string]string{
|
||||
"test-01": filepath.Join(root, "test-01.xlsx"),
|
||||
"test-02": filepath.Join(root, "test-02.xlsx"),
|
||||
"test-03": filepath.Join(root, "test-03.xlsx"),
|
||||
}
|
||||
}
|
||||
261
excel/process.go
Normal file
261
excel/process.go
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"github.com/xuri/excelize/v2"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/exception"
|
||||
)
|
||||
|
||||
func init() {
|
||||
process.RegisterGroup("excel", map[string]process.Handler{
|
||||
"open": processOpen,
|
||||
"close": processClose,
|
||||
"save": processSave,
|
||||
"sheets": processSheets,
|
||||
|
||||
"read.cell": processReadCell,
|
||||
"write.cell": processWriteCell,
|
||||
|
||||
"set.style": processSetStyle,
|
||||
"set.formula": processSetFormula,
|
||||
"set.link": processSetLink,
|
||||
"set.mergecell": processMergeCell,
|
||||
"set.unmergecell": processUnmergeCell,
|
||||
|
||||
"convert.columnnametonumber": processColumnNameToNumber,
|
||||
"convert.columnnumbertoname": processColumnNumberToName,
|
||||
"convert.cellnametocoordinates": processCellNameToCoordinates,
|
||||
"convert.coordinatestocellname": processCoordinatesToCellName,
|
||||
})
|
||||
}
|
||||
|
||||
// processOpen process the excel.open <file> <readonly>
|
||||
func processOpen(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
file := process.ArgsString(0)
|
||||
readonly := false
|
||||
if len(process.Args) > 1 {
|
||||
readonly = process.ArgsBool(1)
|
||||
}
|
||||
|
||||
handle, err := Open(file, readonly)
|
||||
if err != nil {
|
||||
exception.New("excel.open %s error: %s", 500, file, err.Error()).Throw()
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
// processClose process the excel.close <handle>
|
||||
func processClose(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
handle := process.ArgsString(0)
|
||||
err := Close(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.close %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSave process the excel.save <handle>
|
||||
func processSave(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
handle := process.ArgsString(0)
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.save %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.Save()
|
||||
if err != nil {
|
||||
exception.New("excel.save %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSheets process the excel.sheets <handle>
|
||||
func processSheets(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
handle := process.ArgsString(0)
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.sheets %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
return xls.GetSheetList()
|
||||
}
|
||||
|
||||
// processReadCell process the excel.read.cell <handle> <sheet> <cell>
|
||||
func processReadCell(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(3)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
cell := process.ArgsString(2)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.read.cell %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
value, err := xls.GetCellValue(sheet, cell)
|
||||
if err != nil {
|
||||
exception.New("excel.read.cell %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// processWriteCell process the excel.write.cell <handle> <sheet> <cell> <value>
|
||||
func processWriteCell(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(4)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
cell := process.ArgsString(2)
|
||||
value := process.Args[3]
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.write.cell %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.SetCellValue(sheet, cell, value)
|
||||
if err != nil {
|
||||
exception.New("excel.write.cell %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSetStyle process the excel.set.style <handle> <sheet> <cell> <style>
|
||||
func processSetStyle(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(4)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
cell := process.ArgsString(2)
|
||||
styleID := process.ArgsInt(3)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.set.style %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.SetCellStyle(sheet, cell, cell, styleID)
|
||||
if err != nil {
|
||||
exception.New("excel.set.style %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSetFormula process the excel.set.formula <handle> <sheet> <cell> <formula>
|
||||
func processSetFormula(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(4)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
cell := process.ArgsString(2)
|
||||
formula := process.ArgsString(3)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.set.formula %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.SetCellFormula(sheet, cell, formula)
|
||||
if err != nil {
|
||||
exception.New("excel.set.formula %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSetLink process the excel.set.link <handle> <sheet> <cell> <link> <text>
|
||||
func processSetLink(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(5)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
cell := process.ArgsString(2)
|
||||
link := process.ArgsString(3)
|
||||
text := process.ArgsString(4)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.set.link %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.SetCellHyperLink(sheet, cell, link, text)
|
||||
if err != nil {
|
||||
exception.New("excel.set.link %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processMergeCell process the excel.set.mergecell <handle> <sheet> <start> <end>
|
||||
func processMergeCell(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(4)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
start := process.ArgsString(2)
|
||||
end := process.ArgsString(3)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.set.mergecell %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.MergeCell(sheet, start, end)
|
||||
if err != nil {
|
||||
exception.New("excel.set.mergecell %s:%s:%s:%s error: %s", 500, handle, sheet, start, end, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processUnmergeCell process the excel.set.unmergecell <handle> <sheet> <start> <end>
|
||||
func processUnmergeCell(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(4)
|
||||
handle := process.ArgsString(0)
|
||||
sheet := process.ArgsString(1)
|
||||
start := process.ArgsString(2)
|
||||
end := process.ArgsString(3)
|
||||
|
||||
xls, err := Get(handle)
|
||||
if err != nil {
|
||||
exception.New("excel.set.unmergecell %s error: %s", 500, handle, err.Error()).Throw()
|
||||
}
|
||||
err = xls.UnmergeCell(sheet, start, end)
|
||||
if err != nil {
|
||||
exception.New("excel.set.unmergecell %s:%s:%s:%s error: %s", 500, handle, sheet, start, end, err.Error()).Throw()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processColumnNameToNumber process the excel.convert.columnnametonumber <name>
|
||||
func processColumnNameToNumber(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
name := process.ArgsString(0)
|
||||
number, err := excelize.ColumnNameToNumber(name)
|
||||
if err != nil {
|
||||
exception.New("excel.convert.columnnametonumber %s error: %s", 500, name, err.Error()).Throw()
|
||||
}
|
||||
return number
|
||||
}
|
||||
|
||||
// processColumnNumberToName process the excel.convert.columnnumbertoname <number>
|
||||
func processColumnNumberToName(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
number := process.ArgsInt(0)
|
||||
name, err := excelize.ColumnNumberToName(number)
|
||||
if err != nil {
|
||||
exception.New("excel.convert.columnnumbertoname %d error: %s", 500, number, err.Error()).Throw()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// processCellNameToCoordinates process the excel.convert.cellnametocoordinates <cell>
|
||||
func processCellNameToCoordinates(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(1)
|
||||
cell := process.ArgsString(0)
|
||||
x, y, err := excelize.CellNameToCoordinates(cell)
|
||||
if err != nil {
|
||||
exception.New("excel.convert.cellnametocoordinates %s error: %s", 500, cell, err.Error()).Throw()
|
||||
}
|
||||
return []int{x, y}
|
||||
}
|
||||
|
||||
// processCoordinatesToCellName process the excel.convert.coordinatestocellname <col> <row>
|
||||
func processCoordinatesToCellName(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
col := process.ArgsInt(0)
|
||||
row := process.ArgsInt(1)
|
||||
cell, err := excelize.CoordinatesToCellName(col, row)
|
||||
if err != nil {
|
||||
exception.New("excel.convert.coordinatestocellname %d,%d error: %s", 500, col, row, err.Error()).Throw()
|
||||
}
|
||||
return cell
|
||||
}
|
||||
432
excel/process_test.go
Normal file
432
excel/process_test.go
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
func TestProcessOpen(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Test opening file in read-only mode
|
||||
p, err := process.Of("excel.open", files["test-01"], true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, handle)
|
||||
|
||||
// Test opening file in write mode
|
||||
p, err = process.Of("excel.open", files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle2, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, handle2)
|
||||
}
|
||||
|
||||
func TestProcessSheets(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Open file first
|
||||
p, err := process.Of("excel.open", files["test-01"], true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get sheets
|
||||
p, err = process.Of("excel.sheets", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sheets, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sheetList := sheets.([]string)
|
||||
assert.Equal(t, []string{"供销存管理表格", "使用说明"}, sheetList)
|
||||
}
|
||||
|
||||
func TestProcessReadCell(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Open file first
|
||||
p, err := process.Of("excel.open", files["test-01"], true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get sheets first to verify we have the right sheet
|
||||
p, err = process.Of("excel.sheets", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sheets, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sheetList := sheets.([]string)
|
||||
if len(sheetList) == 0 {
|
||||
t.Fatal("no sheets found")
|
||||
}
|
||||
|
||||
// Read cell from the first sheet
|
||||
p, err = process.Of("excel.read.cell", handle, sheetList[0], "B2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Print the value for debugging
|
||||
assert.NotEmpty(t, value)
|
||||
|
||||
// Try reading from a non-existent cell
|
||||
p, err = process.Of("excel.read.cell", handle, sheetList[0], "ZZ999")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value, err = p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Empty(t, value) // Non-existent cell should return empty string
|
||||
}
|
||||
|
||||
func TestProcessClose(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Open file first
|
||||
p, err := process.Of("excel.open", files["test-01"], true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Close file
|
||||
p, err = process.Of("excel.close", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to use closed handle
|
||||
p, err = process.Of("excel.sheets", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProcessConvert(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
// Test ColumnNameToNumber
|
||||
p, err := process.Of("excel.convert.columnnametonumber", "AK")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
number, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, 37, number)
|
||||
|
||||
// Test ColumnNumberToName
|
||||
p, err = process.Of("excel.convert.columnnumbertoname", 37)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "AK", name)
|
||||
|
||||
// Test CellNameToCoordinates
|
||||
p, err = process.Of("excel.convert.cellnametocoordinates", "A1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
coords, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
coordsArr := coords.([]int)
|
||||
assert.Equal(t, []int{1, 1}, coordsArr)
|
||||
|
||||
// Test CoordinatesToCellName
|
||||
p, err = process.Of("excel.convert.coordinatestocellname", 1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cell, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "A1", cell)
|
||||
}
|
||||
|
||||
func TestProcessSave(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Create a new test file path
|
||||
dataRoot := config.Conf.DataRoot
|
||||
newFile := filepath.Join(filepath.Dir(files["test-01"]), "test-save.xlsx")
|
||||
|
||||
// Copy test-01.xlsx to new file
|
||||
content, err := os.ReadFile(filepath.Join(dataRoot, files["test-01"]))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dataRoot, newFile), content, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(filepath.Join(dataRoot, newFile)) // Clean up after test
|
||||
|
||||
// Open new file in write mode
|
||||
p, err := process.Of("excel.open", newFile, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write something to the file
|
||||
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "A1", "Test Save")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Save the file
|
||||
p, err = process.Of("excel.save", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Close the file before reading
|
||||
p, err = process.Of("excel.close", handle)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the file exists and has been modified
|
||||
savedContent, err := os.ReadFile(filepath.Join(dataRoot, newFile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEqual(t, content, savedContent, "New file should be modified")
|
||||
|
||||
// Verify original file is unchanged
|
||||
originalContent, err := os.ReadFile(filepath.Join(dataRoot, files["test-01"]))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, content, originalContent, "Original file should not be modified")
|
||||
|
||||
// Try to save with invalid handle
|
||||
p, err = process.Of("excel.save", "invalid-handle")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProcessWriteCell(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Open file in write mode
|
||||
p, err := process.Of("excel.open", files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write string value
|
||||
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "A1", "Test Write")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Write number value
|
||||
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "B1", 123.45)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify written values
|
||||
p, err = process.Of("excel.read.cell", handle, "供销存管理表格", "A1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "Test Write", value)
|
||||
|
||||
// Test with invalid handle
|
||||
p, err = process.Of("excel.write.cell", "invalid-handle", "供销存管理表格", "A1", "Test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with invalid sheet name
|
||||
p, err = process.Of("excel.write.cell", handle, "InvalidSheet", "A1", "Test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProcessSetStyle(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
files := testFiles(t)
|
||||
|
||||
// Open file in write mode
|
||||
p, err := process.Of("excel.open", files["test-01"], false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handle, err := p.Exec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set style
|
||||
p, err = process.Of("excel.set.style", handle, "供销存管理表格", "A1", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with invalid handle
|
||||
p, err = process.Of("excel.set.style", "invalid-handle", "供销存管理表格", "A1", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with invalid sheet name
|
||||
p, err = process.Of("excel.set.style", handle, "InvalidSheet", "A1", 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with invalid style ID
|
||||
p, err = process.Of("excel.set.style", handle, "供销存管理表格", "A1", -1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = p.Exec()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
75
excel/write.go
Normal file
75
excel/write.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package excel
|
||||
|
||||
import (
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// WriteCell write the cell
|
||||
func (excel *Excel) WriteCell(sheet string, cell string, value interface{}) error {
|
||||
|
||||
_, err := excel.SetSheet(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return excel.SetCellValue(sheet, cell, value)
|
||||
}
|
||||
|
||||
// WriteRow write the row
|
||||
func (excel *Excel) WriteRow(sheet string, cell string, value []interface{}) error {
|
||||
|
||||
_, err := excel.SetSheet(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return excel.SetSheetRow(sheet, cell, &value)
|
||||
}
|
||||
|
||||
// WriteColumn write the column
|
||||
func (excel *Excel) WriteColumn(sheet string, cell string, value []interface{}) error {
|
||||
|
||||
_, err := excel.SetSheet(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return excel.SetSheetCol(sheet, cell, &value)
|
||||
}
|
||||
|
||||
// WriteAll write all the sheet
|
||||
func (excel *Excel) WriteAll(sheet string, cell string, rows [][]interface{}) error {
|
||||
|
||||
_, err := excel.SetSheet(sheet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if err := excel.SetSheetRow(sheet, cell, &row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
colIndex, rowIndex, err := excelize.CellNameToCoordinates(cell)
|
||||
cell, err = excelize.CoordinatesToCellName(colIndex, rowIndex+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSheet set the sheet
|
||||
func (excel *Excel) SetSheet(name string) (int, error) {
|
||||
|
||||
idx, err := excel.GetSheetIndex(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
idx, err = excel.NewSheet(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
1
main.go
1
main.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
_ "github.com/yaoapp/gou/encoding"
|
||||
_ "github.com/yaoapp/yao/aigc"
|
||||
_ "github.com/yaoapp/yao/crypto"
|
||||
_ "github.com/yaoapp/yao/excel"
|
||||
_ "github.com/yaoapp/yao/helper"
|
||||
_ "github.com/yaoapp/yao/openai"
|
||||
_ "github.com/yaoapp/yao/wework"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue