feat: 可扩展档案模式框架 + 内置 3C(电子) 模式
- 新增 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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user