2025-09-01 14:48:20
之前开发的大赛报名网站终于进入了收尾阶段,比赛已结束,现在需要把参赛选手上传的资料及视频文件导出,做备份。
正好借此机会了解一下如何在 golang gin 项目中添加一堆命令行工具。
之前把导出数据的功能,都放到了 gin API 接口中,然后通过 swagger 的文档管理界面调用,再保存。
但是,这样搞有个坏处,就是导致 swagger 文档中,会多出很多不需要前端使用的接口。 此外,像视频批量导出这样需要长时间执行的功能,需要更多详情日志的场景,也不适合放到 API 接口中。
所以,我需要将这些功能从 HTTP API 接口中剥离出来,放到单独的命令行工具中。然后代码目录组织就变成了一个问题 🤔
印象中 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 入口程序
如何设置 package 呢?是否可以使用 main 作为 package 名称?因为原 gin 的入口文件 main.go 就是 main 作为包名。
测试了一下确实可以的:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello world")
}
执行:
> go run main.go
Hello world
然后,我又测试了一下原 gin 项目的编译,并不影响,很好。(๑•̀ㅂ•́)و✧
跟其他包一样引用即可:
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。
等 cmd 命令多了,重合度高了之后,再引入 Cobra 也不迟。
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 的搜索方案。
一个官方的讨论
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 完全匹配,则把这个产品排在第一位。
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.
如果你既需要精确匹配(如"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.
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!
2025-08-31 22:11:51
计划把现有的 Magento 网站使用 Golang 重构一下,替换掉 PHP。 主要是 Magento 的架构太复杂了,耗服务器资源也多,改动起来异常麻烦,还不如用 golang 重写得了。 毕竟只用到了简单的产品展示功能。界面让 AI 实现一下就行,添加上 golang 逻辑即可。 再配合上用的已经很成熟的 React Ant Design Pro 的管理后台,维护成本也很低。
就叫 gogento 吧 😅
推荐使用 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 代码要少要简单。
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 配置,确实是有这条规则。
#location /catalogsearch/result/index {
# return 404;
#}
把上面的配置注释掉就可以了。
nginx -t
nginx -s reload
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 根目录下,方便一键调用。
2025-08-26 12:01:52
今天,继续昨天的系列 Golang JWT 库升级,RegisteredClaims 取代 StandardClaims。 不得不说,golang-jwt/jwt 官方文档太晦涩了,很多细节都需要自己去探索。
之前的 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 代表时间,而不是直接使用 *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) 可以完美解决这个问题:
至于为何不使用 int64,估计是为了减少类型转换的麻烦。
不过,目前的实现方案确实有点绕,费脑子。
如果不想使用 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
}