直出页面时 POST 表单刷新会丢/重复提交。改为 PRG: submit.php 建单后 302 到 /cashier.php?trade_no=(后端渲染同一 收银台页,按订单当前状态展示,刷新安全);nginx 放行 cashier.php。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
134 lines
4.8 KiB
Go
134 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/cyi-cc/fun"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/cyi-cc/cyi-box/backend/internal/config"
|
|
"github.com/cyi-cc/cyi-box/backend/internal/payx"
|
|
"github.com/cyi-cc/cyi-box/backend/internal/service"
|
|
"github.com/cyi-cc/cyi-box/backend/internal/store"
|
|
)
|
|
|
|
// mustWired 装配失败即退出:基础设施缺一不可,粘性错误由 fun 容器返回
|
|
func mustWired[T any](v *T, err error) *T {
|
|
if err != nil {
|
|
log.Fatalf("boot: wired %T failed: %v", v, err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func main() {
|
|
cfg := mustWired(fun.Wired[config.Config]())
|
|
st := mustWired(fun.Wired[store.Store]())
|
|
|
|
// 首启种子:空库写 admin + 示例工具
|
|
if hash, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcrypt.DefaultCost); err == nil {
|
|
if err := st.SeedAdmin(string(hash)); err != nil {
|
|
log.Fatalf("boot: seed admin failed: %v", err)
|
|
}
|
|
}
|
|
if err := st.SeedTools(); err != nil {
|
|
log.Fatalf("boot: seed tools failed: %v", err)
|
|
}
|
|
|
|
f := fun.GetFun()
|
|
if len(cfg.CorsOrigins) > 0 {
|
|
f.CORS(cfg.CorsOrigins...)
|
|
}
|
|
if err := f.BindGuard(&service.AuthGuard{}); err != nil {
|
|
log.Fatalf("boot: bind guard failed: %v", err)
|
|
}
|
|
for _, err := range []error{
|
|
f.BindService(&service.AuthSvc{}),
|
|
f.BindService(&service.UserSvc{}),
|
|
f.BindService(&service.DashboardSvc{}),
|
|
f.BindService(&service.ToolSvc{}),
|
|
f.BindService(&service.VaultSvc{}),
|
|
f.BindService(&service.BookmarkSvc{}),
|
|
f.BindService(&service.ServerSvc{}),
|
|
f.BindService(&service.DbMgrSvc{}),
|
|
f.BindService(&service.WebSvc{}),
|
|
f.BindService(&service.DiskSvc{}),
|
|
f.BindService(&service.ProxySvc{}),
|
|
f.BindService(&service.SettingSvc{}),
|
|
f.BindService(&service.PaySvc{}),
|
|
f.BindService(&service.LicenseSvc{}),
|
|
f.BindService(&service.MemorySvc{}),
|
|
} {
|
|
if err != nil {
|
|
log.Fatalf("boot: bind service failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// 支付:上游客户端 + 订单轮询 + 商户通知(单例)
|
|
up := mustWired(fun.Wired[payx.Client]())
|
|
mustWired(fun.Wired[service.PayNotifier]())
|
|
mustWired(fun.Wired[service.PayPoller]())
|
|
service.BootPay(st, up, cfg)
|
|
|
|
// SFTP 上传放宽请求体上限(默认 4MB → 512MB)
|
|
f.SetBodyLimit(512 << 20)
|
|
for _, err := range []error{
|
|
f.BindRoute("GET", "/files/download", service.SftpDownloadHandler(st, cfg)),
|
|
f.BindRoute("POST", "/files/upload", service.SftpUploadHandler(st, cfg)),
|
|
f.BindRoute("GET", "/ws/terminal", service.TerminalHandler(st, cfg)),
|
|
f.BindRoute("POST", "/dbm/export", service.DbExportHandler(st, cfg)),
|
|
f.BindRoute("POST", "/dbm/import", service.DbImportHandler(st, cfg)),
|
|
f.BindRoute("POST", "/disk/upload", service.DiskUploadHandler(st, cfg)),
|
|
f.BindRoute("GET", "/disk/download", service.DiskDownloadHandler(st, cfg)),
|
|
f.BindRoute("GET", "/d/*", service.ShareDownloadHandler(st, cfg)),
|
|
f.BindRoute("GET", "/v1/proxy", service.ProxyExtractHandler(st)),
|
|
f.BindRoute("GET", "/v1/license", service.LicenseVerifyHandler(st)),
|
|
f.BindRoute("POST", "/v1/license", service.LicenseVerifyHandler(st)),
|
|
// 共享记忆库:MCP 协议端点 + 公开阅读链接
|
|
f.BindRoute("POST", "/mcp", service.McpHandler(st)),
|
|
f.BindRoute("GET", "/m", service.MemoryReadHandler(st)),
|
|
f.BindRoute("GET", "/m/*", service.MemoryReadHandler(st)),
|
|
// 易支付协议端点(GET/POST 同路由)
|
|
f.BindRoute("GET", "/submit.php", service.PaySubmitHandler(st, up)),
|
|
f.BindRoute("POST", "/submit.php", service.PaySubmitHandler(st, up)),
|
|
f.BindRoute("GET", "/mapi.php", service.PayMapiHandler(st, up)),
|
|
f.BindRoute("POST", "/mapi.php", service.PayMapiHandler(st, up)),
|
|
f.BindRoute("GET", "/api.php", service.PayApiHandler(st)),
|
|
f.BindRoute("POST", "/api.php", service.PayApiHandler(st)),
|
|
// 收银台页(submit.php 的 302 落点)+ 页内状态轮询(经代理走 /api 前缀,直连后端也兼容)
|
|
f.BindRoute("GET", "/cashier.php", service.PayCashierPageHandler(st)),
|
|
f.BindRoute("GET", "/cashier-status", service.PayCashierStatusHandler(st)),
|
|
f.BindRoute("GET", "/api/cashier-status", service.PayCashierStatusHandler(st)),
|
|
f.BindRoute("GET", "/healthz", func(c *fun.RouteCtx) error {
|
|
c.RequestCtx.WriteString("ok")
|
|
return nil
|
|
}),
|
|
} {
|
|
if err != nil {
|
|
log.Fatalf("boot: bind route failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// 代理池后台调度:每小时测活、每日 00:00 全量同步
|
|
schedCtx, stopSched := context.WithCancel(context.Background())
|
|
service.StartProxyScheduler(schedCtx, st)
|
|
|
|
// 优雅停机:SIGINT/SIGTERM → 等在途请求收尾
|
|
go func() {
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
<-ch
|
|
stopSched()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
_ = f.Shutdown(ctx)
|
|
}()
|
|
|
|
log.Printf("cyi-box backend listening on :%d, db=%s", cfg.Port, cfg.DBPath)
|
|
f.Start(cfg.Port)
|
|
}
|