Files
ai_site/platform/internal/logic/applogic/impex.go
2026-07-31 10:19:22 +08:00

320 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package applogic
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"path/filepath"
"strconv"
"strings"
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/meta"
"aijianzhan/platform/internal/types"
"github.com/xuri/excelize/v2"
)
// ImportRows accepts .xlsx (preferred) or .csv.
func (l *CrudLogic) ImportRows(slug, resource, filename string, r io.Reader) (*types.ImportResp, error) {
tenantID := authx.TenantID(l.ctx)
userID := authx.UserID(l.ctx)
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
if err != nil {
return nil, err
}
if !hasOp(ref, "import") {
return nil, fmt.Errorf("operation import not allowed")
}
ext := strings.ToLower(filepath.Ext(filename))
raw, err := io.ReadAll(r)
if err != nil {
return nil, err
}
if len(raw) == 0 {
return nil, fmt.Errorf("empty file")
}
var rows [][]string
switch {
case ext == ".xlsx" || ext == ".xlsm" || isZipOOXML(raw):
rows, err = readExcelRows(raw)
case ext == ".csv" || ext == ".txt" || looksLikeCSV(raw):
rows, err = readCSVRows(raw)
default:
// try excel then csv
rows, err = readExcelRows(raw)
if err != nil {
rows, err = readCSVRows(raw)
}
}
if err != nil {
return nil, fmt.Errorf("parse file: %w", err)
}
if len(rows) < 1 {
return nil, fmt.Errorf("no header row")
}
headers := make([]string, len(rows[0]))
for i, h := range rows[0] {
headers[i] = strings.TrimSpace(h)
}
resp := &types.ImportResp{Errors: []string{}}
for rowNum := 1; rowNum < len(rows); rowNum++ {
rec := rows[rowNum]
if rowEmpty(rec) {
continue
}
body := map[string]any{}
for i, h := range headers {
if i >= len(rec) || h == "" {
continue
}
fname := mapHeaderToField(ref, h)
if fname == "" || fname == ref.Entity.PrimaryKey {
continue
}
body[fname] = coerceValue(ref, fname, strings.TrimSpace(rec[i]))
}
if len(body) == 0 {
resp.Skipped++
continue
}
if _, err := l.svcCtx.CRUD.Create(l.ctx, ref, tenantID, userID, body); err != nil {
resp.Skipped++
if len(resp.Errors) < 100 {
resp.Errors = append(resp.Errors, fmt.Sprintf("row %d: %v", rowNum+1, err))
}
continue
}
resp.Inserted++
}
return resp, nil
}
// ImportCSV kept for callers; prefers CSV parsing.
func (l *CrudLogic) ImportCSV(slug, resource string, r io.Reader) (*types.ImportResp, error) {
return l.ImportRows(slug, resource, "import.csv", r)
}
// ExportExcel writes .xlsx workbook.
func (l *CrudLogic) ExportExcel(slug, resource string) ([]byte, string, error) {
tenantID := authx.TenantID(l.ctx)
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
if err != nil {
return nil, "", err
}
if !hasOp(ref, "export") {
return nil, "", fmt.Errorf("operation export not allowed")
}
items, _, err := l.svcCtx.CRUD.List(l.ctx, ref, tenantID, 1, 5000, nil, "")
if err != nil {
return nil, "", err
}
headers := make([]string, 0, len(ref.Entity.Fields))
for _, f := range ref.Entity.Fields {
headers = append(headers, f.Name)
}
f := excelize.NewFile()
sheet := f.GetSheetName(0)
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
_ = f.SetCellValue(sheet, cell, h)
}
for ri, item := range items {
for ci, h := range headers {
cell, _ := excelize.CoordinatesToCellName(ci+1, ri+2)
if v, ok := item[h]; ok && v != nil {
_ = f.SetCellValue(sheet, cell, v)
}
}
}
buf, err := f.WriteToBuffer()
if err != nil {
return nil, "", err
}
return buf.Bytes(), resource + ".xlsx", nil
}
// ExportCSV kept for format=csv.
func (l *CrudLogic) ExportCSV(slug, resource string) ([]byte, error) {
tenantID := authx.TenantID(l.ctx)
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
if err != nil {
return nil, err
}
if !hasOp(ref, "export") {
return nil, fmt.Errorf("operation export not allowed")
}
items, _, err := l.svcCtx.CRUD.List(l.ctx, ref, tenantID, 1, 5000, nil, "")
if err != nil {
return nil, err
}
headers := make([]string, 0, len(ref.Entity.Fields))
for _, f := range ref.Entity.Fields {
headers = append(headers, f.Name)
}
buf := &bytes.Buffer{}
w := csv.NewWriter(buf)
_ = w.Write(headers)
for _, item := range items {
row := make([]string, len(headers))
for i, h := range headers {
if v, ok := item[h]; ok && v != nil {
row[i] = fmt.Sprint(v)
}
}
_ = w.Write(row)
}
w.Flush()
return buf.Bytes(), w.Error()
}
func readExcelRows(raw []byte) ([][]string, error) {
f, err := excelize.OpenReader(bytes.NewReader(raw))
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
sheets := f.GetSheetList()
if len(sheets) == 0 {
return nil, fmt.Errorf("excel has no sheets")
}
// 优先业务数据页,跳过说明/超限汇总
prefer := ""
for _, name := range sheets {
n := strings.ToLower(name)
if strings.Contains(n, "settlement") || (strings.Contains(name, "测点") && !strings.Contains(name, "超限") && !strings.Contains(name, "说明")) {
prefer = name
break
}
}
if prefer == "" {
for _, name := range sheets {
if !strings.Contains(name, "说明") && !strings.Contains(name, "超限") {
prefer = name
break
}
}
}
if prefer == "" {
prefer = sheets[0]
}
return f.GetRows(prefer)
}
func readCSVRows(raw []byte) ([][]string, error) {
cr := csv.NewReader(bytes.NewReader(raw))
cr.FieldsPerRecord = -1
return cr.ReadAll()
}
func isZipOOXML(raw []byte) bool {
// xlsx is a zip archive
return len(raw) >= 4 && raw[0] == 'P' && raw[1] == 'K' && raw[2] == 3 && raw[3] == 4
}
func looksLikeCSV(raw []byte) bool {
sample := raw
if len(sample) > 512 {
sample = sample[:512]
}
s := string(sample)
return strings.Contains(s, ",") || strings.Contains(s, "\t") || strings.Contains(s, ";")
}
func rowEmpty(rec []string) bool {
for _, c := range rec {
if strings.TrimSpace(c) != "" {
return false
}
}
return true
}
func hasOp(ref *meta.ResourceRef, op string) bool {
for _, o := range ref.Resource.Operations {
if o == op {
return true
}
}
return false
}
func mapHeaderToField(ref *meta.ResourceRef, header string) string {
h := strings.TrimSpace(header)
if h == "" {
return ""
}
norm := normalizeHeader(h)
aliases := map[string]string{
"编号dk": "dkilo", "测点dk": "dkilo", "dkilo": "dkilo",
"里程": "chainage", "chainage": "chainage",
"测点编号": "point_code", "测点": "point_code", "point_code": "point_code",
"断面类型": "section_type", "section_type": "section_type",
"工点": "worksite", "worksite": "worksite",
"cjl数值": "cjl_value", "观测值": "cjl_value", "观测值mm": "cjl_value", "cjl_value": "cjl_value",
"cjl颜色": "cjl_color", "观测色": "cjl_color", "cjl_color": "cjl_color",
"设计沉降": "design_settlement_mm", "设计总沉降量mm": "design_settlement_mm", "design_settlement_mm": "design_settlement_mm",
"累积沉降": "cum_settlement_mm", "累积沉降量mm": "cum_settlement_mm", "cum_settlement_mm": "cum_settlement_mm",
"预测沉降": "pred_settlement_mm", "预测沉降mm": "pred_settlement_mm", "pred_settlement_mm": "pred_settlement_mm",
"超限量": "exceed_mm", "超限量mm": "exceed_mm", "exceed_mm": "exceed_mm",
"超限累计天数": "exceed_days", "累计天数天": "exceed_days", "exceed_days": "exceed_days",
"监督天": "supervise_days", "监督周期天": "supervise_days", "supervise_days": "supervise_days",
"超期天数": "overdue_days", "overdue_days": "overdue_days",
"前一日占比": "before_day", "before_day": "before_day", "beforeday": "before_day",
"后一日占比": "next_day", "next_day": "next_day", "nextday": "next_day",
"频率": "frequency", "frequency": "frequency",
"cljd标识": "workinfo_kilo", "workinfo_kilo": "workinfo_kilo",
"状态": "status", "status": "status",
"里程米": "mileage_m", "mileage_m": "mileage_m",
}
if a, ok := aliases[norm]; ok {
h = a
norm = a
}
for _, f := range ref.Entity.Fields {
if f.Name == h || f.Label == header || normalizeHeader(f.Label) == norm || f.Name == norm {
return f.Name
}
}
return ""
}
func normalizeHeader(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
repl := strings.NewReplacer(" ", "", "_", "", "-", "", "(", "", ")", "", "", "", "", "", ".", "")
return repl.Replace(s)
}
func coerceValue(ref *meta.ResourceRef, fieldName, raw string) any {
if raw == "" {
return nil
}
for _, f := range ref.Entity.Fields {
if f.Name != fieldName {
continue
}
switch f.Type {
case "int", "bigint":
n, err := strconv.ParseInt(raw, 10, 64)
if err == nil {
return n
}
case "decimal":
n, err := strconv.ParseFloat(raw, 64)
if err == nil {
return n
}
case "boolean":
return raw == "1" || strings.EqualFold(raw, "true") || raw == "是"
}
return raw
}
return raw
}