This commit is contained in:
2026-04-02 15:34:18 +08:00
parent 6697955054
commit c54b75611d
18 changed files with 2281 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
package httpserver
import (
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"dcalc-go/internal/config"
"dcalc-go/internal/model"
"dcalc-go/internal/service"
)
type Dependencies struct {
Config config.Config
AdventureService *service.AdventureService
OpenAPIService *service.OpenAPIService
ColgService *service.ColgService
TokenService *service.TokenService
CharacterService *service.CharacterService
SkillTreeService *service.SkillTreeService
CalcService *service.CalcService
}
func NewRouter(dep Dependencies) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Compress(5, "application/json"))
r.Use(cors)
r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, model.Success(map[string]any{"service": "dcalc-go", "debug": dep.Config.DebugMode}))
})
r.Route("/api", func(r chi.Router) {
r.Get("/adventure", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.AdventureService.AdventureList()
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/token/get/{alter}", func(w http.ResponseWriter, r *http.Request) {
token, err := dep.TokenService.Create(chi.URLParam(r, "alter"), r.URL.Query().Get("equVersion"))
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(400, err.Error(), nil))
return
}
writeJSON(w, http.StatusOK, model.Success(token))
})
r.Get("/character", func(w http.ResponseWriter, r *http.Request) {
payload, ok := requireAlterToken(w, r, dep.TokenService)
if !ok {
return
}
data, err := dep.CharacterService.StaticInfo(payload.Alter, payload.EquVersion)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Post("/calc", func(w http.ResponseWriter, r *http.Request) {
payload, ok := requireAlterToken(w, r, dep.TokenService)
if !ok {
return
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, http.ErrBodyNotAllowed) {
writeJSON(w, http.StatusBadRequest, model.Error(400, "invalid request body", nil))
return
}
data, err := dep.CalcService.Calculate(payload.Alter, payload.EquVersion, body)
if err != nil {
writeNotImplemented(w, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/skill/{skillId}/{level}", func(w http.ResponseWriter, r *http.Request) {
payload, ok := requireAlterToken(w, r, dep.TokenService)
if !ok {
return
}
level, err := strconv.Atoi(chi.URLParam(r, "level"))
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(400, "level must be an integer", nil))
return
}
data, err := dep.OpenAPIService.SkillDetailByAlter(payload.Alter, chi.URLParam(r, "skillId"), level)
if err != nil {
writeJSON(w, http.StatusNotFound, model.Error(404, err.Error(), nil))
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/dnfhelper/{uid}/", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusNotImplemented, model.Error(501, "dnfhelper integration is pending migration", nil))
})
r.Post("/skillTree/", func(w http.ResponseWriter, r *http.Request) {
payload, ok := requireAlterToken(w, r, dep.TokenService)
if !ok {
return
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
writeJSON(w, http.StatusBadRequest, model.Error(400, "invalid request body", nil))
return
}
code, _ := body["code"].(string)
result, err := dep.SkillTreeService.Parse(code, payload.EquVersion)
if err != nil {
writeJSON(w, http.StatusInternalServerError, model.Error(500, "解析技能加点失败", nil))
return
}
if result.Job["class"] != payload.Alter {
writeJSON(w, http.StatusInternalServerError, model.Error(500, "技能加点代码职业与登录职业不匹配", nil))
return
}
writeJSON(w, http.StatusOK, model.Success(result))
})
r.Route("/open", func(r chi.Router) {
r.Get("/skills/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.OpenAPIService.SearchSkills(r.URL.Query().Get("skillName"))
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/jobInfo/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.OpenAPIService.JobsInfo(r.URL.Query().Get("jobName"))
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/skillData/summary/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.OpenAPIService.SkillDataSummary(
r.URL.Query().Get("job"),
splitCSV(r.URL.Query().Get("skills")),
splitCSV(r.URL.Query().Get("weapons")),
)
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/gold/realtime/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.ColgService.RealTimeGold(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/gold/history/", func(w http.ResponseWriter, r *http.Request) {
region, err := strconv.Atoi(r.URL.Query().Get("region"))
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(400, "region must be an integer", nil))
return
}
timeRange := 7
if raw := r.URL.Query().Get("timeRange"); raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(400, "timeRange must be an integer", nil))
return
}
timeRange = parsed
}
data, err := dep.ColgService.HistoryGold(r.Context(), region, timeRange)
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(404, err.Error(), nil))
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/gold/official/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.ColgService.OfficialGold(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/{jobId}/{jobGrowId}/skills/", func(w http.ResponseWriter, r *http.Request) {
data, err := dep.OpenAPIService.SkillsByJob(chi.URLParam(r, "jobId"), chi.URLParam(r, "jobGrowId"))
if err != nil {
writeError(w, http.StatusNotFound, err)
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
r.Get("/{jobId}/{jobGrowId}/{skillId}/", func(w http.ResponseWriter, r *http.Request) {
var level *int
if raw := r.URL.Query().Get("level"); raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil {
writeJSON(w, http.StatusBadRequest, model.Error(400, "level must be an integer", nil))
return
}
level = &parsed
}
data, err := dep.OpenAPIService.SkillDetail(chi.URLParam(r, "jobId"), chi.URLParam(r, "jobGrowId"), chi.URLParam(r, "skillId"), level)
if err != nil {
writeJSON(w, http.StatusNotFound, model.Error(404, err.Error(), nil))
return
}
writeJSON(w, http.StatusOK, model.Success(data))
})
})
})
return r
}
func requireAlterToken(w http.ResponseWriter, r *http.Request, tokens *service.TokenService) (service.AlterTokenPayload, bool) {
token := r.Header.Get("alter_token")
if token == "" {
writeJSON(w, http.StatusUnauthorized, model.Error(401, "missing alter_token header", nil))
return service.AlterTokenPayload{}, false
}
payload, err := tokens.Parse(token)
if err != nil {
writeJSON(w, http.StatusUnauthorized, model.Error(401, "invalid alter_token header", nil))
return service.AlterTokenPayload{}, false
}
return payload, true
}
func writeNotImplemented(w http.ResponseWriter, err error) {
writeJSON(w, http.StatusNotImplemented, model.Error(501, err.Error(), nil))
}
func writeError(w http.ResponseWriter, status int, err error) {
writeJSON(w, status, model.Error(status, err.Error(), nil))
}
func writeJSON(w http.ResponseWriter, status int, payload model.Envelope) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, alter_token")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func splitCSV(value string) []string {
if value == "" {
return nil
}
parts := strings.Split(value, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}