Refactor import paths to use internal/pkg/httpx/params

- Updated multiple repository and service files to replace imports from "github.com/mlogclub/simple/web/params" with "cs-agent/internal/pkg/httpx/params".
- Adjusted context handling in auth_service and ws_service to use gin.Context instead of iris.Context.
- Ensured consistent usage of HTTP status responses across websocket handlers.
This commit is contained in:
mlogclub
2026-05-23 22:10:20 +08:00
parent f8b1ed42fa
commit 79614b2d07
140 changed files with 1117 additions and 595 deletions
+137
View File
@@ -0,0 +1,137 @@
package ginx
import (
"net/http"
"reflect"
"strconv"
"strings"
"unicode"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/web"
)
var jsonResultType = reflect.TypeOf((*web.JsonResult)(nil))
func HandleController(group *gin.RouterGroup, relativePath string, prototype any, handlers ...gin.HandlerFunc) {
router := group.Group(relativePath, handlers...)
t := reflect.TypeOf(prototype)
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
panic("ginx.HandleController requires a pointer to a controller struct")
}
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
httpMethods, path, ok := parseAction(method)
if !ok {
continue
}
handler := buildHandler(t, method)
for _, httpMethod := range httpMethods {
router.Handle(httpMethod, path, handler)
}
}
}
func parseAction(method reflect.Method) ([]string, string, bool) {
prefixes := []struct {
name string
methods []string
}{
{"Any", []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions}},
{"Get", []string{http.MethodGet}},
{"Post", []string{http.MethodPost}},
{"Put", []string{http.MethodPut}},
{"Delete", []string{http.MethodDelete}},
}
for _, prefix := range prefixes {
if !strings.HasPrefix(method.Name, prefix.name) {
continue
}
suffix := strings.TrimPrefix(method.Name, prefix.name)
return prefix.methods, actionPath(suffix, method.Type.NumIn()-1), true
}
return nil, "", false
}
func actionPath(suffix string, argCount int) string {
if suffix == "" {
return "/"
}
if suffix == "By" && argCount == 1 {
return "/:id"
}
if strings.HasSuffix(suffix, "By") && argCount == 1 {
base := strings.TrimSuffix(suffix, "By")
return "/" + actionSegmentPath(base) + "/:id"
}
return "/" + actionSegmentPath(suffix)
}
func actionSegmentPath(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
for i, part := range parts {
parts[i] = camelToPath(part)
}
return strings.Join(parts, "_")
}
func camelToPath(s string) string {
var b strings.Builder
for i, r := range s {
if unicode.IsUpper(r) {
if i > 0 {
b.WriteByte('/')
}
r = unicode.ToLower(r)
}
b.WriteRune(r)
}
return b.String()
}
func buildHandler(controllerType reflect.Type, method reflect.Method) gin.HandlerFunc {
return func(ctx *gin.Context) {
controller := reflect.New(controllerType.Elem())
if field := controller.Elem().FieldByName("Ctx"); field.IsValid() && field.CanSet() {
field.Set(reflect.ValueOf(ctx))
}
args := []reflect.Value{controller}
for i := 1; i < method.Type.NumIn(); i++ {
argType := method.Type.In(i)
raw := ctx.Param("id")
value, ok := convertPathArg(raw, argType)
if !ok {
ctx.JSON(http.StatusBadRequest, web.JsonErrorMsg("路径参数错误"))
return
}
args = append(args, value)
}
results := method.Func.Call(args)
if len(results) == 0 || results[0].IsNil() {
return
}
if result, ok := results[0].Interface().(*web.JsonResult); ok {
ctx.JSON(http.StatusOK, result)
}
}
}
func convertPathArg(raw string, t reflect.Type) (reflect.Value, bool) {
switch t.Kind() {
case reflect.Int64:
v, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return reflect.Value{}, false
}
return reflect.ValueOf(v), true
case reflect.String:
return reflect.ValueOf(raw), true
default:
return reflect.Zero(t), false
}
}
@@ -1,28 +1,28 @@
package irisx
package httpx
import (
"cs-agent/internal/pkg/httpx/params"
"cs-agent/internal/pkg/openidentity"
"github.com/kataras/iris/v12"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/web/params"
)
const (
ctxKeyExternalUser = "externalUser"
)
func SetExternalUser(ctx iris.Context, ext *openidentity.ExternalUser) {
ctx.Values().Set(ctxKeyExternalUser, ext)
func SetExternalUser(ctx *gin.Context, ext *openidentity.ExternalUser) {
ctx.Set(ctxKeyExternalUser, ext)
}
func GetExternalUser(ctx iris.Context) *openidentity.ExternalUser {
v := ctx.Values().Get(ctxKeyExternalUser)
func GetExternalUser(ctx *gin.Context) *openidentity.ExternalUser {
v, _ := ctx.Get(ctxKeyExternalUser)
ext, _ := v.(*openidentity.ExternalUser)
return ext
}
func GetChannelID(ctx iris.Context) string {
func GetChannelID(ctx *gin.Context) string {
if channelID := ctx.GetHeader("X-Channel-ID"); strs.IsNotBlank(channelID) {
return channelID
}
+270
View File
@@ -0,0 +1,270 @@
package params
import (
"errors"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/gorilla/schema"
"github.com/mlogclub/simple/common/dates"
"github.com/mlogclub/simple/common/jsons"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"github.com/spf13/cast"
)
var (
decoder = schema.NewDecoder()
validate = validator.New()
)
func init() {
decoder.SetAliasTag("form")
decoder.ZeroEmpty(true)
decoder.IgnoreUnknownKeys(true)
}
func paramError(name string) error {
return fmt.Errorf("unable to find param value '%s'", name)
}
func ReadForm(ctx *gin.Context, obj any) error {
if ctx == nil {
return errors.New("request context is nil")
}
if err := ctx.Request.ParseForm(); err != nil {
return err
}
values := ctx.Request.Form
if len(values) == 0 {
if err := ctx.Request.ParseMultipartForm(32 << 20); err != nil && !errors.Is(err, http.ErrNotMultipart) {
return err
}
values = ctx.Request.Form
}
if len(values) == 0 {
return nil
}
if err := decoder.Decode(obj, values); err != nil {
return err
}
return validate.Struct(obj)
}
func ReadJSON(ctx *gin.Context, obj any) error {
if ctx == nil {
return errors.New("request context is nil")
}
if err := ctx.ShouldBindJSON(obj); err != nil {
return err
}
return validate.Struct(obj)
}
func Get(ctx *gin.Context, name string) (string, bool) {
str := FormValue(ctx, name)
return str, str != ""
}
func GetInt64(ctx *gin.Context, name string) (int64, bool) {
str, ok := Get(ctx, name)
if !ok {
return 0, false
}
value, err := cast.ToInt64E(str)
if err != nil {
return 0, false
}
return value, true
}
func GetInt(ctx *gin.Context, name string) (int, bool) {
str, ok := Get(ctx, name)
if !ok {
return 0, false
}
value, err := cast.ToIntE(str)
if err != nil {
return 0, false
}
return value, true
}
func GetBool(ctx *gin.Context, name string) (bool, bool) {
str, ok := Get(ctx, name)
if !ok {
return false, false
}
value, err := cast.ToBoolE(str)
if err != nil {
return false, false
}
return value, true
}
func GetTime(ctx *gin.Context, name string) *time.Time {
value, _ := Get(ctx, name)
if strs.IsBlank(value) {
return nil
}
layouts := []string{dates.FmtDateTime, dates.FmtDate, dates.FmtDateTimeNoSeconds}
for _, layout := range layouts {
if ret, err := dates.Parse(value, layout); err == nil {
return &ret
}
}
return nil
}
func GetInt64Arr(ctx *gin.Context, name string) []int64 {
str, ok := Get(ctx, name)
if !ok {
return nil
}
str = strings.TrimSpace(str)
if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") {
var ret []int64
if err := jsons.Parse(str, &ret); err != nil {
slog.Error(err.Error())
}
return ret
}
return StrSplitToInt64Arr(str)
}
func StrSplitToInt64Arr(str string) (ret []int64) {
if strs.IsBlank(str) {
return ret
}
for _, s := range strings.Split(str, ",") {
i, err := cast.ToInt64E(strings.TrimSpace(s))
if err == nil {
ret = append(ret, i)
}
}
return ret
}
func FormValue(ctx *gin.Context, name string) string {
if ctx == nil {
return ""
}
if value := ctx.PostForm(name); value != "" {
return value
}
return ctx.Query(name)
}
func FormValueRequired(ctx *gin.Context, name string) (string, error) {
str := FormValue(ctx, name)
if len(str) == 0 {
return "", errors.New("参数:" + name + "不能为空")
}
return str, nil
}
func FormValueDefault(ctx *gin.Context, name, def string) string {
if value := FormValue(ctx, name); value != "" {
return value
}
return def
}
func FormValueInt(ctx *gin.Context, name string) (int, error) {
str := FormValue(ctx, name)
if str == "" {
return 0, paramError(name)
}
return strconv.Atoi(str)
}
func FormValueIntDefault(ctx *gin.Context, name string, def int) int {
if v, err := FormValueInt(ctx, name); err == nil {
return v
}
return def
}
func FormValueInt64(ctx *gin.Context, name string) (int64, error) {
str := FormValue(ctx, name)
if str == "" {
return 0, paramError(name)
}
return strconv.ParseInt(str, 10, 64)
}
func FormValueInt64Default(ctx *gin.Context, name string, def int64) int64 {
if v, err := FormValueInt64(ctx, name); err == nil {
return v
}
return def
}
func FormValueInt64Array(ctx *gin.Context, name string) []int64 {
str := strings.TrimSpace(FormValue(ctx, name))
if strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]") {
var ret []int64
if err := jsons.Parse(str, &ret); err != nil {
slog.Error(err.Error())
}
return ret
}
return StrSplitToInt64Arr(str)
}
func FormValueStringArray(ctx *gin.Context, name string) []string {
str := FormValue(ctx, name)
if len(str) == 0 {
return nil
}
var ret []string
for _, s := range strings.Split(str, ",") {
s = strings.TrimSpace(s)
if len(s) > 0 {
ret = append(ret, s)
}
}
return ret
}
func FormValueBool(ctx *gin.Context, name string) (bool, error) {
str := FormValue(ctx, name)
if str == "" {
return false, paramError(name)
}
return strconv.ParseBool(str)
}
func FormValueBoolDefault(ctx *gin.Context, name string, def bool) bool {
str := FormValue(ctx, name)
if str == "" {
return def
}
value, err := strconv.ParseBool(str)
if err != nil {
return def
}
return value
}
func FormDate(ctx *gin.Context, name string) *time.Time {
return GetTime(ctx, name)
}
func GetPaging(ctx *gin.Context) *sqls.Paging {
page := FormValueIntDefault(ctx, "page", 1)
limit := FormValueIntDefault(ctx, "limit", 20)
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 20
}
return &sqls.Paging{Page: page, Limit: limit}
}
@@ -0,0 +1,79 @@
package params
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/common/strs/strcase"
"github.com/mlogclub/simple/sqls"
)
type QueryOp string
const (
Eq QueryOp = "eq"
Gt QueryOp = "gt"
Lt QueryOp = "lt"
Gte QueryOp = "gte"
Lte QueryOp = "lte"
Like QueryOp = "like"
In QueryOp = "in"
Starting QueryOp = "starting"
Ending QueryOp = "ending"
)
type QueryFilter struct {
ParamName string
Op QueryOp
ColumnName string
ValueWrapper func(origin string) string
}
func NewPagedSqlCnd(ctx *gin.Context, filters ...QueryFilter) *sqls.Cnd {
cnd := NewSqlCnd(ctx, filters...)
p := GetPaging(ctx)
cnd.Page(p.Page, p.Limit)
return cnd
}
func NewSqlCnd(ctx *gin.Context, filters ...QueryFilter) *sqls.Cnd {
cnd := sqls.NewCnd()
for _, filter := range filters {
columnName := filter.ColumnName
paramValue := FormValue(ctx, filter.ParamName)
if strs.IsBlank(string(filter.Op)) {
filter.Op = Eq
}
if filter.ValueWrapper != nil {
paramValue = filter.ValueWrapper(paramValue)
}
if strs.IsBlank(paramValue) {
continue
}
if strs.IsBlank(columnName) {
columnName = strcase.ToSnake(filter.ParamName)
}
switch filter.Op {
case Eq:
cnd.Eq(columnName, paramValue)
case Gt:
cnd.Gt(columnName, paramValue)
case Lt:
cnd.Lt(columnName, paramValue)
case Gte:
cnd.Gte(columnName, paramValue)
case Lte:
cnd.Lte(columnName, paramValue)
case Like:
cnd.Like(columnName, paramValue)
case Starting:
cnd.Starting(columnName, paramValue)
case Ending:
cnd.Ending(columnName, paramValue)
case In:
cnd.In(columnName, strings.Split(paramValue, ","))
}
}
return cnd
}
+106
View File
@@ -0,0 +1,106 @@
package params
import (
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/common/strs/strcase"
"github.com/mlogclub/simple/sqls"
)
type QueryParams struct {
Ctx *gin.Context
sqls.Cnd
}
func NewQueryParams(ctx *gin.Context) *QueryParams {
return &QueryParams{Ctx: ctx}
}
func (q *QueryParams) getValueByColumn(column string) string {
if q.Ctx == nil {
return ""
}
return FormValue(q.Ctx, strcase.ToLowerCamel(column))
}
func (q *QueryParams) EqByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Eq(column, value)
}
return q
}
func (q *QueryParams) NotEqByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.NotEq(column, value)
}
return q
}
func (q *QueryParams) GtByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Gt(column, value)
}
return q
}
func (q *QueryParams) GteByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Gte(column, value)
}
return q
}
func (q *QueryParams) LtByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Lt(column, value)
}
return q
}
func (q *QueryParams) LteByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Lte(column, value)
}
return q
}
func (q *QueryParams) LikeByReq(column string) *QueryParams {
if value := q.getValueByColumn(column); len(value) > 0 {
q.Like(column, value)
}
return q
}
func (q *QueryParams) PageByReq() *QueryParams {
if q.Ctx == nil {
return q
}
paging := GetPaging(q.Ctx)
q.Page(paging.Page, paging.Limit)
return q
}
func (q *QueryParams) Asc(column string) *QueryParams {
q.Orders = append(q.Orders, sqls.OrderByCol{Column: column, Asc: true})
return q
}
func (q *QueryParams) Desc(column string) *QueryParams {
q.Orders = append(q.Orders, sqls.OrderByCol{Column: column, Asc: false})
return q
}
func (q *QueryParams) Limit(limit int) *QueryParams {
q.Page(1, limit)
return q
}
func (q *QueryParams) Page(page, limit int) *QueryParams {
if q.Paging == nil {
q.Paging = &sqls.Paging{Page: page, Limit: limit}
} else {
q.Paging.Page = page
q.Paging.Limit = limit
}
return q
}
+7 -7
View File
@@ -7,10 +7,10 @@ import (
"net/url"
"strings"
"cs-agent/internal/pkg/httpx/params"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/kataras/iris/v12"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/web/params"
)
// ExternalUser 外部访客身份(IM 客户),与站内 AuthPrincipal 区分。
@@ -26,7 +26,7 @@ type UserTokenClaims struct {
jwt.RegisteredClaims
}
func GetExternalUser(ctx iris.Context, secret string) (*ExternalUser, error) {
func GetExternalUser(ctx *gin.Context, secret string) (*ExternalUser, error) {
if userToken := getUserToken(ctx); strs.IsNotBlank(userToken) {
claims, err := verifyUserToken(userToken, secret)
if err != nil {
@@ -83,7 +83,7 @@ func verifyUserToken(userToken, secret string) (*UserTokenClaims, error) {
return claims, nil
}
func getUserToken(ctx iris.Context) string {
func getUserToken(ctx *gin.Context) string {
auth := strings.TrimSpace(ctx.GetHeader("Authorization"))
if len(auth) > 7 && strings.EqualFold(auth[:7], "Bearer ") {
if token := strings.TrimSpace(auth[7:]); token != "" {
@@ -94,7 +94,7 @@ func getUserToken(ctx iris.Context) string {
return strings.TrimSpace(userToken)
}
func getGuestUser(ctx iris.Context) (*ExternalUser, error) {
func getGuestUser(ctx *gin.Context) (*ExternalUser, error) {
externalID := getExternalID(ctx)
if strs.IsBlank(externalID) {
return nil, errorsx.Unauthorized("用户标识不能为空")
@@ -106,7 +106,7 @@ func getGuestUser(ctx iris.Context) (*ExternalUser, error) {
}, nil
}
func getExternalID(ctx iris.Context) string {
func getExternalID(ctx *gin.Context) string {
externalID := ctx.GetHeader("X-External-Id")
if strs.IsBlank(externalID) {
externalID, _ = params.Get(ctx, "externalId")
@@ -114,7 +114,7 @@ func getExternalID(ctx iris.Context) string {
return externalID
}
func getExternalName(ctx iris.Context) string {
func getExternalName(ctx *gin.Context) string {
externalName := ctx.GetHeader("X-External-Name")
if strs.IsBlank(externalName) {
externalName, _ = params.Get(ctx, "externalName")