feat: 公开商品页重设计 + web 上传修复 + 二维码配置
- 公开商品页(/product/:uuid)全面重设计:全宽正方形图片轮播、 左右滑动切图、点击放大全屏查看、商品参数含描述、页脚贴底 - 修复 Flutter web 文件上传无反应(path→bytes) - 修复 web 路由空白页(usePathUrlStrategy + 单层 MaterialApp.router) - 二维码 URL 改为从 STORAGE_PUBLIC_URL 环境变量读取 - 新增 PUBLIC_URL dart-define → AppConfig.publicBaseUrl - 新增 CI/CD workflows + NAS runner compose 配置 - seed S001 补充商品描述字段 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@ var allModels = []any{
|
||||
&model.License{},
|
||||
&model.ProductCategory{},
|
||||
&model.Product{},
|
||||
&model.ProductNameOption{},
|
||||
&model.ProductSeriesOption{},
|
||||
&model.ProductSpecOption{},
|
||||
&model.Warehouse{},
|
||||
&model.Partner{},
|
||||
&model.StockInOrder{},
|
||||
@@ -50,7 +53,7 @@ var truncateOrder = []string{
|
||||
"stock_in_items", "stock_in_orders",
|
||||
"finance_records", "number_rules",
|
||||
"partners", "warehouses",
|
||||
"products", "product_categories",
|
||||
"products", "product_spec_options", "product_series_options", "product_name_options", "product_categories",
|
||||
"licenses", "users", "shops",
|
||||
}
|
||||
|
||||
@@ -186,14 +189,14 @@ func main() {
|
||||
remark string
|
||||
}
|
||||
prodSeeds := []prodSeed{
|
||||
{"MT-001", "6901234567890", "飞天茅台 53度 500ml", "茅台", "500ml/瓶", "瓶", "贵州茅台", &catBaijiu.ID, 2350, 2800, 10, "酱香型白酒,53度,飞天系列"},
|
||||
{"WLY-001", "6902345678901", "五粮液 52度 500ml", "五粮液", "500ml/瓶", "瓶", "宜宾五粮液", &catBaijiu.ID, 950, 1200, 6, "浓香型白酒,52度,普五系列"},
|
||||
{"YH-001", "6903456789012", "洋河梦之蓝 M6 500ml", "洋河", "500ml/瓶", "瓶", "江苏洋河", &catBaijiu.ID, 560, 680, 6, "浓香型,绵柔苏酒代表"},
|
||||
{"LZ-001", "6904567890123", "泸州老窖 特曲 500ml", "泸州老窖", "500ml/瓶", "瓶", "泸州老窖", &catBaijiu.ID, 420, 520, 6, "浓香鼻祖,特曲系列"},
|
||||
{"JNC-001", "6905678901234", "剑南春 水晶剑 500ml", "剑南春", "500ml/瓶", "瓶", "剑南春", &catBaijiu.ID, 390, 480, 6, "浓香型,绵竹名酒"},
|
||||
{"LJ-001", "6906789012345", "郎酒 红花郎10 500ml", "郎酒", "500ml/瓶", "瓶", "古蔺郎酒", &catBaijiu.ID, 320, 420, 6, "酱香型,赤水河畔酿造"},
|
||||
{"LF-001", "3760093550058", "拉菲古堡 2018 750ml", "波尔多", "750ml/瓶", "瓶", "Château Lafite", &catImport.ID, 3800, 5200, 3, "波尔多一级名庄,2018年份"},
|
||||
{"RTM-001", "3021691010008", "人头马 VSOP 700ml", "人头马", "700ml/瓶", "瓶", "Rémy Martin", &catImport.ID, 480, 680, 3, "法国干邑,VSOP级别"},
|
||||
{"MT-001", "6901234567890", "飞天茅台 53度", "茅台", "500ml/瓶", "瓶", "贵州茅台", &catBaijiu.ID, 2350, 2800, 10, "酱香型白酒,53度,飞天系列"},
|
||||
{"WLY-001", "6902345678901", "五粮液 52度", "五粮液", "500ml/瓶", "瓶", "宜宾五粮液", &catBaijiu.ID, 950, 1200, 6, "浓香型白酒,52度,普五系列"},
|
||||
{"YH-001", "6903456789012", "洋河梦之蓝 M6", "洋河", "500ml/瓶", "瓶", "江苏洋河", &catBaijiu.ID, 560, 680, 6, "浓香型,绵柔苏酒代表"},
|
||||
{"LZ-001", "6904567890123", "泸州老窖 特曲", "泸州老窖", "500ml/瓶", "瓶", "泸州老窖", &catBaijiu.ID, 420, 520, 6, "浓香鼻祖,特曲系列"},
|
||||
{"JNC-001", "6905678901234", "剑南春 水晶剑", "剑南春", "500ml/瓶", "瓶", "剑南春", &catBaijiu.ID, 390, 480, 6, "浓香型,绵竹名酒"},
|
||||
{"LJ-001", "6906789012345", "郎酒 红花郎10", "郎酒", "500ml/瓶", "瓶", "古蔺郎酒", &catBaijiu.ID, 320, 420, 6, "酱香型,赤水河畔酿造"},
|
||||
{"LF-001", "3760093550058", "拉菲古堡 2018", "波尔多", "750ml/瓶", "瓶", "Château Lafite", &catImport.ID, 3800, 5200, 3, "波尔多一级名庄,2018年份"},
|
||||
{"RTM-001", "3021691010008", "人头马 VSOP", "人头马", "700ml/瓶", "瓶", "Rémy Martin", &catImport.ID, 480, 680, 3, "法国干邑,VSOP级别"},
|
||||
}
|
||||
var prods []model.Product
|
||||
for _, s := range prodSeeds {
|
||||
@@ -209,6 +212,52 @@ func main() {
|
||||
prods = append(prods, p)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// 商品名称/系列/规格选项
|
||||
// ═══════════════════════════════════════════════════
|
||||
type dimSeed struct{ code, name string }
|
||||
|
||||
for _, s := range []dimSeed{
|
||||
{"NA001", "飞天茅台 53度"}, {"NA002", "五粮液 52度"}, {"NA003", "洋河梦之蓝 M6"},
|
||||
{"NA004", "泸州老窖 特曲"}, {"NA005", "剑南春 水晶剑"}, {"NA006", "郎酒 红花郎10"},
|
||||
{"NA007", "拉菲古堡 2018"}, {"NA008", "人头马 VSOP"},
|
||||
} {
|
||||
var opt model.ProductNameOption
|
||||
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
|
||||
opt = model.ProductNameOption{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: s.code, Name: s.name}
|
||||
db.Create(&opt)
|
||||
fmt.Printf("✅ 名称选项:%s %s\n", s.code, s.name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range []dimSeed{
|
||||
{"SE001", "茅台"}, {"SE002", "五粮液"}, {"SE003", "洋河"}, {"SE004", "泸州老窖"},
|
||||
{"SE005", "剑南春"}, {"SE006", "郎酒"}, {"SE007", "波尔多"}, {"SE008", "人头马"},
|
||||
} {
|
||||
var opt model.ProductSeriesOption
|
||||
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
|
||||
opt = model.ProductSeriesOption{TenantBase: model.TenantBase{ShopID: shop.ID}, Code: s.code, Name: s.name}
|
||||
db.Create(&opt)
|
||||
fmt.Printf("✅ 系列选项:%s %s\n", s.code, s.name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range []struct{ code, name string; quantity int }{
|
||||
{"GG001", "500ml/瓶", 1}, {"GG002", "750ml/瓶", 1}, {"GG003", "700ml/瓶", 1},
|
||||
} {
|
||||
var opt model.ProductSpecOption
|
||||
if db.Where("shop_id = ? AND name = ?", shop.ID, s.name).First(&opt).Error != nil {
|
||||
opt = model.ProductSpecOption{
|
||||
TenantBase: model.TenantBase{ShopID: shop.ID},
|
||||
Code: s.code,
|
||||
Name: s.name,
|
||||
Quantity: s.quantity,
|
||||
}
|
||||
db.Create(&opt)
|
||||
fmt.Printf("✅ 规格选项:%s\n", s.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
// 往来单位
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
@@ -38,6 +38,7 @@ type LicenseConfig struct {
|
||||
type StorageConfig struct {
|
||||
UploadDir string `mapstructure:"upload_dir"`
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
PublicURL string `mapstructure:"public_url"` // 商品公开页基础 URL,用于生成二维码
|
||||
}
|
||||
|
||||
var C Config
|
||||
@@ -58,6 +59,7 @@ func Load() {
|
||||
_ = viper.BindEnv("license.hmac_secret", "LICENSE_HMAC_SECRET")
|
||||
_ = viper.BindEnv("storage.upload_dir", "STORAGE_UPLOAD_DIR")
|
||||
_ = viper.BindEnv("storage.base_url", "STORAGE_BASE_URL")
|
||||
_ = viper.BindEnv("storage.public_url", "STORAGE_PUBLIC_URL")
|
||||
|
||||
// 默认值
|
||||
viper.SetDefault("server.port", "8080")
|
||||
@@ -67,6 +69,7 @@ func Load() {
|
||||
viper.SetDefault("jwt.refresh_expire_h", 168) // 7天
|
||||
viper.SetDefault("storage.upload_dir", "./uploads/images")
|
||||
viper.SetDefault("storage.base_url", "http://localhost:8080/images")
|
||||
viper.SetDefault("storage.public_url", "http://localhost:8081")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Println("[config] no config file found, using defaults and env vars")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/jiu/backend/config"
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/model"
|
||||
)
|
||||
@@ -181,7 +182,12 @@ func (h *ProductHandler) QRCode(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
url := "https://jiu.51yanmei.com/product/" + product.PublicID
|
||||
if product.PublicID == "" {
|
||||
product.PublicID = uuid.New().String()
|
||||
h.db.Model(&product).Update("public_id", product.PublicID)
|
||||
}
|
||||
|
||||
url := config.C.Storage.PublicURL + "/product/" + product.PublicID
|
||||
png, err := qrcode.Encode(url, qrcode.Medium, 256)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -62,15 +62,15 @@ INSERT INTO product_categories (id, shop_id, name, sort_order, created_at, updat
|
||||
-- ── 商品 ────────────────────────────────────────────────────
|
||||
-- id=1~6 白酒, id=7~8 进口烈酒
|
||||
INSERT INTO products (id, shop_id, public_id, code, barcode, name, series, spec, unit, category_id, brand,
|
||||
purchase_price, sale_price, min_stock, remark, created_at, updated_at) VALUES
|
||||
(1, 1, 'a1b2c3d4-0001-0001-0001-000000000001', 'MT-001', '6901234567890', '飞天茅台 53度', '茅台', '500ml/瓶', '瓶', 1, '贵州茅台', 2350, 2800, 10, '酱香型白酒,53度,飞天系列', NOW(), NOW()),
|
||||
(2, 1, 'a1b2c3d4-0001-0001-0001-000000000002', 'WLY-001', '6902345678901', '五粮液 52度', '五粮液', '500ml/瓶', '瓶', 1, '宜宾五粮液', 950, 1200, 6, '浓香型白酒,52度,普五系列', NOW(), NOW()),
|
||||
(3, 1, 'a1b2c3d4-0001-0001-0001-000000000003', 'YH-001', '6903456789012', '洋河梦之蓝 M6', '洋河', '500ml/瓶', '瓶', 1, '江苏洋河', 560, 680, 6, '浓香型,绵柔苏酒代表', NOW(), NOW()),
|
||||
(4, 1, 'a1b2c3d4-0001-0001-0001-000000000004', 'LZ-001', '6904567890123', '泸州老窖 特曲', '泸州老窖', '500ml/瓶', '瓶', 1, '泸州老窖', 420, 520, 6, '浓香鼻祖,特曲系列', NOW(), NOW()),
|
||||
(5, 1, 'a1b2c3d4-0001-0001-0001-000000000005', 'JNC-001', '6905678901234', '剑南春 水晶剑', '剑南春', '500ml/瓶', '瓶', 1, '剑南春', 390, 480, 6, '浓香型,绵竹名酒', NOW(), NOW()),
|
||||
(6, 1, 'a1b2c3d4-0001-0001-0001-000000000006', 'LJ-001', '6906789012345', '郎酒 红花郎10', '郎酒', '500ml/瓶', '瓶', 1, '古蔺郎酒', 320, 420, 6, '酱香型,赤水河畔酿造', NOW(), NOW()),
|
||||
(7, 1, 'a1b2c3d4-0001-0001-0001-000000000007', 'LF-001', '3760093550058', '拉菲古堡 2018', '波尔多', '750ml/瓶', '瓶', 2, 'Château Lafite', 3800, 5200, 3, '波尔多一级名庄,2018年份', NOW(), NOW()),
|
||||
(8, 1, 'a1b2c3d4-0001-0001-0001-000000000008', 'RTM-001', '3021691010008', '人头马 VSOP', '人头马', '700ml/瓶', '瓶', 2, 'Rémy Martin', 480, 680, 3, '法国干邑,VSOP级别', NOW(), NOW());
|
||||
purchase_price, sale_price, min_stock, remark, description, created_at, updated_at) VALUES
|
||||
(1, 1, 'a1b2c3d4-0001-0001-0001-000000000001', 'MT-001', '6901234567890', '飞天茅台 53度', '茅台', '500ml/瓶', '瓶', 1, '贵州茅台', 2350, 2800, 10, '酱香型白酒,53度,飞天系列', '飞天茅台采用优质高粱、小麦,经传统酱香工艺精心酿制。酒体醇厚丰满,酱香突出,幽雅细腻,空杯留香持久。53度黄金度数,是馈赠佳品的首选。', NOW(), NOW()),
|
||||
(2, 1, 'a1b2c3d4-0001-0001-0001-000000000002', 'WLY-001', '6902345678901', '五粮液 52度', '五粮液', '500ml/瓶', '瓶', 1, '宜宾五粮液', 950, 1200, 6, '浓香型白酒,52度,普五系列', '五粮液以高粱、大米、糯米、小麦、玉米五种粮食为原料,经地窖发酵精酿而成。香气悠久,味醇厚,入口甘美,入喉净爽,浓香典范。', NOW(), NOW()),
|
||||
(3, 1, 'a1b2c3d4-0001-0001-0001-000000000003', 'YH-001', '6903456789012', '洋河梦之蓝 M6', '洋河', '500ml/瓶', '瓶', 1, '江苏洋河', 560, 680, 6, '浓香型,绵柔苏酒代表', '梦之蓝M6以"绵、柔、甜、净、香"五大特点著称,是洋河旗舰产品。选用苏北优质小麦制曲,宿迁软水酿造,入口柔顺舒适,回味悠长。', NOW(), NOW()),
|
||||
(4, 1, 'a1b2c3d4-0001-0001-0001-000000000004', 'LZ-001', '6904567890123', '泸州老窖 特曲', '泸州老窖', '500ml/瓶', '瓶', 1, '泸州老窖', 420, 520, 6, '浓香鼻祖,特曲系列', '泸州老窖特曲拥有400余年历史的国宝窖池酿制,被誉为"浓香鼻祖"。酒体醇厚净爽,窖香优雅,尾净余长,是中国浓香型白酒的典型代表。', NOW(), NOW()),
|
||||
(5, 1, 'a1b2c3d4-0001-0001-0001-000000000005', 'JNC-001', '6905678901234', '剑南春 水晶剑', '剑南春', '500ml/瓶', '瓶', 1, '剑南春', 390, 480, 6, '浓香型,绵竹名酒', '剑南春产自四川绵竹,素有"唐时宫廷酒,盛世剑南春"之美誉。水晶剑系列香气浓郁,味道甘洌,风格典雅,是商务宴请和收藏的佳品。', NOW(), NOW()),
|
||||
(6, 1, 'a1b2c3d4-0001-0001-0001-000000000006', 'LJ-001', '6906789012345', '郎酒 红花郎10', '郎酒', '500ml/瓶', '瓶', 1, '古蔺郎酒', 320, 420, 6, '酱香型,赤水河畔酿造', '红花郎10产自赤水河畔二郎镇,坤沙工艺酿造,两年窖藏。酒体酱香突出,醇厚细腻,回味悠长,性价比极高的优质酱香白酒。', NOW(), NOW()),
|
||||
(7, 1, 'a1b2c3d4-0001-0001-0001-000000000007', 'LF-001', '3760093550058', '拉菲古堡 2018', '波尔多', '750ml/瓶', '瓶', 2, 'Château Lafite', 3800, 5200, 3, '波尔多一级名庄,2018年份', '拉菲古堡是法国波尔多五大一级名庄之首,2018年份得益于该年出色的气候条件,酒体结构完美,单宁细腻,黑醋栗与雪松木香交织,陈年潜力达50年以上。', NOW(), NOW()),
|
||||
(8, 1, 'a1b2c3d4-0001-0001-0001-000000000008', 'RTM-001', '3021691010008', '人头马 VSOP', '人头马', '700ml/瓶', '瓶', 2, 'Rémy Martin', 480, 680, 3, '法国干邑,VSOP级别', '人头马VSOP精选法国干邑地区Fine Champagne产区葡萄,经二次蒸馏后在法国橡木桶中陈年至少四年。口感丝滑,带有香草、杏干与蜂蜜的复杂香气,是干邑入门经典。', NOW(), NOW());
|
||||
|
||||
-- ── 商品名称选项 ────────────────────────────────────────────
|
||||
INSERT INTO product_name_options (shop_id, code, name, created_at, updated_at) VALUES
|
||||
|
||||
Reference in New Issue
Block a user