sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
@@ -0,0 +1,335 @@
package exportcenter
import (
"context"
"reflect"
"strings"
"sync"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
// fieldList 导出列字段名清单(json tag)。
type fieldList []string
// columnsFromRequest 从当前请求里读取导出列清单。
// 前端统一传 exportColumnsJson(JSON 字符串):导出接口多为 x-www-form-urlencoded,
// 数组套对象会在表单编码时被拆散,字符串形式不会。
// 这样各导出无需改参数结构即可支持「按页面显示列导出」。
func columnsFromRequest(ctx context.Context) []model.ExportColumn {
r := ghttp.RequestFromCtx(ctx)
if r == nil {
return nil
}
if raw := strings.TrimSpace(r.Get("exportColumnsJson").String()); raw != "" {
cols := make([]model.ExportColumn, 0)
if err := gjson.New(raw).Scan(&cols); err == nil && len(cols) > 0 {
return cols
}
}
// 兼容直接传数组的调用(如 JSON 请求体)
if v := r.Get("exportColumns"); v != nil && !v.IsNil() {
cols := make([]model.ExportColumn, 0)
if err := gconv.Structs(v.Interface(), &cols); err == nil && len(cols) > 0 {
return cols
}
}
return nil
}
// resolveColumns 生成表头。
// 有列清单时按清单顺序取 label 作表头,并按 field 逐行取值;
// 没有列清单时沿用调用方给的 Tags(兼容未改造的导出)。
func resolveColumns(req *Request) (header []interface{}, fields fieldList, err error) {
for _, c := range req.Columns {
field := strings.TrimSpace(c.Field)
if field == "" {
continue
}
label := strings.TrimSpace(c.Label)
if label == "" {
label = field
}
fields = append(fields, field)
header = append(header, label)
}
if len(fields) > 0 {
// 自定义取值的导出(ToRow)要按 RowFields 取列,字段名对不上会静默出空列,这里留一条日志。
if req.ToRow != nil && len(req.RowFields) > 0 {
if missing := missingRowFields(fields, req.RowFields); len(missing) > 0 {
g.Log().Warningf(context.Background(),
"[exportcenter] 导出列在 RowFields 中不存在,将输出空列 biz=%s fields=%v", req.BizKey, missing)
}
}
return header, fields, nil
}
tags, err := req.Tags()
if err != nil {
return nil, nil, err
}
header = make([]interface{}, len(tags))
for i, v := range tags {
header[i] = v
}
return header, nil, nil
}
// rowValues 按列清单取值;无列清单时保持原有转换方式。
func rowValues(v interface{}, fields fieldList, req *Request) []interface{} {
if len(fields) > 0 {
// 自定义取值的导出(ToRow)按 RowFields 声明的位置取列,避免绕过原有格式化。
if req.ToRow != nil && len(req.RowFields) > 0 {
values := pickRowFields(req.ToRow(v), req.RowFields, fields)
i := 0
for _, column := range req.Columns {
if strings.TrimSpace(column.Field) == "" {
continue
}
// 字典列/固定值映射列始终取列表模型的原值,避免对 ToRow 已翻译的中文再次查 key。
if (column.DictType != "" || len(column.ValueMap) > 0) && v != nil {
values[i] = valuesByFields(v, fieldList{column.Field})[0]
}
i++
}
return values
}
values := valuesByFields(v, fields)
i := 0
for _, column := range req.Columns {
if strings.TrimSpace(column.Field) == "" {
continue
}
if column.FallbackField != "" && (values[i] == nil || gconv.String(values[i]) == "") {
values[i] = valuesByFields(v, fieldList{column.FallbackField})[0]
}
i++
}
return values
}
if req.ToRow != nil {
return req.ToRow(v)
}
return structValues(v)
}
// loadColumnDicts 每个任务一次性加载列表列使用的字典。
func loadColumnDicts(ctx context.Context, columns []model.ExportColumn, load func(context.Context, []string) ([]model.SysDictDataListModel, error)) (map[string]map[string]string, error) {
types := make([]string, 0)
seen := make(map[string]bool)
for _, column := range columns {
typ := strings.TrimSpace(column.DictType)
if strings.TrimSpace(column.Field) != "" && typ != "" && !seen[typ] {
seen[typ] = true
types = append(types, typ)
}
}
result := make(map[string]map[string]string)
if len(types) == 0 {
return result, nil
}
items, err := load(ctx, types)
if err != nil {
return nil, err
}
for _, item := range items {
if result[item.DictType] == nil {
result[item.DictType] = make(map[string]string)
}
result[item.DictType][item.DictValue] = item.DictLabel
}
return result, nil
}
func translateRow(values []interface{}, columns []model.ExportColumn, dicts map[string]map[string]string) {
i := 0
for _, column := range columns {
if strings.TrimSpace(column.Field) == "" {
continue
}
if i >= len(values) {
return
}
value := values[i]
str := ""
if value != nil {
str = gconv.String(value)
}
// 固定值映射优先:页面用三元表达式渲染的列(非字典)直接出显示文案。
if len(column.ValueMap) > 0 {
if mapped, ok := column.ValueMap[str]; ok {
values[i] = mapped
i++
continue
}
if column.ValueDefault != "" {
values[i] = column.ValueDefault
i++
continue
}
}
if str != "" {
key := str
if normalized, ok := column.DictValueMap[key]; ok {
key = normalized
}
if label, ok := dicts[strings.TrimSpace(column.DictType)][key]; ok {
values[i] = label
}
}
i++
}
}
// missingRowFields 找出列清单里 RowFields 无法提供的字段。
func missingRowFields(fields fieldList, rowFields []string) []string {
known := make(map[string]bool, len(rowFields))
for _, name := range rowFields {
known[name] = true
}
missing := make([]string, 0)
for _, name := range fields {
if !known[name] {
missing = append(missing, name)
}
}
return missing
}
// pickRowFields 从自定义行数据里按字段名取列:row 与 rowFields 顺序一一对应。
func pickRowFields(row []interface{}, rowFields []string, fields fieldList) []interface{} {
index := make(map[string]int, len(rowFields))
for i, name := range rowFields {
index[name] = i
}
out := make([]interface{}, len(fields))
for i, name := range fields {
pos, ok := index[name]
if !ok || pos < 0 || pos >= len(row) {
out[i] = ""
continue
}
out[i] = row[pos]
}
return out
}
// jsonFieldIndexCache 缓存「结构体类型 -> json tag 到字段下标的映射」,
// 导出大结果集时避免每行重复解析 tag。
var jsonFieldIndexCache sync.Map
func jsonFieldIndexes(t reflect.Type) map[string]int {
if cached, ok := jsonFieldIndexCache.Load(t); ok {
return cached.(map[string]int)
}
indexes := make(map[string]int, t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" { // 未导出字段
continue
}
name := strings.Split(field.Tag.Get("json"), ",")[0]
if name == "" || name == "-" {
continue
}
if _, exists := indexes[name]; !exists {
indexes[name] = i
}
}
jsonFieldIndexCache.Store(t, indexes)
return indexes
}
// valuesByFields 按字段名(json tag)从结构体取值,缺失字段留空。
// 支持 "createdBySumma.name" 这类嵌套路径(列表页常按嵌套字段显示)。
func valuesByFields(v interface{}, fields fieldList) []interface{} {
out := make([]interface{}, len(fields))
rv := reflect.ValueOf(v)
for rv.IsValid() && rv.Kind() == reflect.Ptr {
if rv.IsNil() {
return out
}
rv = rv.Elem()
}
if !rv.IsValid() || rv.Kind() != reflect.Struct {
return out
}
for i, name := range fields {
if value, ok := structFieldValue(rv, name); ok {
out[i] = exportCellValue(value)
} else {
out[i] = ""
}
}
return out
}
// structFieldValue 按 "a.b.c" 的 json tag 路径逐层取值。
func structFieldValue(rv reflect.Value, path string) (reflect.Value, bool) {
cur := rv
for _, name := range strings.Split(path, ".") {
for cur.IsValid() {
if (cur.Kind() == reflect.Ptr || cur.Kind() == reflect.Interface) && cur.IsNil() {
return reflect.Value{}, false
}
if cur.CanInterface() {
if object, ok := cur.Interface().(interface{ Map() map[string]interface{} }); ok {
cur = reflect.ValueOf(object.Map())
break
}
}
if cur.Kind() != reflect.Ptr && cur.Kind() != reflect.Interface {
break
}
cur = cur.Elem()
}
if !cur.IsValid() {
return reflect.Value{}, false
}
if cur.Kind() == reflect.Map && cur.Type().Key().Kind() == reflect.String {
cur = cur.MapIndex(reflect.ValueOf(name))
if !cur.IsValid() {
return reflect.Value{}, false
}
continue
}
if cur.Kind() != reflect.Struct {
return reflect.Value{}, false
}
pos, ok := jsonFieldIndexes(cur.Type())[name]
if !ok {
return reflect.Value{}, false
}
cur = cur.Field(pos)
}
return cur, true
}
// exportCellValue 把字段值转成可写入单元格的值。
// 指针为空、时间、嵌套结构都要处理,避免把内存地址写进 Excel。
func exportCellValue(v reflect.Value) interface{} {
for v.IsValid() && v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if !v.IsValid() || !v.CanInterface() {
return ""
}
switch v.Kind() {
case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array:
// 时间等有 String() 的类型交给 gconv,其余复合类型同样转字符串。
if s, ok := v.Interface().(interface{ String() string }); ok {
return s.String()
}
return gconv.String(v.Interface())
default:
return v.Interface()
}
}