MoreRSS

site iconSunZhongWei | 孙仲维 修改

博客名「大象笔记」,全干程序员一名,曾在金山,DNSPod,腾讯云,常驻烟台。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

SunZhongWei | 孙仲维 的 RSS 预览

golang gin 项目中添加多个 cmd 命令行工具,如何进行目录组织

2025-09-01 14:48:20

需求

之前开发的大赛报名网站终于进入了收尾阶段,比赛已结束,现在需要把参赛选手上传的资料及视频文件导出,做备份。

正好借此机会了解一下如何在 golang gin 项目中添加一堆命令行工具。

为何要引入 cmd 目录

之前把导出数据的功能,都放到了 gin API 接口中,然后通过 swagger 的文档管理界面调用,再保存。

但是,这样搞有个坏处,就是导致 swagger 文档中,会多出很多不需要前端使用的接口。 此外,像视频批量导出这样需要长时间执行的功能,需要更多详情日志的场景,也不适合放到 API 接口中。

所以,我需要将这些功能从 HTTP API 接口中剥离出来,放到单独的命令行工具中。然后代码目录组织就变成了一个问题 🤔

golang gin 中 cmd 命令的目录结构

印象中 Golang 官方是有目录结构组织的推荐结构的。

cmd/ 目录区分

在项目根目录下建立 cmd 文件夹,其中包含多个子目录,每个子目录是一个独立的命令(如 cmd/format-tool, cmd/cli-tool),每个子目录都有自己的 main.go 文件。

例如:

my-gin-app/
├── cmd/          // 存放多个入口命令
│   ├── tool1/
│   │   └── main.go
│   ├── tool2/
│   │   └── main.go
│   └── tool3/
│       └── main.go
├── go.mod
├── go.sum
└── main.go

这样,我只需要在当前的 gin 项目目录中新建一个 cmd/export 目录,在里面放一个 main.go 文件。

mkdir -p cmd/export
touch cmd/export/main.go

我发现多年前,我就测试过多个 main 的情况:

如何组织 Golang 项目目录,使一个项目包含多个 main 入口程序

main.go 代码示例

如何设置 package 呢?是否可以使用 main 作为 package 名称?因为原 gin 的入口文件 main.go 就是 main 作为包名。

测试了一下确实可以的:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello world")
}

执行:

> go run main.go
Hello world

然后,我又测试了一下原 gin 项目的编译,并不影响,很好。(๑•̀ㅂ•́)و✧

如何引用 gin 中的 Model 代码

跟其他包一样引用即可:

package main

import (
	"fmt"

	"sunzhongwei.com/my_project/models"
)

func main() {
	fmt.Println("Hello world")

	fmt.Println(models.Account{}.EditableFields())
}

不便的地方

由于之前 gin 项目中把数据库初始化放到了 main.go 中,导致在 cmd 中想直接使用 db 很麻烦。。。 还是把初始化逻辑放到 models 模块中比较合适。

是否需要使用 Cobra

对应简单的使用场景,不需要大量子命令的情况,我感觉可以不使用 Cobra。

等 cmd 命令多了,重合度高了之后,再引入 Cobra 也不迟。

magento 2.4 使用 SKU 搜索产品,返回一堆无关的产品

2025-08-31 22:52:54

问题现象

Magento 2.4 中,使用类似 “ab-cd-e-9”这样的 SKU 去搜索时,会发现返回了几千个产品。 而排在前面的并不是我想要搜索的 SKU 完全匹配的产品,而是一些无关的产品。

"ab-cd-e-9”两侧加上双引号能解决,但是用户不会这样干。

之前处理过一例 magento 1.7 的远古版本的 SKU 搜索问题, 参考 Magento 网站中无法通过 SKU 搜索到产品的问题排查 解决方案是,改成 LIKE 的方式。但是 magento 2 之后没有了这个配置,直接使用了 Elasticsearch。在后台没法切换 MySQL 的搜索方案。

github issue

一个官方的讨论

https://github.com/magento/magento2/issues/38125

Product with sku that contains the complete search should be printed first and not last as the weight for the sku is superior to the name

说是在 2.4 - dev 最新版本中修复了,今年6月份,修复了两年。。。

思路

如果 sku 完全匹配,则把这个产品排在第一位。

google 搜索词

magento search sku Hyphen split word issue

hyphen 是横杠的意思

问题的根源

Elasticsearch 把横杠作为了一个分词符合,所以拆分成了 4 组,每组都很短,没有实际意义的英文短词,甚至字母,数字。

https://magento.stackexchange.com/questions/358143/search-by-sku-in-magento-not-working-properly

It seems that Elasticsearch treats the "-" character as word delimiter. You could tell Elasticsearch to ignore (strip) the "-" when indexing the SKU. The user can still enter the character, but Elasticsearch will treat it as being left out during the search process.

使用 Elasticsearch 多字段 (multi-field) 特性

如果你既需要精确匹配(如"aaa-bbb"整体),又希望支持全文检索(如单独搜索"aaa"或"bbb"),可以为字段设置多字段特性。这样,Elasticsearch 会为同一个字段值索引两种不同的方式。

json
PUT /your_index
{
  "mappings": {
    "properties": {
      "your_field": {
        "type": "text",          // 主字段用于全文检索(会分词)
        "fields": {
          "raw": {               // 子字段,通过 your_field.raw 访问
            "type": "keyword"    // 子字段类型为 keyword,用于精确匹配、聚合
          }
        }
      }
    }
  }
}

另一个方案

https://mirasvit.com/blog/how-to-enhance-the-search-by-sku-in-magento-2-with-long-tail-search.html#:~:text=By%20default%2C%20Magento%202%20search,search%20will%20return%20zero%20results.

reddit 的一个讨论

https://www.reddit.com/r/Magento/comments/1iimuyv/help_with_search_issues/

If you’re using Elasticsearch 7+, tweaking the ngram or edge_ngram filters in catalogsearch_fulltext index settings might also help. Let me know if you need more details!

Golang 重构 Magento 电商网站之一,UI 设计

2025-08-31 22:11:51

计划把现有的 Magento 网站使用 Golang 重构一下,替换掉 PHP。 主要是 Magento 的架构太复杂了,耗服务器资源也多,改动起来异常麻烦,还不如用 golang 重写得了。 毕竟只用到了简单的产品展示功能。界面让 AI 实现一下就行,添加上 golang 逻辑即可。 再配合上用的已经很成熟的 React Ant Design Pro 的管理后台,维护成本也很低。

先起个名字

就叫 gogento 吧 😅

AI 提示词

推荐使用 Claude 4

我想开发一个类似 Magento 风格及功能的在线电商网站,主要产品是XXX,内容是英文的,需要手机自适应,现在需要输出网页代码。请通过以下方式帮我完成所有界面的代码:

1、用户体验分析:先分析这个网站的主要功能和用户需求,确定核心交互逻辑。主要包括首页,产品列表页,产品详情页面,联系我们,关于我们,及博客等。 
2、产品界面规划:作为产品经理,定义关键界面,确保信息架构合理。 (顶部导航菜单的上面,加上一行信息,显示联系邮箱和电话)
3、高保真 UI 设计:作为 UI 设计师,设计贴近真实 网页设计规范的界面,使用现代化的 UI 元素,使其具有良好的视觉体验。
4、HTML 实现:使用 HTML + Tailwind CSS 生成所有界面,并使用 FontAwesome(或其他开源 UI 组件)让界面更加精美、接近真实的网站设计。拆分代码文件,保持结构清晰。
5、每个界面应作为独立的 HTML 文件存放,例如 index.html、article.html 等。index.html 作为主入口。 
6、真实感增强:优先保持手机的体验,然后才是电脑端,做到自适应 。 
7、使用真实的 UI 图片,而非占位符图片(可从 Unsplash、Pexels、Apple 官方 UI 资源中选择)。

请按照以上要求生成完整的 HTML 代码。注意,不要复杂的 js 逻辑,尽量 js 代码要少要简单。

magento 搜索结果第二页不显示内容,报 404 错误

2025-08-31 10:30:02

magento 搜索关键词,如果返回的结果多于两屏幕,第一页显示正常,但是第二页开始,就无法打开,报 404 错误。

我对比了一下,URL 链接格式的差异:

第一页的链接格式

https://magento.sunzhongwei.com/catalogsearch/result/?q=iphone

第二页的链接格式

https://magento.sunzhongwei.com/catalogsearch/result/index/?p=2&q=iphone

第二页跟第一页的链接格式有明显的不同,多了个 index,这个链接我有点印象。 似乎之前为了防止用户搜索敏感词造成 Google 封禁,特意禁止了这个路径的返回。

确认 Nginx 配置,确实是有这条规则。

修正 Nginx 配置

  #location /catalogsearch/result/index {
  #        return 404;
  #}

把上面的配置注释掉就可以了。

reload Nginx 使配置生效

nginx -t
nginx -s reload

[Magento 2 定制化开发] 之十七:Magento 部署开发的自定义模块

2025-08-29 15:26:54

开发了一个自定义的 Magento 扩展模块,需要部署到服务器上。 不记录不行了,根本记不住这么繁琐的操作。

打包

首先在本地,把目录打成 zip 包。

上传服务器位置

在 Magento 项目根目录的 app/code/ 目录下。

将前面的 zip 包解压。

unzip some_module.zip

启用模块

cd /path/to/magento
php bin/magento module:enable Dir1_Dir2
php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento setup:static-content:deploy -f
php bin/magento cache:flush

Dir1_Dir2 是模块名称,通常由二级目录的名字组成。 需要跟 registration.php 中的名字一致。

这串命令最好做成 shell 脚本,放到 magento 根目录下,方便一键调用。

继续阅读 🌳

Magento 2 主题定制化开发系列教程

Golang JWT Token 升级之二,RegisteredClaims 的使用细节

2025-08-26 12:01:52

今天,继续昨天的系列 Golang JWT 库升级,RegisteredClaims 取代 StandardClaims。 不得不说,golang-jwt/jwt 官方文档太晦涩了,很多细节都需要自己去探索。

RegisteredClaims 过期时间的设置

之前的 StandardClaims 的 exp 字段是 int64 类型,表示 Unix 时间戳。例如:

claims["exp"] = time.Now().Add(time.Second * sec).Unix()   // 废弃 ⚠️

但是,最新的 RegisteredClaims 的 exp 字段(ExpiresAt)是 *jwt.NumericDate 类型:

type NumericDate struct {
    time.Time
}

type RegisteredClaims struct {
    Issuer string `json:"iss,omitempty"`
    Subject string `json:"sub,omitempty"`
    Audience ClaimStrings `json:"aud,omitempty"`
    ExpiresAt *NumericDate `json:"exp,omitempty"`
    NotBefore *NumericDate `json:"nbf,omitempty"`
    IssuedAt *NumericDate `json:"iat,omitempty"`
    ID string `json:"jti,omitempty"`
}

设置 ExpiresAt 字段示例:

now := time.Now()
claims := &jwt.RegisteredClaims{
    Issuer: "Test",
    ExpiresAt: &jwt.NumericDate{Time: now.Add(time.Hour * 24)},
}

为何这里使用 *NumericDate

这是一个非常有趣的问题,为何要使用 *NumericDate 代表时间,而不是直接使用 *time.Time 或者 time.Time,或者是 int64 呢?

如果直接使用 time.Time,会导致序列化之后,变成了字符串形式,形如:"2024-12-31T00:00:00Z"。 但是 JWT Token 的 RFC 7519 规范中,要求 exp 字段必须是一个数字,表示 Unix 时间戳。单位为秒。 封装一层,可以方便的自定义序列化和反序列化逻辑。

使用指针 (*NumericDate) 的原因:区分“零值”和“未设置”。 这是一个非常经典的Go语言设计模式,用于处理可选字段。 如果一个 time.Time 字段是零值,我们无法判断是用户故意设置了一个远古的日期,还是这个字段根本就没设置。 对于 exp 来说,0 是一个有意义的值(1970年1月1日),但它也经常是未初始化变量的值。如果将其编码到JWT中,Token会在1970年就“过期”,这显然是错误的。

使用指针 (*NumericDate) 可以完美解决这个问题:

  • 如果指针为 nil: 表示这个声明未被设置。在序列化(编码)成JWT时,这个字段会被完全忽略(因为标签有 omitempty)。
  • 如果指针指向一个有效的 NumericDate: 表示这个声明已被显式设置。在序列化时,会将其编码为对应的数字时间戳。

至于为何不使用 int64,估计是为了减少类型转换的麻烦。

不过,目前的实现方案确实有点绕,费脑子。

MapClaims: 完全自定义的 Claims

如果不想使用 RegisteredClaims 的内置字段,可以使用 MapClaims 来完全自定义 Claims 字段。

var (
  key *ecdsa.PrivateKey
  t   *jwt.Token
  s   string
)

key = /* Load key from somewhere, for example a file */
t = jwt.NewWithClaims(jwt.SigningMethodES256,
  jwt.MapClaims{
    "iss": "my-auth-server",
    "sub": "john",
    "foo": 2,
  })
s = t.SignedString(key)

但是,我的使用场景来看,RegisteredClaims 的内置字段已经足够满足需求了。

扩展字段

或者想基于 RegisteredClaims 添加自定义字段,可以这样做:

type MyCustomClaims struct {
	UID  int    `json:"uid"`
	Role string `json:"role"`
	jwt.RegisteredClaims
}

参考资料

  • Github 仓库:https://github.com/golang-jwt/jwt
  • 如何创建 JWT Token: https://golang-jwt.github.io/jwt/usage/create/
  • 解析及验证:https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Hmac