916bdd9c7c
- 新增 category.archive_kind 与 kind_field 字段模板表(迁移 0010) - 种子 electronics 字段模板 + 3C 品类树(手机/笔记本/平板等) - 后端按档案模式动态计算完整度/合格:食品沿用 food_detail, 其它模式走 product.attributes + kind_field - 新增 GET /api/kind-fields?kind= 接口 - 后台编辑页按品类模式动态渲染规格参数表单 - 公开详情页/接口输出带标签的规格表 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
package adminstore
|
|
|
|
import "context"
|
|
|
|
// DefaultKind is used for products whose category has no archive kind (or no
|
|
// category at all).
|
|
const DefaultKind = "generic"
|
|
|
|
// FoodKind keeps the mature, dedicated food_detail path; every other kind is
|
|
// driven generically by kind_field + product.attributes.
|
|
const FoodKind = "food"
|
|
|
|
// genericBaseFields are the core completeness fields for any non-food kind.
|
|
// Food keeps its own richer CompletenessFields list.
|
|
var genericBaseFields = []string{"name", "gtin", "brand", "category", "image"}
|
|
|
|
// KindField describes one editable spec field for an archive kind. It drives
|
|
// both the dynamic admin form and the kind-aware completeness computation.
|
|
type KindField struct {
|
|
Kind string `json:"kind"`
|
|
FieldKey string `json:"field_key"`
|
|
GroupLabel string `json:"group_label"`
|
|
LabelZH string `json:"label_zh"`
|
|
FieldType string `json:"field_type"`
|
|
Unit *string `json:"unit"`
|
|
Options []string `json:"options"`
|
|
Placeholder *string `json:"placeholder"`
|
|
SortOrder int `json:"sort_order"`
|
|
Qualified bool `json:"qualified"`
|
|
}
|
|
|
|
// ListKindFields returns the ordered field template for one archive kind.
|
|
func (s *Store) ListKindFields(ctx context.Context, kind string) ([]KindField, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT kind, field_key, group_label, label_zh, field_type, unit, options,
|
|
placeholder, sort_order, qualified
|
|
FROM kind_field WHERE kind = $1 ORDER BY sort_order, field_key`, kind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []KindField{}
|
|
for rows.Next() {
|
|
var f KindField
|
|
if err := rows.Scan(&f.Kind, &f.FieldKey, &f.GroupLabel, &f.LabelZH,
|
|
&f.FieldType, &f.Unit, &f.Options, &f.Placeholder, &f.SortOrder,
|
|
&f.Qualified); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, f)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// kindQualifiedKeys returns, per kind, the attribute keys that count toward the
|
|
// completeness/qualified score. Loaded in one query so list views stay cheap.
|
|
func (s *Store) kindQualifiedKeys(ctx context.Context, q queryer) (map[string][]string, error) {
|
|
rows, err := s.pool.Query(ctx,
|
|
"SELECT kind, field_key FROM kind_field WHERE qualified ORDER BY sort_order, field_key")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
m := map[string][]string{}
|
|
for rows.Next() {
|
|
var kind, key string
|
|
if err := rows.Scan(&kind, &key); err != nil {
|
|
return nil, err
|
|
}
|
|
m[kind] = append(m[kind], key)
|
|
}
|
|
return m, rows.Err()
|
|
}
|
|
|
|
// completenessKeys returns the ordered list of field keys that define a full
|
|
// archive for the given kind.
|
|
func completenessKeys(kind string, qualifiedAttrKeys []string) []string {
|
|
if kind == FoodKind {
|
|
return CompletenessFields
|
|
}
|
|
keys := make([]string, 0, len(genericBaseFields)+len(qualifiedAttrKeys))
|
|
keys = append(keys, genericBaseFields...)
|
|
keys = append(keys, qualifiedAttrKeys...)
|
|
return keys
|
|
}
|
|
|
|
// attrPresent reports whether an attribute value is meaningfully filled in.
|
|
func attrPresent(attrs map[string]any, key string) bool {
|
|
v, ok := attrs[key]
|
|
if !ok || v == nil {
|
|
return false
|
|
}
|
|
switch t := v.(type) {
|
|
case string:
|
|
return t != ""
|
|
case []any:
|
|
return len(t) > 0
|
|
default:
|
|
return true
|
|
}
|
|
}
|