119 lines
5.9 KiB
Go
119 lines
5.9 KiB
Go
package cmd
|
||
|
||
import (
|
||
"context"
|
||
|
||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||
"github.com/gogf/gf/v2/errors/gerror"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/net/ghttp"
|
||
"github.com/gogf/gf/v2/os/gcmd"
|
||
"github.com/gogf/gf/v2/os/gcron"
|
||
"github.com/gogf/gf/v2/os/gfile"
|
||
|
||
"tool-api/internal/controller"
|
||
"tool-api/internal/logic"
|
||
)
|
||
|
||
// GoFrame gcron 的 pattern 是 **6 段**(秒 分 时 日 月 周),不是标准 crontab 的 5 段。
|
||
//
|
||
// ⚠️ 写成 5 段既不会 panic 也不会启动失败,只会让 AddSingleton 返回 error 并被下面的
|
||
// WARNING 吞掉 —— 任务静默不注册、功能完全失效(本项目曾把「笔记提醒扫描」写成
|
||
// "*/1 * * * *" 而无人察觉)。故把 pattern 提为常量,并由 cmd_test.go 固化校验。
|
||
// 若确实要用标准 5 段 crontab,需前置一个 `#`(如 "# */1 * * * *")声明忽略秒。
|
||
const (
|
||
cronPatternSweepOrders = "0 */5 * * * *" // 会员订单扫单:每 5 分钟
|
||
cronPatternScanRemind = "0 */1 * * * *" // 笔记提醒扫描:每 1 分钟
|
||
cronPatternReleaseCoupons = "0 */5 * * * *" // 免单券超时占用释放:每 5 分钟
|
||
cronPatternCloseOrders = "0 */5 * * * *" // 订单超时关闭:每 5 分钟
|
||
)
|
||
|
||
var (
|
||
Main = gcmd.Command{
|
||
Name: "main",
|
||
Usage: "main",
|
||
Brief: "start http server",
|
||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||
// 启动时幂等迁移(建表 / 加列),数据库不可用时仅告警
|
||
logic.Migrate(ctx)
|
||
// 启动时幂等写入种子数据(管理员/模块/等级/工具/会员套餐/次数包/额度设置),数据库不可用时仅告警
|
||
logic.Seed(ctx)
|
||
|
||
// 会员订单兜底扫单:推送丢失时主动查单补发货(建议间隔 5 分钟)
|
||
if _, err = gcron.AddSingleton(ctx, cronPatternSweepOrders, func(c context.Context) {
|
||
logic.SweepPendingOrders(c)
|
||
}, "member-order-sweep"); err != nil {
|
||
g.Log().Warningf(ctx, "[cron] 会员订单扫单任务注册失败: %v", err)
|
||
} else {
|
||
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "member-order-sweep", cronPatternSweepOrders)
|
||
}
|
||
|
||
// 笔记定时提醒扫描:每分钟扫描到点的提醒并发订阅消息(条件更新保证只发一次)
|
||
if _, err = gcron.AddSingleton(ctx, cronPatternScanRemind, func(c context.Context) {
|
||
logic.ScanDueReminders(c)
|
||
}, "note-remind-scan"); err != nil {
|
||
g.Log().Warningf(ctx, "[cron] 笔记提醒扫描任务注册失败: %v", err)
|
||
} else {
|
||
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "note-remind-scan", cronPatternScanRemind)
|
||
}
|
||
|
||
// 免单券超时占用释放:订单取消/支付失败/超时(coupon.lock.ttlSeconds)后把券释放回可用
|
||
if _, err = gcron.AddSingleton(ctx, cronPatternReleaseCoupons, func(c context.Context) {
|
||
logic.ReleaseExpiredCouponLocks(c)
|
||
}, "coupon-lock-release"); err != nil {
|
||
g.Log().Warningf(ctx, "[cron] 免单券释放任务注册失败: %v", err)
|
||
} else {
|
||
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "coupon-lock-release", cronPatternReleaseCoupons)
|
||
}
|
||
|
||
// 订单超时关闭:把超时未支付的待支付单置为已关闭(status=3)并释放其占用中的券。
|
||
// 时限 orderPendingTtlSeconds 与上面「免单券释放」同源(同一 settings 键,D2)。
|
||
if _, err = gcron.AddSingleton(ctx, cronPatternCloseOrders, func(c context.Context) {
|
||
logic.ClosePendingOrders(c)
|
||
}, "order-close-pending"); err != nil {
|
||
g.Log().Warningf(ctx, "[cron] 订单超时关闭任务注册失败: %v", err)
|
||
} else {
|
||
g.Log().Infof(ctx, "[cron] 已注册任务 %s pattern=%q", "order-close-pending", cronPatternCloseOrders)
|
||
}
|
||
|
||
s := g.Server()
|
||
// 公开接口:小程序登录、开发登录、管理端登录、虚拟支付发货推送回调
|
||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||
group.Middleware(logic.CORS, logic.Response)
|
||
group.Bind(controller.UserPub, controller.AdminPub, controller.EventPub)
|
||
// 平台回调无 token,单独注册(响应体为 XML,不走统一 JSON 包装)
|
||
group.POST("/pay/notify", controller.PayNotify)
|
||
})
|
||
// 小程序用户接口
|
||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||
group.Middleware(logic.CORS, logic.Response, logic.UserAuth)
|
||
group.Bind(controller.UserAuth, controller.Member, controller.Quota, controller.Note,
|
||
controller.Promo, controller.Coupon)
|
||
})
|
||
// 管理端接口:鉴权(AdminAuth)+ 权限分级(AdminPerm,按角色权限点放行)
|
||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||
group.Middleware(logic.CORS, logic.Response, logic.AdminAuth, logic.AdminPerm)
|
||
group.Bind(controller.AdminAuth, controller.Event, controller.AdminPromo, controller.AdminCoupon, controller.AdminOrder)
|
||
})
|
||
// 活动小程序码静态资源(生成时落在 manifest/qrcode,供管理端/前端访问)
|
||
// ⚠️ AddStaticPath 在目录不存在时**直接 FATAL 退出、不降级**,而该目录原先只在
|
||
// 生成码图时才惰性创建 → 首次启动必然崩在「目录还没被创建」上(先有鸡先有蛋)。
|
||
// 故此处先幂等建目录(gfile.Mkdir = os.MkdirAll,已存在返回 nil)再注册。
|
||
if err = gfile.Mkdir(logic.QrCodeDir); err != nil {
|
||
return gerror.Wrapf(err, "预创建码图目录 %s 失败(静态路由 /qrcode 无法注册)", logic.QrCodeDir)
|
||
}
|
||
s.AddStaticPath("/qrcode", logic.QrCodeDir)
|
||
|
||
// 用户头像静态资源(N3):落盘于 manifest/static/avatar/...,由 /static 提供访问。
|
||
// 与 /qrcode 同一套坑与治法:先幂等建目录再注册静态路由(AddStaticPath 目录不存在会 FATAL)。
|
||
if err = gfile.Mkdir(logic.StaticDir); err != nil {
|
||
return gerror.Wrapf(err, "预创建静态目录 %s 失败(静态路由 /static 无法注册)", logic.StaticDir)
|
||
}
|
||
s.AddStaticPath("/static", logic.StaticDir)
|
||
|
||
s.Run()
|
||
return nil
|
||
},
|
||
}
|
||
)
|