From 6ce1a303f110cc87fb85a630a6f444ff44dc9695 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Thu, 21 Nov 2024 12:14:34 +0800 Subject: [PATCH] feat(all): v1.0 --- app/order/cmd/api/desc/order.api | 24 + app/order/cmd/rpc/pb/order.proto | 5 + app/query/cmd/rpc/pb/query.proto | 17 + app/user/cmd/api/.air.toml | 6 - app/user/cmd/api/desc/auth/auth.api | 2 +- app/user/cmd/api/desc/main.api | 14 + app/user/cmd/api/desc/pay.api | 28 + app/user/cmd/api/desc/product.api | 25 + app/user/cmd/api/desc/product/product.api | 36 + app/user/cmd/api/desc/query.api | 84 +++ app/user/cmd/api/desc/query/query.api | 75 ++ app/user/cmd/api/desc/user.api | 14 +- app/user/cmd/api/desc/user/user.api | 1 - app/user/cmd/api/etc/user.yaml | 19 - app/user/cmd/api/internal/config/config.go | 34 + app/user/cmd/api/internal/handler/routes.go | 135 +++- .../api/internal/logic/auth/sendsmslogic.go | 10 +- .../api/internal/logic/user/detaillogic.go | 2 +- .../api/internal/logic/user/registerlogic.go | 5 +- .../api/internal/queue/paySuccessNotify.go | 179 +++++ app/user/cmd/api/internal/queue/routes.go | 28 + .../cmd/api/internal/service/alipayService.go | 137 ++++ .../cmd/api/internal/service/asynqService.go | 59 ++ .../internal/service/verificationService.go | 116 +++ .../api/internal/service/wechatpayService.go | 188 +++++ .../api/internal/service/westdexService.go | 707 ++++++++++++++++++ .../cmd/api/internal/svc/servicecontext.go | 60 +- app/user/cmd/api/internal/types/payload.go | 5 + app/user/cmd/api/internal/types/query.go | 61 ++ app/user/cmd/api/internal/types/queryMap.go | 47 ++ .../cmd/api/internal/types/queryParams.go | 73 ++ app/user/cmd/api/internal/types/taskname.go | 3 + app/user/cmd/api/internal/types/types.go | 93 ++- app/user/cmd/api/merchant/apiclient_key.pem | 28 + app/user/cmd/api/user.go | 31 - app/user/model/userauthmodel.go | 27 - app/user/model/userauthmodel_gen.go | 435 ----------- app/user/model/usermodel.go | 27 - app/user/model/usermodel_gen.go | 410 ---------- app/user/model/vars.go | 15 - common/xerr/errCode.go | 4 +- deploy/script/genModel.ps1 | 25 + deploy/sql/order.sql | 25 + deploy/sql/product.sql | 99 +++ deploy/sql/query.sql | 18 + deploy/sql/user.sql | 12 +- deploy/template/model/insert.tpl | 1 - deploy/template/model/update.tpl | 2 +- docker-compose-env.yml | 18 +- docker-compose.yml | 24 - go.mod | 16 +- go.sum | 34 + modd.conf | 5 - pkg/lzkit/crypto/crypto.go | 105 +++ pkg/lzkit/crypto/generate.go | 63 ++ pkg/lzkit/crypto/west_crypto.go | 150 ++++ pkg/lzkit/delay/ProgressiveDelay.go | 65 ++ pkg/lzkit/lzUtils/sqlutls.go | 38 + pkg/lzkit/lzUtils/utils.go | 15 + pkg/lzkit/validator/validator.go | 20 + test/test.go | 148 ++++ 61 files changed, 3106 insertions(+), 1046 deletions(-) create mode 100644 app/order/cmd/api/desc/order.api create mode 100644 app/order/cmd/rpc/pb/order.proto create mode 100644 app/query/cmd/rpc/pb/query.proto delete mode 100644 app/user/cmd/api/.air.toml create mode 100644 app/user/cmd/api/desc/main.api create mode 100644 app/user/cmd/api/desc/pay.api create mode 100644 app/user/cmd/api/desc/product.api create mode 100644 app/user/cmd/api/desc/product/product.api create mode 100644 app/user/cmd/api/desc/query.api create mode 100644 app/user/cmd/api/desc/query/query.api delete mode 100644 app/user/cmd/api/etc/user.yaml create mode 100644 app/user/cmd/api/internal/queue/paySuccessNotify.go create mode 100644 app/user/cmd/api/internal/queue/routes.go create mode 100644 app/user/cmd/api/internal/service/alipayService.go create mode 100644 app/user/cmd/api/internal/service/asynqService.go create mode 100644 app/user/cmd/api/internal/service/verificationService.go create mode 100644 app/user/cmd/api/internal/service/wechatpayService.go create mode 100644 app/user/cmd/api/internal/service/westdexService.go create mode 100644 app/user/cmd/api/internal/types/payload.go create mode 100644 app/user/cmd/api/internal/types/query.go create mode 100644 app/user/cmd/api/internal/types/queryMap.go create mode 100644 app/user/cmd/api/internal/types/queryParams.go create mode 100644 app/user/cmd/api/internal/types/taskname.go create mode 100644 app/user/cmd/api/merchant/apiclient_key.pem delete mode 100644 app/user/cmd/api/user.go delete mode 100644 app/user/model/userauthmodel.go delete mode 100644 app/user/model/userauthmodel_gen.go delete mode 100644 app/user/model/usermodel.go delete mode 100644 app/user/model/usermodel_gen.go delete mode 100644 app/user/model/vars.go create mode 100644 deploy/script/genModel.ps1 create mode 100644 deploy/sql/order.sql create mode 100644 deploy/sql/product.sql create mode 100644 deploy/sql/query.sql delete mode 100644 docker-compose.yml delete mode 100644 modd.conf create mode 100644 pkg/lzkit/crypto/crypto.go create mode 100644 pkg/lzkit/crypto/generate.go create mode 100644 pkg/lzkit/crypto/west_crypto.go create mode 100644 pkg/lzkit/delay/ProgressiveDelay.go create mode 100644 pkg/lzkit/lzUtils/sqlutls.go create mode 100644 pkg/lzkit/lzUtils/utils.go create mode 100644 test/test.go diff --git a/app/order/cmd/api/desc/order.api b/app/order/cmd/api/desc/order.api new file mode 100644 index 0000000..ea237ce --- /dev/null +++ b/app/order/cmd/api/desc/order.api @@ -0,0 +1,24 @@ +syntax = "v1" + +info ( + title: "支付服务" + desc: "支付服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1 + group: pay +) +service main { + // 微信支付回调 + @handler WechatPayCallback + post /pay/wechat/callback + + // 支付宝支付回调 + @handler AlipayCallback + post /pay/alipay/callback +} + diff --git a/app/order/cmd/rpc/pb/order.proto b/app/order/cmd/rpc/pb/order.proto new file mode 100644 index 0000000..a76b7e7 --- /dev/null +++ b/app/order/cmd/rpc/pb/order.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +option go_package = "./pb"; + +package pb; \ No newline at end of file diff --git a/app/query/cmd/rpc/pb/query.proto b/app/query/cmd/rpc/pb/query.proto new file mode 100644 index 0000000..5c62a92 --- /dev/null +++ b/app/query/cmd/rpc/pb/query.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option go_package = "./pb"; + +package pb; + +message StreamReq { + string name = 1; +} + +message StreamResp { + string greet = 1; +} + +service StreamGreeter { + rpc greet(StreamReq) returns (StreamResp); +} \ No newline at end of file diff --git a/app/user/cmd/api/.air.toml b/app/user/cmd/api/.air.toml deleted file mode 100644 index 5c90bb6..0000000 --- a/app/user/cmd/api/.air.toml +++ /dev/null @@ -1,6 +0,0 @@ -root = "." -[build] -cmd = "go build -o ./tmp/main ./app/user/cmd/api/user.go" # 指定 user-api 的入口文件 -bin = "./tmp/main" -include_ext = ["go", "tmpl", "html"] -exclude_dir = ["tmp", "deploy", "data"] diff --git a/app/user/cmd/api/desc/auth/auth.api b/app/user/cmd/api/desc/auth/auth.api index e290eac..f523c72 100644 --- a/app/user/cmd/api/desc/auth/auth.api +++ b/app/user/cmd/api/desc/auth/auth.api @@ -10,7 +10,7 @@ info ( type ( sendSmsReq { Mobile string `json:"mobile" validate:"required,mobile"` - ActionType string `json:"actionType" validate:"required,oneof=loginCode registerCode QueryCode"` + ActionType string `json:"actionType" validate:"required,oneof=login register query"` } ) diff --git a/app/user/cmd/api/desc/main.api b/app/user/cmd/api/desc/main.api new file mode 100644 index 0000000..b735f1d --- /dev/null +++ b/app/user/cmd/api/desc/main.api @@ -0,0 +1,14 @@ +syntax = "v1" + +info ( + title: "单体服务中心" + desc: "单体服务中心" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +import "user.api" +import "query.api" +import "pay.api" +import "product.api" diff --git a/app/user/cmd/api/desc/pay.api b/app/user/cmd/api/desc/pay.api new file mode 100644 index 0000000..38138ad --- /dev/null +++ b/app/user/cmd/api/desc/pay.api @@ -0,0 +1,28 @@ +syntax = "v1" + +info ( + title: "支付服务" + desc: "支付服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1 + group: pay +) +service main { + // 微信支付回调 + @handler WechatPayCallback + post /pay/wechat/callback + + // 支付宝支付回调 + @handler AlipayCallback + post /pay/alipay/callback + + // 微信退款回调 + @handler WechatPayRefundCallback + post /pay/wechat/refund_callback +} + diff --git a/app/user/cmd/api/desc/product.api b/app/user/cmd/api/desc/product.api new file mode 100644 index 0000000..af7aac1 --- /dev/null +++ b/app/user/cmd/api/desc/product.api @@ -0,0 +1,25 @@ +syntax = "v1" + +info ( + title: "产品服务" + desc: "产品服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) +import ( + "product/product.api" +) +@server ( + prefix: api/v1/product + group: product + jwt: JwtAuth +) +service main { + @handler GetProductByID + get /:id (GetProductByIDRequest) returns (ProductResponse) + + @handler GetProductByEn + get /en/:product_en (GetProductByEnRequest) returns (ProductResponse) +} + diff --git a/app/user/cmd/api/desc/product/product.api b/app/user/cmd/api/desc/product/product.api new file mode 100644 index 0000000..9d69933 --- /dev/null +++ b/app/user/cmd/api/desc/product/product.api @@ -0,0 +1,36 @@ +syntax = "v1" + +info ( + title: "产品查询服务" + desc: "产品查询服务" + author: "Liangzai" + email: "2440983361@qq.com" +) + +type Product { + ProductName string `json:"product_name"` + ProductEn string `json:"product_en"` + Description string `json:"description"` + Notes string `json:"notes,optional"` + SellPrice float64 `json:"sell_price"` + Features []Feature `json:"features"` // 关联功能列表 +} + +type Feature { + ID int64 `json:"id"` // 功能ID + ApiID string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 +} + +type GetProductByIDRequest { + Id int64 `path:"id"` +} + +type GetProductByEnRequest { + ProductEn string `path:"product_en"` +} + +type ProductResponse { + Product +} + diff --git a/app/user/cmd/api/desc/query.api b/app/user/cmd/api/desc/query.api new file mode 100644 index 0000000..e36dfc9 --- /dev/null +++ b/app/user/cmd/api/desc/query.api @@ -0,0 +1,84 @@ +syntax = "v1" + +info ( + title: "产品查询服务" + desc: "产品查询服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +import ( + "query/query.api" +) + +//============================> query v1 <============================ +@server ( + prefix: api/v1 + group: query + jwt: JwtAuth +) +service main { + @doc "query marriage" + @handler marriage + post /query/marriage (QueryReq) returns (QueryResp) + + // 家政服务查询 + @doc "query home service" + @handler homeService + post /query/homeService (QueryReq) returns (QueryResp) + + // 风险评估查询 + @doc "query risk assessment" + @handler riskAssessment + post /query/riskAssessment (QueryReq) returns (QueryResp) + + // 企业信息查询 + @doc "query company info" + @handler companyInfo + post /query/companyInfo (QueryReq) returns (QueryResp) + + // 租赁信息查询 + @doc "query rental info" + @handler rentalInfo + post /query/rentalInfo (QueryReq) returns (QueryResp) + + // 贷前背景调查 + @doc "query pre-loan background check" + @handler preLoanBackgroundCheck + post /query/preLoanBackgroundCheck (QueryReq) returns (QueryResp) + + // 一般背景调查 + @doc "query general background check" + @handler backgroundCheck + post /query/backgroundCheck (QueryReq) returns (QueryResp) +} + +@server ( + prefix: api/v1 + group: query + jwt: JwtAuth +) +service main { + @doc "查询示例" + @handler queryExample + get /query/example (QueryExampleReq) returns (QueryExampleResp) + + @doc "查询列表" + @handler queryList + get /query/list (QueryListReq) returns (QueryListResp) + + @doc "查询详情 按订单号 付款查询时" + @handler queryDetailByOrderId + get /query/orderId/:order_id (QueryDetailByOrderIdReq) returns (QueryDetailByOrderIdResp) + + @doc "查询详情" + @handler queryDetail + get /query/:id (QueryDetailReq) returns (QueryDetailResp) + + @doc "重试查询" + @handler queryRetry + post /query/retry/:id (QueryRetryReq) returns (QueryRetryResp) + + +} diff --git a/app/user/cmd/api/desc/query/query.api b/app/user/cmd/api/desc/query/query.api new file mode 100644 index 0000000..d317cb6 --- /dev/null +++ b/app/user/cmd/api/desc/query/query.api @@ -0,0 +1,75 @@ +syntax = "v1" + +info ( + title: "产品查询服务" + desc: "产品查询服务" + author: "Liangzai" + email: "2440983361@qq.com" +) + +type ( + QueryReq { + Data string `json:"data" validate:"required"` + } + QueryResp { + prepayID string `json:"prepay_id"` + OrderID int64 `json:"order_id"` + } +) + +type Query { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductId int64 `json:"product_id"` // 产品ID + QueryData []map[string]interface{} `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type ( + QueryListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + QueryListResp { + Total int64 `json:"total"` // 总记录数 + List []Query `json:"list"` // 查询列表 + } +) + +type ( + QueryExampleReq { + feature string `form:"feature"` + } + QueryExampleResp { + Query + } +) + +type ( + QueryDetailReq { + Id int64 `path:"id"` + } + QueryDetailResp { + Query + } +) + +type ( + QueryDetailByOrderIdReq { + OrderId int64 `path:"order_id"` + } + QueryDetailByOrderIdResp { + Query + } +) + +type ( + QueryRetryReq { + Id int64 `path:"id"` + } + QueryRetryResp {} +) + diff --git a/app/user/cmd/api/desc/user.api b/app/user/cmd/api/desc/user.api index 7af7cc4..e5a65fe 100644 --- a/app/user/cmd/api/desc/user.api +++ b/app/user/cmd/api/desc/user.api @@ -16,10 +16,10 @@ import ( //============================> user v1 <============================ //no need login @server ( - prefix: user/v1 + prefix: api/v1 group: user ) -service user { +service main { @doc "register" @handler register post /user/register (RegisterReq) returns (RegisterResp) @@ -35,14 +35,14 @@ service user { //need login @server ( - prefix: user/v1 + prefix: api/v1 group: user jwt: JwtAuth ) -service user { +service main { @doc "get user info" @handler detail - post /user/detail (UserInfoReq) returns (UserInfoResp) + get /user/detail returns (UserInfoResp) @doc "wechat mini auth" @handler wxMiniAuth @@ -51,10 +51,10 @@ service user { //============================> auth v1 <============================ @server ( - prefix: auth/v1 + prefix: api/v1 group: auth ) -service user { +service main { @doc "get mobile verify code" @handler sendSms post /auth/sendSms (sendSmsReq) diff --git a/app/user/cmd/api/desc/user/user.api b/app/user/cmd/api/desc/user/user.api index 19e92b1..60bab5c 100644 --- a/app/user/cmd/api/desc/user/user.api +++ b/app/user/cmd/api/desc/user/user.api @@ -64,7 +64,6 @@ type ( ) type ( - UserInfoReq {} UserInfoResp { UserInfo User `json:"userInfo"` } diff --git a/app/user/cmd/api/etc/user.yaml b/app/user/cmd/api/etc/user.yaml deleted file mode 100644 index c59a0e6..0000000 --- a/app/user/cmd/api/etc/user.yaml +++ /dev/null @@ -1,19 +0,0 @@ -Name: user -Host: 0.0.0.0 -Port: 8888 -DataSource: "qnc:5vg67b3UNHu8@tcp(127.0.0.1:20001)/qnc?charset=utf8mb4&parseTime=True&loc=Local" -CacheRedis: - - Host: "127.0.0.1:20002" - Pass: "3m3WsgyCKWqz" # Redis 密码,如果未设置则留空 - Type: "node" # 单节点模式 -JwtAuth: - AccessSecret: "WUvoIwL-FK0qnlxhvxR9tV6SjfOpeJMpKmY2QvT99lA" - AccessExpire: 86400 # JWT过期时间 - RefreshAfter: 43200 # 更新时间 -VerifyCode: - AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9" - AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65" - EndpointURL: "dysmsapi.aliyuncs.com" - SignName: "天远数据" - TemplateCode: "SMS_474525324" - ValidTime: 300 \ No newline at end of file diff --git a/app/user/cmd/api/internal/config/config.go b/app/user/cmd/api/internal/config/config.go index 192ae21..c7f0e79 100644 --- a/app/user/cmd/api/internal/config/config.go +++ b/app/user/cmd/api/internal/config/config.go @@ -11,6 +11,11 @@ type Config struct { CacheRedis cache.CacheConf JwtAuth JwtAuth // JWT 鉴权相关配置 VerifyCode VerifyCode + Encrypt Encrypt + Alipay AlipayConfig + Wxpay WxpayConfig + Ali AliConfig + WestConfig WestConfig } // JwtAuth 用于 JWT 鉴权配置 @@ -27,3 +32,32 @@ type VerifyCode struct { TemplateCode string ValidTime int } +type Encrypt struct { + SecretKey string +} + +type AlipayConfig struct { + AppID string + PrivateKey string + AlipayPublicKey string + IsProduction bool + NotifyUrl string +} +type WxpayConfig struct { + AppID string + MchID string + MchCertificateSerialNumber string + MchApiv3Key string + MchPrivateKeyPath string + NotifyUrl string + RefundNotifyUrl string +} +type AliConfig struct { + Code string +} +type WestConfig struct { + Url string + Key string + SecretId string + SecretSecondId string +} diff --git a/app/user/cmd/api/internal/handler/routes.go b/app/user/cmd/api/internal/handler/routes.go index df4eef8..999e390 100644 --- a/app/user/cmd/api/internal/handler/routes.go +++ b/app/user/cmd/api/internal/handler/routes.go @@ -5,6 +5,9 @@ import ( "net/http" auth "qnc-server/app/user/cmd/api/internal/handler/auth" + pay "qnc-server/app/user/cmd/api/internal/handler/pay" + product "qnc-server/app/user/cmd/api/internal/handler/product" + query "qnc-server/app/user/cmd/api/internal/handler/query" user "qnc-server/app/user/cmd/api/internal/handler/user" "qnc-server/app/user/cmd/api/internal/svc" @@ -21,7 +24,131 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: auth.SendSmsHandler(serverCtx), }, }, - rest.WithPrefix("/auth/v1"), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/pay/alipay/callback", + Handler: pay.AlipayCallbackHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/wechat/callback", + Handler: pay.WechatPayCallbackHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/wechat/refund_callback", + Handler: pay.WechatPayRefundCallbackHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/:id", + Handler: product.GetProductByIDHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/en/:product_en", + Handler: product.GetProductByEnHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/product"), + ) + + server.AddRoutes( + []rest.Route{ + { + // query general background check + Method: http.MethodPost, + Path: "/query/backgroundCheck", + Handler: query.BackgroundCheckHandler(serverCtx), + }, + { + // query company info + Method: http.MethodPost, + Path: "/query/companyInfo", + Handler: query.CompanyInfoHandler(serverCtx), + }, + { + // query home service + Method: http.MethodPost, + Path: "/query/homeService", + Handler: query.HomeServiceHandler(serverCtx), + }, + { + // query marriage + Method: http.MethodPost, + Path: "/query/marriage", + Handler: query.MarriageHandler(serverCtx), + }, + { + // query pre-loan background check + Method: http.MethodPost, + Path: "/query/preLoanBackgroundCheck", + Handler: query.PreLoanBackgroundCheckHandler(serverCtx), + }, + { + // query rental info + Method: http.MethodPost, + Path: "/query/rentalInfo", + Handler: query.RentalInfoHandler(serverCtx), + }, + { + // query risk assessment + Method: http.MethodPost, + Path: "/query/riskAssessment", + Handler: query.RiskAssessmentHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 查询详情 + Method: http.MethodGet, + Path: "/query/:id", + Handler: query.QueryDetailHandler(serverCtx), + }, + { + // 查询示例 + Method: http.MethodGet, + Path: "/query/example", + Handler: query.QueryExampleHandler(serverCtx), + }, + { + // 查询列表 + Method: http.MethodGet, + Path: "/query/list", + Handler: query.QueryListHandler(serverCtx), + }, + { + // 查询详情 按订单号 付款查询时 + Method: http.MethodGet, + Path: "/query/orderId/:order_id", + Handler: query.QueryDetailByOrderIdHandler(serverCtx), + }, + { + // 重试查询 + Method: http.MethodPost, + Path: "/query/retry/:id", + Handler: query.QueryRetryHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), ) server.AddRoutes( @@ -45,14 +172,14 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Handler: user.RegisterHandler(serverCtx), }, }, - rest.WithPrefix("/user/v1"), + rest.WithPrefix("/api/v1"), ) server.AddRoutes( []rest.Route{ { // get user info - Method: http.MethodPost, + Method: http.MethodGet, Path: "/user/detail", Handler: user.DetailHandler(serverCtx), }, @@ -64,6 +191,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { }, }, rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), - rest.WithPrefix("/user/v1"), + rest.WithPrefix("/api/v1"), ) } diff --git a/app/user/cmd/api/internal/logic/auth/sendsmslogic.go b/app/user/cmd/api/internal/logic/auth/sendsmslogic.go index 067bf2e..d8be020 100644 --- a/app/user/cmd/api/internal/logic/auth/sendsmslogic.go +++ b/app/user/cmd/api/internal/logic/auth/sendsmslogic.go @@ -34,8 +34,8 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { // 检查手机号是否在一分钟内已发送过验证码 - redisKey := fmt.Sprintf("%s:%s", req.ActionType, req.Mobile) - exists, err := l.svcCtx.Redis.Exists(redisKey) + limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, req.Mobile) + exists, err := l.svcCtx.Redis.Exists(limitCodeKey) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取redis缓存失败: %s", req.Mobile) } @@ -55,14 +55,14 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { if *smsResp.Body.Code != "OK" { return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 阿里客户端响应失败: %s", *smsResp.Body.Message) } - + codeKey := fmt.Sprintf("%s:%s", req.ActionType, req.Mobile) // 将验证码保存到 Redis,设置过期时间 - err = l.svcCtx.Redis.Setex(req.Mobile, code, l.svcCtx.Config.VerifyCode.ValidTime) // 验证码有效期5分钟 + err = l.svcCtx.Redis.Setex(codeKey, code, l.svcCtx.Config.VerifyCode.ValidTime) // 验证码有效期5分钟 if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置过期时间失败: %+v", err) } // 在 Redis 中设置 1 分钟的标记,限制重复请求 - err = l.svcCtx.Redis.Setex(redisKey, code, 60) // 标记 1 分钟内不能重复请求 + err = l.svcCtx.Redis.Setex(limitCodeKey, code, 60) // 标记 1 分钟内不能重复请求 if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置限制重复请求失败: %+v", err) } diff --git a/app/user/cmd/api/internal/logic/user/detaillogic.go b/app/user/cmd/api/internal/logic/user/detaillogic.go index 8e28d3f..f4a66cd 100644 --- a/app/user/cmd/api/internal/logic/user/detaillogic.go +++ b/app/user/cmd/api/internal/logic/user/detaillogic.go @@ -26,7 +26,7 @@ func NewDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DetailLogi } } -func (l *DetailLogic) Detail(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) { +func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) { userID, err := ctxdata.GetUidFromCtx(l.ctx) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %+v", err) diff --git a/app/user/cmd/api/internal/logic/user/registerlogic.go b/app/user/cmd/api/internal/logic/user/registerlogic.go index 196fbf5..fb13d3b 100644 --- a/app/user/cmd/api/internal/logic/user/registerlogic.go +++ b/app/user/cmd/api/internal/logic/user/registerlogic.go @@ -12,6 +12,7 @@ import ( jwtx "qnc-server/common/jwt" "qnc-server/common/tool" "qnc-server/common/xerr" + "qnc-server/pkg/lzkit/lzUtils" "github.com/zeromicro/go-zero/core/logx" ) @@ -32,7 +33,7 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterResp, err error) { // 检查手机号是否在一分钟内已发送过验证码 - redisKey := fmt.Sprintf("%s:%s", "registerCode", req.Mobile) + redisKey := fmt.Sprintf("%s:%s", "register", req.Mobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) if err != nil { if errors.Is(err, redis.Nil) { @@ -58,7 +59,7 @@ func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.RegisterRe user.Nickname = req.Mobile } if len(req.Password) > 0 { - user.Password = tool.Md5ByString(req.Password) + user.Password = lzUtils.StringToNullString(tool.Md5ByString(req.Password)) } insertResult, userInsertErr := l.svcCtx.UserModel.Insert(ctx, session, user) if userInsertErr != nil { diff --git a/app/user/cmd/api/internal/queue/paySuccessNotify.go b/app/user/cmd/api/internal/queue/paySuccessNotify.go new file mode 100644 index 0000000..55aebe7 --- /dev/null +++ b/app/user/cmd/api/internal/queue/paySuccessNotify.go @@ -0,0 +1,179 @@ +package queue + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" + "qnc-server/app/user/cmd/api/internal/svc" + "qnc-server/app/user/cmd/api/internal/types" + "qnc-server/app/user/model" + "qnc-server/pkg/lzkit/crypto" + "qnc-server/pkg/lzkit/lzUtils" +) + +type PaySuccessNotifyUserHandler struct { + svcCtx *svc.ServiceContext +} + +func NewPaySuccessNotifyUserHandler(svcCtx *svc.ServiceContext) *PaySuccessNotifyUserHandler { + return &PaySuccessNotifyUserHandler{ + svcCtx: svcCtx, + } +} + +var payload struct { + OrderID int64 `json:"order_id"` +} + +func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + // 从任务的负载中解码数据 + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return fmt.Errorf("解析任务负载失败: %w", err) + } + + order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID) + if err != nil { + return fmt.Errorf("无效的订单ID: %d, %v", payload.OrderID, err) + } + if order.Status != "paid" { + err = fmt.Errorf("无效的订单: %d", payload.OrderID) + logx.Errorf("处理任务失败,原因: %v", err) + return asynq.SkipRetry + } + product, err := l.svcCtx.ProductModel.FindOne(ctx, order.ProductId) + if err != nil { + return fmt.Errorf("找不到相关产品: orderID: %d, productID: %d", payload.OrderID, order.ProductId) + } + + query, findQueryErr := l.svcCtx.QueryModel.FindOneByOrderId(ctx, order.Id) + if findQueryErr != nil { + findQueryErr = fmt.Errorf("获取任务请求参数失败: %v", findQueryErr) + logx.Errorf("处理任务失败,原因: %v", findQueryErr) + return asynq.SkipRetry + } + if query.QueryState != "pending" { + err = fmt.Errorf("查询已处理: %d", query.Id) + logx.Errorf("处理任务失败,原因: %v", err) + return asynq.SkipRetry + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + err = fmt.Errorf("获取AES密钥失败: %v", decodeErr) + return l.handleError(ctx, err, order, query) + } + + decryptData, aesdecryptErr := crypto.AesDecrypt(query.QueryParams, key) + if aesdecryptErr != nil { + aesdecryptErr = fmt.Errorf("加密响应信息失败: %v", aesdecryptErr) + return l.handleError(ctx, aesdecryptErr, order, query) + } + requests, exists := types.WestDexParams[product.ProductEn] + if !exists { + err = fmt.Errorf("未找到有效的参数配置: productEn: %s", product.ProductEn) + return l.handleError(ctx, err, order, query) + } + // 根据产品类型选择结构体类型 + var requestData interface{} + switch product.ProductEn { + case "marriage": + requestData = &types.MarriageReq{} + case "homeservice": + requestData = &types.HomeServiceReq{} + case "riskassessment": + requestData = &types.RiskAssessmentReq{} + case "companyinfo": + requestData = &types.CompanyInfoReq{} + case "rentalinfo": + requestData = &types.RentalInfoReq{} + case "preloanbackgroundcheck": + requestData = &types.PreLoanBackgroundCheckReq{} + case "backgroundcheck": + requestData = &types.BackgroundCheckReq{} + default: + err = fmt.Errorf("未支持的产品类型: productEn: %s", product.ProductEn) + return l.handleError(ctx, err, order, query) + } + unmarshalErr := json.Unmarshal(decryptData, &requestData) + if unmarshalErr != nil { + unmarshalErr = fmt.Errorf("解析参数失败: %v", unmarshalErr) + return l.handleError(ctx, unmarshalErr, order, query) + } + + combinedResponse, err := l.svcCtx.WestDexService.ProcessRequests(requestData, requests) + if err != nil { + return l.handleError(ctx, err, order, query) + } + // 加密返回响应 + encryptData, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key) + if aesEncryptErr != nil { + err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr) + return l.handleError(ctx, aesEncryptErr, order, query) + } + query.QueryData = lzUtils.StringToNullString(encryptData) + updateErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateErr != nil { + err = fmt.Errorf("保存响应数据失败: %v", updateErr) + return l.handleError(ctx, updateErr, order, query) + } + + query.QueryState = "success" + updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateQueryErr != nil { + updateQueryErr = fmt.Errorf("修改查询状态失败: %v", updateQueryErr) + return l.handleError(ctx, updateQueryErr, order, query) + } + + return nil +} + +// 定义一个中间件函数 +func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error, order *model.Order, query *model.Query) error { + logx.Errorf("处理任务失败,原因: %v", err) + + if order.Status == "paid" && query.QueryState == "pending" { + // 更新查询状态为失败 + query.QueryState = "failed" + updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateQueryErr != nil { + logx.Errorf("更新查询状态失败,订单ID: %d, 错误: %v", order.Id, updateQueryErr) + return asynq.SkipRetry + } + + // 退款 + if order.PaymentPlatform == "wechat" { + refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount) + if refundErr != nil { + logx.Error(refundErr) + return asynq.SkipRetry + } + } else { + refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount) + if refundErr != nil { + logx.Error(refundErr) + return asynq.SkipRetry + } + if refund.IsSuccess() { + logx.Errorf("支付宝退款成功, orderID: %d", order.Id) + // 更新订单状态为退款 + order.Status = "refunded" + updateOrderErr := l.svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order) + if updateOrderErr != nil { + logx.Errorf("更新订单状态失败,订单ID: %d, 错误: %v", order.Id, updateOrderErr) + return fmt.Errorf("更新订单状态失败: %v", updateOrderErr) + } + return asynq.SkipRetry + } else { + logx.Errorf("支付宝退款失败:%v", refundErr) + return asynq.SkipRetry + } + // 直接成功 + } + + } + + return asynq.SkipRetry +} diff --git a/app/user/cmd/api/internal/queue/routes.go b/app/user/cmd/api/internal/queue/routes.go new file mode 100644 index 0000000..1e5923c --- /dev/null +++ b/app/user/cmd/api/internal/queue/routes.go @@ -0,0 +1,28 @@ +package queue + +import ( + "context" + "github.com/hibiken/asynq" + "qnc-server/app/user/cmd/api/internal/svc" + "qnc-server/app/user/cmd/api/internal/types" +) + +type CronJob struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCronJob(ctx context.Context, svcCtx *svc.ServiceContext) *CronJob { + return &CronJob{ + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CronJob) Register() *asynq.ServeMux { + + mux := asynq.NewServeMux() + + mux.Handle(types.MsgPaySuccessQuery, NewPaySuccessNotifyUserHandler(l.svcCtx)) + return mux +} diff --git a/app/user/cmd/api/internal/service/alipayService.go b/app/user/cmd/api/internal/service/alipayService.go new file mode 100644 index 0000000..3b6d654 --- /dev/null +++ b/app/user/cmd/api/internal/service/alipayService.go @@ -0,0 +1,137 @@ +package service + +import ( + "context" + "fmt" + "github.com/smartwalle/alipay/v3" + mathrand "math/rand" + "net/http" + "qnc-server/app/user/cmd/api/internal/config" + "qnc-server/pkg/lzkit/lzUtils" + "strconv" + "time" +) + +type AliPayService struct { + config config.AlipayConfig + AlipayClient *alipay.Client +} + +// NewAliPayService 是一个构造函数,用于初始化 AliPayService +func NewAliPayService(c config.Config) *AliPayService { + client, err := alipay.New(c.Alipay.AppID, c.Alipay.PrivateKey, c.Alipay.IsProduction) + if err != nil { + panic(fmt.Sprintf("创建支付宝客户端失败: %v", err)) + } + + // 加载支付宝公钥 + err = client.LoadAliPayPublicKey(c.Alipay.AlipayPublicKey) + if err != nil { + panic(fmt.Sprintf("加载支付宝公钥失败: %v", err)) + } + return &AliPayService{ + config: c.Alipay, + AlipayClient: client, + } +} + +func (a *AliPayService) CreateAlipayAppOrder(amount float64, subject string, outTradeNo string) (string, error) { + client := a.AlipayClient + totalAmount := lzUtils.ToAlipayAmount(amount) + // 构造移动支付请求 + p := alipay.TradeAppPay{ + Trade: alipay.Trade{ + Subject: subject, + OutTradeNo: outTradeNo, + TotalAmount: totalAmount, + ProductCode: "QUICK_MSECURITY_PAY", // 移动端支付专用代码 + NotifyURL: a.config.NotifyUrl, // 异步回调通知地址 + }, + } + + // 获取APP支付字符串,这里会签名 + payStr, err := client.TradeAppPay(p) + if err != nil { + return "", fmt.Errorf("创建支付宝订单失败: %v", err) + } + + return payStr, nil +} + +// AliRefund 发起支付宝退款 +func (a *AliPayService) AliRefund(ctx context.Context, outTradeNo string, refundAmount float64) (*alipay.TradeRefundRsp, error) { + refund := alipay.TradeRefund{ + OutTradeNo: outTradeNo, + RefundAmount: lzUtils.ToAlipayAmount(refundAmount), + OutRequestNo: fmt.Sprintf("%s-refund", outTradeNo), + } + + // 发起退款请求 + refundResp, err := a.AlipayClient.TradeRefund(ctx, refund) + if err != nil { + return nil, fmt.Errorf("支付宝退款请求错误:%v", err) + } + return refundResp, nil +} + +// HandleAliPaymentNotification 支付宝支付回调 +func (a *AliPayService) HandleAliPaymentNotification(r *http.Request) (*alipay.Notification, error) { + // 解析表单 + err := r.ParseForm() + if err != nil { + return nil, fmt.Errorf("解析请求表单失败:%v", err) + } + // 解析并验证通知,DecodeNotification 会自动验证签名 + notification, err := a.AlipayClient.DecodeNotification(r.Form) + if err != nil { + return nil, fmt.Errorf("验证签名失败: %v", err) + } + return notification, nil +} +func (a *AliPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*alipay.TradeQueryRsp, error) { + queryRequest := alipay.TradeQuery{ + OutTradeNo: outTradeNo, + } + + // 发起查询请求 + resp, err := a.AlipayClient.TradeQuery(ctx, queryRequest) + if err != nil { + return nil, fmt.Errorf("查询支付宝订单失败: %v", err) + } + + // 返回交易状态 + if resp.IsSuccess() { + return resp, nil + } + + return nil, fmt.Errorf("查询支付宝订单失败: %v", resp.SubMsg) +} + +// GenerateOutTradeNo 生成唯一订单号的函数 +func (a *AliPayService) GenerateOutTradeNo() string { + length := 16 + // 获取当前时间戳 + timestamp := time.Now().UnixNano() + + // 转换为字符串 + timeStr := strconv.FormatInt(timestamp, 10) + + // 生成随机数 + mathrand.Seed(time.Now().UnixNano()) + randomPart := strconv.Itoa(mathrand.Intn(1000000)) + + // 组合时间戳和随机数 + combined := timeStr + randomPart + + // 如果长度超出指定值,则截断;如果不够,则填充随机字符 + if len(combined) >= length { + return combined[:length] + } + + // 如果长度不够,填充0 + for len(combined) < length { + combined += strconv.Itoa(mathrand.Intn(10)) // 填充随机数 + } + + return combined +} diff --git a/app/user/cmd/api/internal/service/asynqService.go b/app/user/cmd/api/internal/service/asynqService.go new file mode 100644 index 0000000..f95e5da --- /dev/null +++ b/app/user/cmd/api/internal/service/asynqService.go @@ -0,0 +1,59 @@ +// asynq_service.go + +package service + +import ( + "encoding/json" + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" + "qnc-server/app/user/cmd/api/internal/config" + "qnc-server/app/user/cmd/api/internal/types" +) + +type AsynqService struct { + client *asynq.Client + config config.Config +} + +// NewAsynqService 创建并初始化 Asynq 客户端 +func NewAsynqService(c config.Config) *AsynqService { + client := asynq.NewClient(asynq.RedisClientOpt{ + Addr: c.CacheRedis[0].Host, + Password: c.CacheRedis[0].Pass, + }) + + return &AsynqService{client: client, config: c} +} + +// Close 关闭 Asynq 客户端 +func (s *AsynqService) Close() error { + return s.client.Close() +} +func (s *AsynqService) SendQueryTask(orderID int64) error { + // 准备任务的 payload + payload := types.MsgPaySuccessQueryPayload{ + OrderID: orderID, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + logx.Errorf("发送异步任务失败 (无法编码 payload): %v, 订单号: %d", err, orderID) + return err // 直接返回错误,避免继续执行 + } + + options := []asynq.Option{ + asynq.MaxRetry(5), // 设置最大重试次数 + } + // 创建任务 + task := asynq.NewTask(types.MsgPaySuccessQuery, payloadBytes, options...) + + // 将任务加入队列并获取任务信息 + info, err := s.client.Enqueue(task) + if err != nil { + logx.Errorf("发送异步任务失败 (加入队列失败): %+v, 订单号: %d", err, orderID) + return err + } + + // 记录成功日志,带上任务 ID 和队列信息 + logx.Infof("发送异步任务成功,任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID) + return nil +} diff --git a/app/user/cmd/api/internal/service/verificationService.go b/app/user/cmd/api/internal/service/verificationService.go new file mode 100644 index 0000000..bebeeb9 --- /dev/null +++ b/app/user/cmd/api/internal/service/verificationService.go @@ -0,0 +1,116 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "qnc-server/app/user/cmd/api/internal/config" + "strings" +) + +type VerificationService struct { + c config.Config +} + +func NewVerificationService(c config.Config) *VerificationService { + return &VerificationService{ + c: c, + } +} + +type TwoFactorVerificationRequest struct { + Name string + IDCard string +} +type TwoFactorVerificationResp struct { + Msg string `json:"msg"` + Success bool `json:"success"` + Code int `json:"code"` + Data *TwoFactorVerificationData `json:"data"` // +} +type TwoFactorVerificationData struct { + Birthday string `json:"birthday"` + Result int `json:"result"` + Address string `json:"address"` + OrderNo string `json:"orderNo"` + Sex string `json:"sex"` + Desc string `json:"desc"` +} + +// VerificationResult 定义校验结果结构体 +type VerificationResult struct { + Passed bool + Err error +} + +// ValidationError 定义校验错误类型 +type ValidationError struct { + Message string +} + +func (e *ValidationError) Error() string { + return e.Message +} + +func (r *VerificationService) TwoFactorVerification(request TwoFactorVerificationRequest) (*VerificationResult, error) { + appCode := r.c.Ali.Code + requestUrl := "https://kzidcardv1.market.alicloudapi.com/api-mall/api/id_card/check" + + // 构造查询参数 + data := url.Values{} + data.Add("name", request.Name) + data.Add("idcard", request.IDCard) + + req, err := http.NewRequest(http.MethodPost, requestUrl, strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %+v", err) + } + req.Header.Set("Authorization", "APPCODE "+appCode) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("请求失败: %+v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("请求失败, 状态码: %d", resp.StatusCode) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("响应体读取失败:%v", err) + } + var twoFactorVerificationResp TwoFactorVerificationResp + err = json.Unmarshal(respBody, &twoFactorVerificationResp) + if err != nil { + return nil, fmt.Errorf("二要素解析错误: %v", err) + } + + if !twoFactorVerificationResp.Success { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "请输入有效的身份证号码"}, + }, nil + } + + if twoFactorVerificationResp.Code != 200 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: twoFactorVerificationResp.Msg}, + }, nil + } + + if twoFactorVerificationResp.Data.Result == 1 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "姓名与身份证不一致"}, + }, nil + } + + return &VerificationResult{Passed: true, Err: nil}, nil +} diff --git a/app/user/cmd/api/internal/service/wechatpayService.go b/app/user/cmd/api/internal/service/wechatpayService.go new file mode 100644 index 0000000..e256085 --- /dev/null +++ b/app/user/cmd/api/internal/service/wechatpayService.go @@ -0,0 +1,188 @@ +package service + +import ( + "context" + "fmt" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" + "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" + "github.com/wechatpay-apiv3/wechatpay-go/core/notify" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "github.com/zeromicro/go-zero/core/logx" + "net/http" + "qnc-server/app/user/cmd/api/internal/config" + "qnc-server/pkg/lzkit/lzUtils" + "strconv" + "time" +) + +const ( + TradeStateSuccess = "SUCCESS" // 支付成功 + TradeStateRefund = "REFUND" // 转入退款 + TradeStateNotPay = "NOTPAY" // 未支付 + TradeStateClosed = "CLOSED" // 已关闭 + TradeStateRevoked = "REVOKED" // 已撤销(付款码支付) + TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付) + TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败) +) + +type WechatPayService struct { + config config.WxpayConfig + wechatClient *core.Client + notifyHandler *notify.Handler +} + +// NewWechatPayService 初始化微信支付服务 +func NewWechatPayService(c config.Config) *WechatPayService { + // 从配置中加载商户信息 + mchID := c.Wxpay.MchID + mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber + mchAPIv3Key := c.Wxpay.MchApiv3Key + + // 从文件中加载商户私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.Wxpay.MchPrivateKeyPath) + if err != nil { + logx.Errorf("加载商户私钥失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序 + } + + // 使用商户私钥和其他参数初始化微信支付客户端 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key), + } + client, err := core.NewClient(context.Background(), opts...) + if err != nil { + logx.Errorf("创建微信支付客户端失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序 + } + // 在初始化时获取证书访问器并创建 notifyHandler + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + notifyHandler, err := notify.NewRSANotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) + if err != nil { + logx.Errorf("获取证书访问器失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序 + } + return &WechatPayService{ + config: c.Wxpay, + wechatClient: client, + notifyHandler: notifyHandler, + } +} + +// CreateWechatAppOrder 创建微信APP支付订单 +func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount float64, description string, outTradeNo string) (string, error) { + totalAmount := lzUtils.ToWechatAmount(amount) + + // 构建支付请求参数 + payRequest := app.PrepayRequest{ + Appid: core.String(w.config.AppID), + Mchid: core.String(w.config.MchID), + Description: core.String(description), + OutTradeNo: core.String(outTradeNo), + NotifyUrl: core.String(w.config.NotifyUrl), + Amount: &app.Amount{ + Total: core.Int64(totalAmount), + }, + } + + // 初始化 AppApiService + svc := app.AppApiService{Client: w.wechatClient} + + // 发起预支付请求 + resp, result, err := svc.Prepay(ctx, payRequest) + if err != nil { + return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + + // 返回预支付交易会话标识 + return *resp.PrepayId, nil +} + +// HandleWechatPayNotification 处理微信支付回调 +func (w *WechatPayService) HandleWechatPayNotification(ctx context.Context, req *http.Request) (*payments.Transaction, error) { + transaction := new(payments.Transaction) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, transaction) + if err != nil { + return nil, fmt.Errorf("微信支付通知处理失败: %v", err) + } + // 返回交易信息 + return transaction, nil +} + +// HandleRefundNotification 处理微信退款回调 +func (w *WechatPayService) HandleRefundNotification(ctx context.Context, req *http.Request) (*refunddomestic.Refund, error) { + refund := new(refunddomestic.Refund) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, refund) + if err != nil { + return nil, fmt.Errorf("微信退款回调通知处理失败: %v", err) + } + return refund, nil +} + +// QueryOrderStatus 主动查询订单状态 +func (w *WechatPayService) QueryOrderStatus(ctx context.Context, transactionID string) (*payments.Transaction, error) { + svc := jsapi.JsapiApiService{Client: w.wechatClient} + + // 调用 QueryOrderById 方法查询订单状态 + resp, result, err := svc.QueryOrderById(ctx, jsapi.QueryOrderByIdRequest{ + TransactionId: core.String(transactionID), + Mchid: core.String(w.config.MchID), + }) + if err != nil { + return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + return resp, nil + +} + +// WeChatRefund 申请微信退款 +func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string, refundAmount float64, totalAmount float64) error { + + // 生成唯一的退款单号 + outRefundNo := fmt.Sprintf("%s-refund", outTradeNo) + + // 初始化退款服务 + svc := refunddomestic.RefundsApiService{Client: w.wechatClient} + + // 创建退款请求 + resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{ + OutTradeNo: core.String(outTradeNo), + OutRefundNo: core.String(outRefundNo), + NotifyUrl: core.String(w.config.RefundNotifyUrl), + Amount: &refunddomestic.AmountReq{ + Currency: core.String("CNY"), + Refund: core.Int64(lzUtils.ToWechatAmount(refundAmount)), + Total: core.Int64(lzUtils.ToWechatAmount(totalAmount)), + }, + }) + if err != nil { + return fmt.Errorf("微信订单申请退款错误: %v", err) + } + // 打印退款结果 + logx.Infof("退款申请成功,状态码=%d,退款单号=%s,微信退款单号=%s", result.Response.StatusCode, *resp.OutRefundNo, *resp.RefundId) + return nil +} + +// GenerateOutTradeNo 生成唯一订单号 +func (w *WechatPayService) GenerateOutTradeNo() string { + length := 16 + timestamp := time.Now().UnixNano() + timeStr := strconv.FormatInt(timestamp, 10) + randomPart := strconv.Itoa(int(timestamp % 1e6)) + combined := timeStr + randomPart + + if len(combined) >= length { + return combined[:length] + } + + for len(combined) < length { + combined += strconv.Itoa(int(timestamp % 10)) + } + + return combined +} diff --git a/app/user/cmd/api/internal/service/westdexService.go b/app/user/cmd/api/internal/service/westdexService.go new file mode 100644 index 0000000..c35c0b0 --- /dev/null +++ b/app/user/cmd/api/internal/service/westdexService.go @@ -0,0 +1,707 @@ +package service + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/zeromicro/go-zero/core/logx" + "io" + "net/http" + "qnc-server/app/user/cmd/api/internal/config" + "qnc-server/app/user/cmd/api/internal/types" + "qnc-server/pkg/lzkit/crypto" + "reflect" + "strconv" + "sync" + "sync/atomic" + "time" +) + +type WestResp struct { + Message string `json:"message"` + Code string `json:"code"` + Data string `json:"data"` + ID string `json:"id"` + ErrorCode *int `json:"error_code"` + Reason string `json:"reason"` +} +type G05HZ01WestResp struct { + Message string `json:"message"` + Code string `json:"code"` + Data json.RawMessage `json:"data"` + ID string `json:"id"` + ErrorCode *int `json:"error_code"` + Reason string `json:"reason"` +} +type WestDexService struct { + config config.WestConfig +} +type APIResponseData struct { + ApiID string `json:"apiID"` + Data json.RawMessage `json:"data"` // 这里用 RawMessage 来存储原始的 data + Success bool `json:"success"` + Timestamp string `json:"timestamp"` + Error string `json:"error,omitempty"` +} + +// NewWestDexService 是一个构造函数,用于初始化 WestDexService +func NewWestDexService(c config.Config) *WestDexService { + return &WestDexService{ + config: c.WestConfig, + } +} + +// CallAPI 调用西部数据的 API +func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) { + // 生成当前的13位时间戳 + timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + + // 构造请求URL + reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretId, code, timestamp) + + jsonData, marshalErr := json.Marshal(reqData) + if marshalErr != nil { + return nil, marshalErr + } + + // 创建HTTP POST请求 + req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData)) + if newRequestErr != nil { + return nil, newRequestErr + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + + // 发送请求 + client := &http.Client{} + httpResp, clientDoErr := client.Do(req) + if clientDoErr != nil { + return nil, clientDoErr + } + defer func(Body io.ReadCloser) { + closeErr := Body.Close() + if closeErr != nil { + + } + }(httpResp.Body) + + // 检查请求是否成功 + if httpResp.StatusCode == 200 { + // 读取响应体 + bodyBytes, ReadErr := io.ReadAll(httpResp.Body) + if ReadErr != nil { + return nil, ReadErr + } + + // 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法 + var westDexResp WestResp + UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp) + if UnmarshalErr != nil { + return nil, UnmarshalErr + } + logx.Infof("西部数据请求响应, code: %s, response: %v", code, westDexResp) + if westDexResp.Code != "00000" { + if westDexResp.Data == "" { + return nil, errors.New(westDexResp.Message) + } + decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key) + if DecryptErr != nil { + return nil, DecryptErr + } + return decryptedData, errors.New(westDexResp.Message) + } + if westDexResp.Data == "" { + return nil, errors.New(westDexResp.Message) + } + // 解密响应数据 + decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key) + if DecryptErr != nil { + return nil, DecryptErr + } + // 输出解密后的数据 + return decryptedData, nil + } + + return nil, fmt.Errorf("西部请求失败Code: %d", httpResp.StatusCode) +} + +// CallAPI 调用西部数据的 API +func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) { + // 生成当前的13位时间戳 + timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + + // 构造请求URL + reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretSecondId, code, timestamp) + + jsonData, marshalErr := json.Marshal(reqData) + if marshalErr != nil { + return nil, marshalErr + } + + // 创建HTTP POST请求 + req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData)) + if newRequestErr != nil { + return nil, newRequestErr + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + + // 发送请求 + client := &http.Client{} + httpResp, clientDoErr := client.Do(req) + if clientDoErr != nil { + return nil, clientDoErr + } + defer func(Body io.ReadCloser) { + closeErr := Body.Close() + if closeErr != nil { + + } + }(httpResp.Body) + + // 检查请求是否成功 + if httpResp.StatusCode == 200 { + // 读取响应体 + bodyBytes, ReadErr := io.ReadAll(httpResp.Body) + if ReadErr != nil { + return nil, ReadErr + } + + // 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法 + var westDexResp G05HZ01WestResp + UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp) + if UnmarshalErr != nil { + return nil, UnmarshalErr + } + logx.Infof("西部数据请求响应, code: %s, response: %v", code, westDexResp) + if westDexResp.Code != "0000" { + if westDexResp.Data == nil { + return nil, errors.New(westDexResp.Message) + } + return westDexResp.Data, errors.New(westDexResp.Message) + } + if westDexResp.Data == nil { + return nil, errors.New(westDexResp.Message) + } + return westDexResp.Data, nil + } + + return nil, fmt.Errorf("西部请求失败Code: %d", httpResp.StatusCode) +} + +// EncryptStructFields 加密字段的函数,处理不同类型,并跳过空值字段 +func (w *WestDexService) EncryptStructFields(inputStruct interface{}) (map[string]interface{}, error) { + encryptedFields := make(map[string]interface{}) + + // 使用反射获取结构体的类型和值 + v := reflect.ValueOf(inputStruct) + // 检查并解引用指针类型 + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return nil, errors.New("传入的interfact不是struct") + } + + // 遍历结构体字段 + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + fieldValue := v.Field(i) + + // 检查字段的 encrypt 标签是否为 "false" + encryptTag := field.Tag.Get("encrypt") + if encryptTag == "false" { + encryptedFields[field.Name] = fieldValue.Interface() + continue + } + + // 如果字段为空值,跳过 + if fieldValue.IsZero() { + continue + } + + // 将字段的值转换为字符串进行加密 + strValue := fmt.Sprintf("%v", fieldValue.Interface()) + + // 执行加密操作 + encryptedValue, err := crypto.WestDexEncrypt(strValue, w.config.Key) + if err != nil { + return nil, err + } + + // 将加密后的值存入结果映射 + encryptedFields[field.Name] = encryptedValue + } + + return encryptedFields, nil +} + +// MapStructToAPIRequest 字段映射 +func (w *WestDexService) MapStructToAPIRequest(encryptedFields map[string]interface{}, fieldMapping map[string]string, wrapField string) map[string]interface{} { + apiRequest := make(map[string]interface{}) + + // 遍历字段映射表 + for structField, apiField := range fieldMapping { + // 如果加密后的字段存在,才添加到请求 + if structField == "InquiredAuth" { + apiRequest[apiField] = GetDateRange() + } else if structField == "TimeRange" { + apiRequest[apiField] = "5" + } else if value, exists := encryptedFields[structField]; exists { + apiRequest[apiField] = value + } + } + + // 如果 wrapField 不为空,将 apiRequest 包裹到该字段下 + if wrapField != "" { + return map[string]interface{}{ + wrapField: apiRequest, + } + } + return apiRequest +} + +// ProcessRequests 批量处理 +func (w *WestDexService) ProcessRequests(data interface{}, requests []types.WestDexServiceRequestParams) ([]byte, error) { + var ( + wg sync.WaitGroup + resultsCh = make(chan APIResponseData, len(requests)) + errorsCh = make(chan error, len(requests)) + ctx, cancel = context.WithCancel(context.Background()) + errorCount int32 + errorLimit = 4 + ) + defer cancel() + + for i, req := range requests { + wg.Add(1) + go func(i int, req types.WestDexServiceRequestParams) { + defer wg.Done() + + select { + case <-ctx.Done(): + return + default: + } + // 请求参数预处理 + apiRequest, preprocessErr := w.PreprocessRequestParams(req.ApiID, data) + if preprocessErr != nil { + errorsCh <- fmt.Errorf("请求预处理失败: %v", preprocessErr) + atomic.AddInt32(&errorCount, 1) + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + cancel() + } + return + } + var resp []byte + var callApiErr error + if req.ApiID == "G05HZ01" { + resp, callApiErr = w.G05HZ01CallAPI(req.ApiID, apiRequest) + } else { + resp, callApiErr = w.CallAPI(req.ApiID, apiRequest) + } + timestamp := time.Now().Format("2006-01-02 15:04:05") + + result := APIResponseData{ + ApiID: req.ApiID, + Success: false, + Timestamp: timestamp, + } + + if callApiErr != nil { + errorsCh <- fmt.Errorf("西部请求, 请求失败: %+v", callApiErr) + atomic.AddInt32(&errorCount, 1) + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + cancel() + } + result.Error = callApiErr.Error() + result.Data = resp + resultsCh <- result + return + } + + processedResp, processErr := processResponse(resp, req.ApiID) + if processErr != nil { + errorsCh <- fmt.Errorf("处理响应失败: %v", processErr) + atomic.AddInt32(&errorCount, 1) + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + cancel() + } + result.Error = processErr.Error() + } else { + result.Data = processedResp + result.Success = true + } + resultsCh <- result + }(i, req) + } + + go func() { + wg.Wait() + close(resultsCh) + close(errorsCh) + }() + // 收集所有结果并合并 + var responseData []APIResponseData + for result := range resultsCh { + responseData = append(responseData, result) + } + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + var allErrors []error + for err := range errorsCh { + allErrors = append(allErrors, err) + } + return nil, fmt.Errorf("请求失败次数超过 %d 次: %v", errorLimit, allErrors) + } + + combinedResponse, err := json.Marshal(responseData) + if err != nil { + return nil, fmt.Errorf("响应数据转 JSON 失败: %+v", err) + } + + return combinedResponse, nil +} + +// ------------------------------------请求处理器-------------------------- +var requestProcessors = map[string]func(*WestDexService, interface{}) (map[string]interface{}, error){ + "G09SC02": (*WestDexService).ProcessG09SC02Request, + "G27BJ05": (*WestDexService).ProcessG27BJ05Request, + "G26BJ05": (*WestDexService).ProcessG26BJ05Request, + "G34BJ03": (*WestDexService).ProcessG34BJ03Request, + "G35SC01": (*WestDexService).ProcessG35SC01Request, + "G28BJ05": (*WestDexService).ProcessG28BJ05Request, + "G05HZ01": (*WestDexService).ProcessG05HZ01Request, +} + +// PreprocessRequestParams 调用指定的请求处理函数 +func (w *WestDexService) PreprocessRequestParams(apiID string, params interface{}) (map[string]interface{}, error) { + if processor, exists := requestProcessors[apiID]; exists { + return processor(w, params) // 调用 WestDexService 方法 + } + + var request map[string]interface{} + return request, nil +} + +// / 将处理函数作为 WestDexService 的方法 +func (w *WestDexService) ProcessG09SC02Request(params interface{}) (map[string]interface{}, error) { + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G09SC02FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG27BJ05Request(params interface{}) (map[string]interface{}, error) { + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G27BJ05FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG26BJ05Request(params interface{}) (map[string]interface{}, error) { + // 特殊名单 G26BJ05 + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G26BJ05FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG34BJ03Request(params interface{}) (map[string]interface{}, error) { + // 个人不良 G34BJ03 + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G34BJ03FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG35SC01Request(params interface{}) (map[string]interface{}, error) { + // 个人涉诉 G35SC01 + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G35SC01FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG28BJ05Request(params interface{}) (map[string]interface{}, error) { + // 借贷行为 G28BJ05 + encryptedFields, err := w.EncryptStructFields(params) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G28BJ05FieldMapping, "data") + return apiRequest, nil +} + +func (w *WestDexService) ProcessG05HZ01Request(params interface{}) (map[string]interface{}, error) { + // 使用 reflect 获取 params 的值和类型 + val := reflect.ValueOf(params) + if val.Kind() == reflect.Ptr { + val = val.Elem() // 如果是指针,获取指向的实际值 + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("请求参数必须是结构体类型") + } + + // 初始化一个 map 来存储加密后的字段 + encryptedFields := make(map[string]interface{}) + + // 遍历结构体字段,将其转换为 map[string]interface{} + valType := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldName := valType.Field(i).Name + + // 如果字段名为 "IDCard",对其值进行加密 + if fieldName == "IDCard" { + if field.Kind() != reflect.String { + return nil, fmt.Errorf("IDCard 字段不是字符串类型") + } + idCard := field.String() + encryptedIDCard := crypto.Md5Encrypt(idCard) + encryptedFields[fieldName] = encryptedIDCard + } else { + // 否则直接将字段值添加到 map 中 + encryptedFields[fieldName] = field.Interface() + } + } + + // 使用字段映射表生成最终的 API 请求 + apiRequest := w.MapStructToAPIRequest(encryptedFields, types.G05HZ01FieldMapping, "") + return apiRequest, nil +} + +// ----------------------------------------------------------------------------- +// 响应处理器 +var responseProcessors = map[string]func([]byte) ([]byte, error){ + "G09SC02": processG09SC02Response, // 单人婚姻 + "G27BJ05": processG27BJ05Response, // 借贷意向 + "G28BJ05": processG28BJ05Response, // 借贷行为 + "G26BJ05": processG26BJ05Response, // 特殊名单 + "G05HZ01": processG05HZ01Response, // 股东人企关系 + "G34BJ03": processG34BJ03Response, // 个人不良 + "G35SC01": processG35SC01Response, // 个人涉诉 +} + +// processResponse 处理响应数据 +func processResponse(resp []byte, apiID string) ([]byte, error) { + if processor, exists := responseProcessors[apiID]; exists { + return processor(resp) + } + return resp, nil +} + +func processG09SC02Response(resp []byte) ([]byte, error) { + // 使用 GJSON 递归搜索 "maritalStatus" 字段 + result := gjson.GetBytes(resp, "data.0.maritalStatus") + + if result.Exists() { + // 如果字段存在,构造包含 "status" 的 JSON 响应 + responseMap := map[string]string{"status": result.String()} + jsonResponse, err := json.Marshal(responseMap) + if err != nil { + return nil, err + } + return jsonResponse, nil + } else { + return nil, errors.New("查询为空") + } +} + +func processG27BJ05Response(resp []byte) ([]byte, error) { + // 获取 code 字段 + codeResult := gjson.GetBytes(resp, "code") + if !codeResult.Exists() { + return nil, fmt.Errorf("code 字段不存在") + } + if codeResult.String() != "00" { + return nil, fmt.Errorf("未匹配到相关结果") + } + + // 获取 data 字段 + dataResult := gjson.GetBytes(resp, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("data 字段不存在") + } + + // 将 data 字段解析为 map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析 data 字段失败: %v", err) + } + + // 删除指定字段 + delete(dataMap, "swift_number") + delete(dataMap, "DataStrategy") + + // 重新编码为 JSON + modifiedData, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("编码修改后的 data 失败: %v", err) + } + return modifiedData, nil +} + +func processG28BJ05Response(resp []byte) ([]byte, error) { + // 处理借贷行为的响应数据 + // 获取 code 字段 + codeResult := gjson.GetBytes(resp, "code") + if !codeResult.Exists() { + return nil, fmt.Errorf("code 字段不存在") + } + if codeResult.String() != "00" { + return nil, fmt.Errorf("未匹配到相关结果") + } + + // 获取 data 字段 + dataResult := gjson.GetBytes(resp, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("data 字段不存在") + } + + // 将 data 字段解析为 map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析 data 字段失败: %v", err) + } + + // 删除指定字段 + delete(dataMap, "swift_number") + delete(dataMap, "DataStrategy") + + // 重新编码为 JSON + modifiedData, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("编码修改后的 data 失败: %v", err) + } + return modifiedData, nil +} + +func processG26BJ05Response(resp []byte) ([]byte, error) { + // 处理特殊名单的响应数据 + // 获取 code 字段 + codeResult := gjson.GetBytes(resp, "code") + if !codeResult.Exists() { + return nil, fmt.Errorf("code 字段不存在") + } + if codeResult.String() != "00" { + return nil, fmt.Errorf("未匹配到相关结果") + } + + // 获取 data 字段 + dataResult := gjson.GetBytes(resp, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("data 字段不存在") + } + + // 将 data 字段解析为 map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析 data 字段失败: %v", err) + } + + // 删除指定字段 + delete(dataMap, "swift_number") + delete(dataMap, "DataStrategy") + + // 重新编码为 JSON + modifiedData, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("编码修改后的 data 失败: %v", err) + } + return modifiedData, nil +} + +func processG05HZ01Response(resp []byte) ([]byte, error) { + // 处理股东人企关系的响应数据 + code := gjson.GetBytes(resp, "code") + if !code.Exists() { + return nil, fmt.Errorf("响应中缺少 code 字段") + } + + // 判断 code 是否等于 "0000" + if code.String() == "0000" { + // 获取 data 字段的值 + data := gjson.GetBytes(resp, "data") + if !data.Exists() { + return nil, fmt.Errorf("响应中缺少 data 字段") + } + // 返回 data 字段的内容 + return []byte(data.Raw), nil + } + + // code 不等于 "0000",返回错误 + return nil, fmt.Errorf("响应code错误%s", code.String()) +} + +func processG34BJ03Response(resp []byte) ([]byte, error) { + // 处理个人不良的响应数据 + dataResult := gjson.GetBytes(resp, "negative_info.data.risk_level") + if dataResult.Exists() { + // 如果字段存在,构造包含 "status" 的 JSON 响应 + responseMap := map[string]string{"risk_level": dataResult.String()} + jsonResponse, err := json.Marshal(responseMap) + if err != nil { + return nil, err + } + return jsonResponse, nil + } else { + return nil, errors.New("查询为空") + } +} + +func processG35SC01Response(resp []byte) ([]byte, error) { + // 第一步:提取外层的 data 字段 + dataResult := gjson.GetBytes(resp, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("外层 data 字段不存在") + } + + // 第二步:解析外层 data 的 JSON 字符串 + var outerDataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.String()), &outerDataMap); err != nil { + return nil, fmt.Errorf("解析外层 data 字段失败: %v", err) + } + + // 第三步:提取内层的 data 字段 + innerData, ok := outerDataMap["data"].(string) + if !ok { + return nil, fmt.Errorf("内层 data 字段不存在或类型错误") + } + + // 第四步:解析内层 data 的 JSON 字符串 + var finalDataMap map[string]interface{} + if err := json.Unmarshal([]byte(innerData), &finalDataMap); err != nil { + return nil, fmt.Errorf("解析内层 data 字段失败: %v", err) + } + + // 将最终的 JSON 对象编码为字节数组返回 + finalDataBytes, err := json.Marshal(finalDataMap) + if err != nil { + return nil, fmt.Errorf("编码最终的 JSON 对象失败: %v", err) + } + + return finalDataBytes, nil +} + +// GetDateRange 返回今天到明天的日期范围,格式为 "yyyyMMdd-yyyyMMdd" +func GetDateRange() string { + today := time.Now().Format("20060102") // 获取今天的日期 + tomorrow := time.Now().Add(24 * time.Hour).Format("20060102") // 获取明天的日期 + return fmt.Sprintf("%s-%s", today, tomorrow) // 拼接日期范围并返回 +} diff --git a/app/user/cmd/api/internal/svc/servicecontext.go b/app/user/cmd/api/internal/svc/servicecontext.go index e9b095e..9d5d2cc 100644 --- a/app/user/cmd/api/internal/svc/servicecontext.go +++ b/app/user/cmd/api/internal/svc/servicecontext.go @@ -1,30 +1,70 @@ package svc import ( + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/sqlx" "qnc-server/app/user/cmd/api/internal/config" + "qnc-server/app/user/cmd/api/internal/service" "qnc-server/app/user/model" ) type ServiceContext struct { - Config config.Config - Redis *redis.Redis - UserModel model.UserModel - UserAuthModel model.UserAuthModel + Config config.Config + Redis *redis.Redis + UserModel model.UserModel + UserAuthModel model.UserAuthModel + ProductModel model.ProductModel + FeatureModel model.FeatureModel + ProductFeatureModel model.ProductFeatureModel + OrderModel model.OrderModel + QueryModel model.QueryModel + AlipayService *service.AliPayService + WechatPayService *service.WechatPayService + WestDexService *service.WestDexService + AsynqServer *asynq.Server // 服务端 + AsynqService *service.AsynqService // 客户端 + VerificationService *service.VerificationService } func NewServiceContext(c config.Config) *ServiceContext { - db := sqlx.NewMysql(c.DataSource) // 创建数据库连接 + db := sqlx.NewMysql(c.DataSource) redisConf := redis.RedisConf{ Host: c.CacheRedis[0].Host, Pass: c.CacheRedis[0].Pass, - Type: c.CacheRedis[0].Type, // Redis 节点类型,如 "node" + Type: c.CacheRedis[0].Type, } + asynqServer := asynq.NewServer( + asynq.RedisClientOpt{Addr: c.CacheRedis[0].Host, Password: c.CacheRedis[0].Pass}, + asynq.Config{ + IsFailure: func(err error) bool { + logx.Errorf("异步任务失败: %+v \n", err) + return true + }, + Concurrency: 10, + }, + ) return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(redisConf), - UserModel: model.NewUserModel(db, c.CacheRedis), - UserAuthModel: model.NewUserAuthModel(db, c.CacheRedis), + Config: c, + Redis: redis.MustNewRedis(redisConf), + AlipayService: service.NewAliPayService(c), + WechatPayService: service.NewWechatPayService(c), + WestDexService: service.NewWestDexService(c), + VerificationService: service.NewVerificationService(c), + AsynqServer: asynqServer, + AsynqService: service.NewAsynqService(c), + UserModel: model.NewUserModel(db, c.CacheRedis), + UserAuthModel: model.NewUserAuthModel(db, c.CacheRedis), + ProductModel: model.NewProductModel(db, c.CacheRedis), + OrderModel: model.NewOrderModel(db, c.CacheRedis), + QueryModel: model.NewQueryModel(db, c.CacheRedis), + FeatureModel: model.NewFeatureModel(db, c.CacheRedis), + ProductFeatureModel: model.NewProductFeatureModel(db, c.CacheRedis), + } +} +func (s *ServiceContext) Close() { + if s.AsynqService != nil { + s.AsynqService.Close() } } diff --git a/app/user/cmd/api/internal/types/payload.go b/app/user/cmd/api/internal/types/payload.go new file mode 100644 index 0000000..672918f --- /dev/null +++ b/app/user/cmd/api/internal/types/payload.go @@ -0,0 +1,5 @@ +package types + +type MsgPaySuccessQueryPayload struct { + OrderID int64 `json:"order_id"` +} diff --git a/app/user/cmd/api/internal/types/query.go b/app/user/cmd/api/internal/types/query.go new file mode 100644 index 0000000..c620622 --- /dev/null +++ b/app/user/cmd/api/internal/types/query.go @@ -0,0 +1,61 @@ +package types + +type MarriageReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} +type HomeServiceReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} + +// RiskAssessment 查询请求结构 +type RiskAssessmentReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} + +// CompanyInfo 查询请求结构 +type CompanyInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} + +// RentalInfo 查询请求结构 +type RentalInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} + +// PreLoanBackgroundCheck 查询请求结构 +type PreLoanBackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} + +// BackgroundCheck 查询请求结构 +type BackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + PayMethod string `json:"pay_method" validate:"required,payMethod"` +} diff --git a/app/user/cmd/api/internal/types/queryMap.go b/app/user/cmd/api/internal/types/queryMap.go new file mode 100644 index 0000000..13256a6 --- /dev/null +++ b/app/user/cmd/api/internal/types/queryMap.go @@ -0,0 +1,47 @@ +package types + +// 特殊名单 G26BJ05 +var G26BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", + "TimeRange": "time_range", +} + +// 个人不良 +var G34BJ03FieldMapping = map[string]string{ + "IDCard": "id_card", + "Name": "name", +} + +// 个人涉诉 G35SC01 +var G35SC01FieldMapping = map[string]string{ + "Name": "name", + "IDCard": "idcard", + "InquiredAuth": "inquired_auth", +} + +// 单人婚姻 G09SC02 +var G09SC02FieldMapping = map[string]string{ + "IDCard": "certNumMan", + "Name": "nameMan", +} + +// 借贷意向 G27BJ05 +var G27BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", +} + +// 借贷行为 G28BJ05 +var G28BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", +} + +// 股东人企关系精准版 G05HZ01 +var G05HZ01FieldMapping = map[string]string{ + "IDCard": "pid", +} diff --git a/app/user/cmd/api/internal/types/queryParams.go b/app/user/cmd/api/internal/types/queryParams.go new file mode 100644 index 0000000..1c243d5 --- /dev/null +++ b/app/user/cmd/api/internal/types/queryParams.go @@ -0,0 +1,73 @@ +package types + +type WestDexServiceRequestParams struct { + FieldMapping map[string]string + ApiID string +} + +var WestDexParams = map[string][]WestDexServiceRequestParams{ + "marriage": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + + }, + "backgroundcheck": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "companyinfo": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "homeservice": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "preloanbackgroundcheck": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "rentalinfo": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "riskassessment": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, +} diff --git a/app/user/cmd/api/internal/types/taskname.go b/app/user/cmd/api/internal/types/taskname.go new file mode 100644 index 0000000..98029d9 --- /dev/null +++ b/app/user/cmd/api/internal/types/taskname.go @@ -0,0 +1,3 @@ +package types + +const MsgPaySuccessQuery = "msg:pay_success:query" diff --git a/app/user/cmd/api/internal/types/types.go b/app/user/cmd/api/internal/types/types.go index 191c06a..9776b50 100644 --- a/app/user/cmd/api/internal/types/types.go +++ b/app/user/cmd/api/internal/types/types.go @@ -1,6 +1,20 @@ // Code generated by goctl. DO NOT EDIT. package types +type Feature struct { + ID int64 `json:"id"` // 功能ID + ApiID string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 +} + +type GetProductByEnRequest struct { + ProductEn string `path:"product_en"` +} + +type GetProductByIDRequest struct { + Id int64 `path:"id"` +} + type MobileCodeLoginReq struct { Mobile string `json:"mobile"` Code string `json:"code" validate:"required"` @@ -23,6 +37,80 @@ type MobileLoginResp struct { RefreshAfter int64 `json:"refreshAfter"` } +type Product struct { + ProductName string `json:"product_name"` + ProductEn string `json:"product_en"` + Description string `json:"description"` + Notes string `json:"notes,optional"` + SellPrice float64 `json:"sell_price"` + Features []Feature `json:"features"` // 关联功能列表 +} + +type ProductResponse struct { + Product +} + +type Query struct { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductId int64 `json:"product_id"` // 产品ID + QueryData []map[string]interface{} `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type QueryDetailByOrderIdReq struct { + OrderId int64 `path:"order_id"` +} + +type QueryDetailByOrderIdResp struct { + Query +} + +type QueryDetailReq struct { + Id int64 `path:"id"` +} + +type QueryDetailResp struct { + Query +} + +type QueryExampleReq struct { + Feature string `form:"feature"` +} + +type QueryExampleResp struct { + Query +} + +type QueryListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type QueryListResp struct { + Total int64 `json:"total"` // 总记录数 + List []Query `json:"list"` // 查询列表 +} + +type QueryReq struct { + Data string `json:"data" validate:"required"` +} + +type QueryResp struct { + PrepayID string `json:"prepay_id"` + OrderID int64 `json:"order_id"` +} + +type QueryRetryReq struct { + Id int64 `path:"id"` +} + +type QueryRetryResp struct { +} + type RegisterReq struct { Mobile string `json:"mobile" validate:"required,mobile"` Password string `json:"password" validate:"required,min=11,max=11,password"` @@ -41,9 +129,6 @@ type User struct { NickName string `json:"nickName"` } -type UserInfoReq struct { -} - type UserInfoResp struct { UserInfo User `json:"userInfo"` } @@ -62,5 +147,5 @@ type WXMiniAuthResp struct { type SendSmsReq struct { Mobile string `json:"mobile" validate:"required,mobile"` - ActionType string `json:"actionType" validate:"required,oneof=loginCode registerCode QueryCode"` + ActionType string `json:"actionType" validate:"required,oneof=login register query"` } diff --git a/app/user/cmd/api/merchant/apiclient_key.pem b/app/user/cmd/api/merchant/apiclient_key.pem new file mode 100644 index 0000000..246c1df --- /dev/null +++ b/app/user/cmd/api/merchant/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCP6fWm1vXXybH +m3Ne6PjacGrN2+iMrzWZlzdHCZ31udDPqSUYaZ+78b441KZK/CJFQWeSJ/1h//A+ +BGsQDKvE/fj2QzN1KkOuQ8WJXNGpixE5uu5bv/QTN/ukurGdA1aO2aFCANumlOmB +HkB/B2so57ii8iQQjwK2xM4r3oOU/IfcFGKL+9/QjLGFFp9PJXCDBCgrxxlZGaj1 +3wowlfVOzlaX94gemQsCYVkuAFIYMAnFHs9cKNZQIU80somW/yy2Gy38N6n7NnbD +nvFSaq4GoDROqRgKbRZ5e706d/p7A3aS/2oRqq1jomUIugK8g++LmoHFTgfhfQkI +v1aG/nPzAgMBAAECggEAD2RN31J2J42xm/V0YdviBCUOQXugZK1peN8jkSxw6Myt +gBbuCo4sCw9vvD8VYjGyYXx6QXmLuV03YyKkfSQT5EsflBvlEu6jaEaUe3rwXhfX +6JQoWPrP00oHVZk5g7CFBlK2VW2N+hgonIOSJr6mvhoGZlr7gphiZasYjx9Vm9N3 +Pbnfru5ttzplYNniwH3DF6ph8VmdbD1nnbWSKLXvHCsXQT2wBcnsIagIH3vyq6K1 +pc5abWsQJrixOPebpI8jD5w0HxHAqVLx58H/OC2zW/roAw1WS2AkueJ1j7dQ7Z0C +mc9Xexz5gcAP0nMAQv+LP7iYqsa/niFhfcTFWfdxkQKBgQD5JkKNmInU2/IVYCwO +c483MCSv1+MnbRXlb7vut8T0IupHTU6hCge6C3q3HsjbKSBn8bRChtPUzvw9JFxK +QWKiQqQDPLDJ08AIKhfQD2JiLtoikkZN0bF6OTL+Soney1yGx51mlfHM194+PcCJ +jF7iWdMVbcBwHbgydNxxIS5cKQKBgQDHlvQ4lw6gvLILpGK494/vNYSJP/Jmd66V +3oSGYi84YRKTSwH4NlbBVVieb3Dv+pPugbsXEuFHBif7WsivbYgNTE9++8Yvt0gh +duB1G4yh7m/ylQeSuipgQU9tozrU/15cWwmcCRV50wWXBGoVEM0kf7mzEKSxmjYk +Qzko/zxSuwKBgQCY6Bc+SViFz3qSDdTcBaXma+CIHsmlH7ipd9px1kzEvEzl95cD +FGHLl1H34qfIgUQHJvrHPXHyEBoT+CW/2MMM7DM2XV/ubctT92ln4pkxwqlTQExv +Y/s1FLesAtj8Z/hgK0/5bprYab9WmZV5lTGCXzhB1XqeFE9AgCHuODv4iQKBgQC8 +g2uwd5ytXQydymokYk9klJvWNrvw5GHV1BJAC0Smb6lnzZTSqCBRAxdsrb1yLK7E +u2vGY2K7/qiM1DZw23eBd+4t9gg+0VIjqXBfq+GsoNTDvtckUwnrWER5PY831ut9 +N89fvYS3SAUjmlvIAdKBAtKWusWTqiAxJ/05J7oGOQKBgB5PSr5i0LlupIbKui9t +XtXnRqGPxxrZZUpTkyrGOAnlCz/zq2QiwFpBWo/NMHOp0KmxzJpQ8yEY2LWlRZ61 +Oc9m0J/HtPw3Ohi1treBosEVG/0NOI9Tq1Obny23N51MVibdW6zEIyGUp/DbFS8h +5DljdOYX9IYIHHn3Ig4GeTGe +-----END PRIVATE KEY----- diff --git a/app/user/cmd/api/user.go b/app/user/cmd/api/user.go deleted file mode 100644 index cf25b5d..0000000 --- a/app/user/cmd/api/user.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "qnc-server/app/user/cmd/api/internal/config" - "qnc-server/app/user/cmd/api/internal/handler" - "qnc-server/app/user/cmd/api/internal/svc" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/rest" -) - -var configFile = flag.String("f", "etc/user.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - conf.MustLoad(*configFile, &c) - - ctx := svc.NewServiceContext(c) - server := rest.MustNewServer(c.RestConf) - - defer server.Stop() - - handler.RegisterHandlers(server, ctx) - - fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) - server.Start() -} diff --git a/app/user/model/userauthmodel.go b/app/user/model/userauthmodel.go deleted file mode 100644 index 0812e1d..0000000 --- a/app/user/model/userauthmodel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ UserAuthModel = (*customUserAuthModel)(nil) - -type ( - // UserAuthModel is an interface to be customized, add more methods here, - // and implement the added methods in customUserAuthModel. - UserAuthModel interface { - userAuthModel - } - - customUserAuthModel struct { - *defaultUserAuthModel - } -) - -// NewUserAuthModel returns a model for the database table. -func NewUserAuthModel(conn sqlx.SqlConn, c cache.CacheConf) UserAuthModel { - return &customUserAuthModel{ - defaultUserAuthModel: newUserAuthModel(conn, c), - } -} diff --git a/app/user/model/userauthmodel_gen.go b/app/user/model/userauthmodel_gen.go deleted file mode 100644 index 833d9b6..0000000 --- a/app/user/model/userauthmodel_gen.go +++ /dev/null @@ -1,435 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" - "qnc-server/common/globalkey" -) - -var ( - userAuthFieldNames = builder.RawFieldNames(&UserAuth{}) - userAuthRows = strings.Join(userAuthFieldNames, ",") - userAuthRowsExpectAutoSet = strings.Join(stringx.Remove(userAuthFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - userAuthRowsWithPlaceHolder = strings.Join(stringx.Remove(userAuthFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheUserAuthIdPrefix = "cache:userAuth:id:" - cacheUserAuthAuthTypeAuthKeyPrefix = "cache:userAuth:authType:authKey:" - cacheUserAuthUserIdAuthTypePrefix = "cache:userAuth:userId:authType:" -) - -type ( - userAuthModel interface { - Insert(ctx context.Context, session sqlx.Session, data *UserAuth) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*UserAuth, error) - FindOneByAuthTypeAuthKey(ctx context.Context, authType string, authKey string) (*UserAuth, error) - FindOneByUserIdAuthType(ctx context.Context, userId int64, authType string) (*UserAuth, error) - Update(ctx context.Context, session sqlx.Session, data *UserAuth) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *UserAuth) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *UserAuth) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*UserAuth, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*UserAuth, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*UserAuth, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*UserAuth, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*UserAuth, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultUserAuthModel struct { - sqlc.CachedConn - table string - } - - UserAuth struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime time.Time `db:"delete_time"` - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - UserId int64 `db:"user_id"` - AuthKey string `db:"auth_key"` // 平台唯一id - AuthType string `db:"auth_type"` // 平台类型 - } -) - -func newUserAuthModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultUserAuthModel { - return &defaultUserAuthModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`user_auth`", - } -} - -func (m *defaultUserAuthModel) Insert(ctx context.Context, session sqlx.Session, data *UserAuth) (sql.Result, error) { - data.DeleteTime = time.Unix(0, 0) - data.DelState = globalkey.DelStateNo - userAuthAuthTypeAuthKeyKey := fmt.Sprintf("%s%v:%v", cacheUserAuthAuthTypeAuthKeyPrefix, data.AuthType, data.AuthKey) - userAuthIdKey := fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, data.Id) - userAuthUserIdAuthTypeKey := fmt.Sprintf("%s%v:%v", cacheUserAuthUserIdAuthTypePrefix, data.UserId, data.AuthType) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?)", m.table, userAuthRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.AuthKey, data.AuthType) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.UserId, data.AuthKey, data.AuthType) - }, userAuthAuthTypeAuthKeyKey, userAuthIdKey, userAuthUserIdAuthTypeKey) -} - -func (m *defaultUserAuthModel) FindOne(ctx context.Context, id int64) (*UserAuth, error) { - userAuthIdKey := fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, id) - var resp UserAuth - err := m.QueryRowCtx(ctx, &resp, userAuthIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userAuthRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) FindOneByAuthTypeAuthKey(ctx context.Context, authType string, authKey string) (*UserAuth, error) { - userAuthAuthTypeAuthKeyKey := fmt.Sprintf("%s%v:%v", cacheUserAuthAuthTypeAuthKeyPrefix, authType, authKey) - var resp UserAuth - err := m.QueryRowIndexCtx(ctx, &resp, userAuthAuthTypeAuthKeyKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `auth_type` = ? and `auth_key` = ? and del_state = ? limit 1", userAuthRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, authType, authKey, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) FindOneByUserIdAuthType(ctx context.Context, userId int64, authType string) (*UserAuth, error) { - userAuthUserIdAuthTypeKey := fmt.Sprintf("%s%v:%v", cacheUserAuthUserIdAuthTypePrefix, userId, authType) - var resp UserAuth - err := m.QueryRowIndexCtx(ctx, &resp, userAuthUserIdAuthTypeKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `user_id` = ? and `auth_type` = ? and del_state = ? limit 1", userAuthRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, userId, authType, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) Update(ctx context.Context, session sqlx.Session, newData *UserAuth) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - userAuthAuthTypeAuthKeyKey := fmt.Sprintf("%s%v:%v", cacheUserAuthAuthTypeAuthKeyPrefix, data.AuthType, data.AuthKey) - userAuthIdKey := fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, data.Id) - userAuthUserIdAuthTypeKey := fmt.Sprintf("%s%v:%v", cacheUserAuthUserIdAuthTypePrefix, data.UserId, data.AuthType) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userAuthRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.UserId, newData.AuthKey, newData.AuthType, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.UserId, newData.AuthKey, newData.AuthType, newData.Id) - }, userAuthAuthTypeAuthKeyKey, userAuthIdKey, userAuthUserIdAuthTypeKey) -} - -func (m *defaultUserAuthModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *UserAuth) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - userAuthAuthTypeAuthKeyKey := fmt.Sprintf("%s%v:%v", cacheUserAuthAuthTypeAuthKeyPrefix, data.AuthType, data.AuthKey) - userAuthIdKey := fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, data.Id) - userAuthUserIdAuthTypeKey := fmt.Sprintf("%s%v:%v", cacheUserAuthUserIdAuthTypePrefix, data.UserId, data.AuthType) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, userAuthRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.UserId, newData.AuthKey, newData.AuthType, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.UserId, newData.AuthKey, newData.AuthType, newData.Id, oldVersion) - }, userAuthAuthTypeAuthKeyKey, userAuthIdKey, userAuthUserIdAuthTypeKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultUserAuthModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *UserAuth) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = time.Now() - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "UserAuthModel delete err : %+v", err) - } - return nil -} - -func (m *defaultUserAuthModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultUserAuthModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultUserAuthModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*UserAuth, error) { - - builder = builder.Columns(userAuthRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*UserAuth - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*UserAuth, error) { - - builder = builder.Columns(userAuthRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*UserAuth - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*UserAuth, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(userAuthRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*UserAuth - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultUserAuthModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*UserAuth, error) { - - builder = builder.Columns(userAuthRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*UserAuth - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*UserAuth, error) { - - builder = builder.Columns(userAuthRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*UserAuth - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserAuthModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultUserAuthModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultUserAuthModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - userAuthAuthTypeAuthKeyKey := fmt.Sprintf("%s%v:%v", cacheUserAuthAuthTypeAuthKeyPrefix, data.AuthType, data.AuthKey) - userAuthIdKey := fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, id) - userAuthUserIdAuthTypeKey := fmt.Sprintf("%s%v:%v", cacheUserAuthUserIdAuthTypePrefix, data.UserId, data.AuthType) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, userAuthAuthTypeAuthKeyKey, userAuthIdKey, userAuthUserIdAuthTypeKey) - return err -} -func (m *defaultUserAuthModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheUserAuthIdPrefix, primary) -} -func (m *defaultUserAuthModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userAuthRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultUserAuthModel) tableName() string { - return m.table -} diff --git a/app/user/model/usermodel.go b/app/user/model/usermodel.go deleted file mode 100644 index 8123712..0000000 --- a/app/user/model/usermodel.go +++ /dev/null @@ -1,27 +0,0 @@ -package model - -import ( - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var _ UserModel = (*customUserModel)(nil) - -type ( - // UserModel is an interface to be customized, add more methods here, - // and implement the added methods in customUserModel. - UserModel interface { - userModel - } - - customUserModel struct { - *defaultUserModel - } -) - -// NewUserModel returns a model for the database table. -func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel { - return &customUserModel{ - defaultUserModel: newUserModel(conn, c), - } -} diff --git a/app/user/model/usermodel_gen.go b/app/user/model/usermodel_gen.go deleted file mode 100644 index 77ebe19..0000000 --- a/app/user/model/usermodel_gen.go +++ /dev/null @@ -1,410 +0,0 @@ -// Code generated by goctl. DO NOT EDIT! - -package model - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "time" - - "github.com/Masterminds/squirrel" - "github.com/pkg/errors" - "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" - "github.com/zeromicro/go-zero/core/stores/sqlc" - "github.com/zeromicro/go-zero/core/stores/sqlx" - "github.com/zeromicro/go-zero/core/stringx" - "qnc-server/common/globalkey" -) - -var ( - userFieldNames = builder.RawFieldNames(&User{}) - userRows = strings.Join(userFieldNames, ",") - userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), ",") - userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`create_time`", "`update_time`"), "=?,") + "=?" - - cacheUserIdPrefix = "cache:user:id:" - cacheUserMobilePrefix = "cache:user:mobile:" -) - -type ( - userModel interface { - Insert(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) - FindOne(ctx context.Context, id int64) (*User, error) - FindOneByMobile(ctx context.Context, mobile string) (*User, error) - Update(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) - UpdateWithVersion(ctx context.Context, session sqlx.Session, data *User) error - Trans(ctx context.Context, fn func(context context.Context, session sqlx.Session) error) error - SelectBuilder() squirrel.SelectBuilder - DeleteSoft(ctx context.Context, session sqlx.Session, data *User) error - FindSum(ctx context.Context, sumBuilder squirrel.SelectBuilder, field string) (float64, error) - FindCount(ctx context.Context, countBuilder squirrel.SelectBuilder, field string) (int64, error) - FindAll(ctx context.Context, rowBuilder squirrel.SelectBuilder, orderBy string) ([]*User, error) - FindPageListByPage(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, error) - FindPageListByPageWithTotal(ctx context.Context, rowBuilder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, int64, error) - FindPageListByIdDESC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*User, error) - FindPageListByIdASC(ctx context.Context, rowBuilder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*User, error) - Delete(ctx context.Context, session sqlx.Session, id int64) error - } - - defaultUserModel struct { - sqlc.CachedConn - table string - } - - User struct { - Id int64 `db:"id"` - CreateTime time.Time `db:"create_time"` - UpdateTime time.Time `db:"update_time"` - DeleteTime time.Time `db:"delete_time"` - DelState int64 `db:"del_state"` - Version int64 `db:"version"` // 版本号 - Mobile string `db:"mobile"` - Password string `db:"password"` - Nickname string `db:"nickname"` - Info string `db:"info"` - } -) - -func newUserModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultUserModel { - return &defaultUserModel{ - CachedConn: sqlc.NewConn(conn, c), - table: "`user`", - } -} - -func (m *defaultUserModel) Insert(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) { - data.DeleteTime = time.Unix(0, 0) - data.DelState = globalkey.DelStateNo - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) - userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?)", m.table, userRowsExpectAutoSet) - if session != nil { - return session.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info) - } - return conn.ExecCtx(ctx, query, data.DeleteTime, data.DelState, data.Version, data.Mobile, data.Password, data.Nickname, data.Info) - }, userIdKey, userMobileKey) -} - -func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) { - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) - var resp User - err := m.QueryRowCtx(ctx, &resp, userIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id, globalkey.DelStateNo) - }) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserModel) FindOneByMobile(ctx context.Context, mobile string) (*User, error) { - userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile) - var resp User - err := m.QueryRowIndexCtx(ctx, &resp, userMobileKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { - query := fmt.Sprintf("select %s from %s where `mobile` = ? and del_state = ? limit 1", userRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, mobile, globalkey.DelStateNo); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) - switch err { - case nil: - return &resp, nil - case sqlc.ErrNotFound: - return nil, ErrNotFound - default: - return nil, err - } -} - -func (m *defaultUserModel) Update(ctx context.Context, session sqlx.Session, newData *User) (sql.Result, error) { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return nil, err - } - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) - userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile) - return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Id) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Id) - }, userIdKey, userMobileKey) -} - -func (m *defaultUserModel) UpdateWithVersion(ctx context.Context, session sqlx.Session, newData *User) error { - - oldVersion := newData.Version - newData.Version += 1 - - var sqlResult sql.Result - var err error - - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) - userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile) - sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, userRowsWithPlaceHolder) - if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Id, oldVersion) - } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.Mobile, newData.Password, newData.Nickname, newData.Info, newData.Id, oldVersion) - }, userIdKey, userMobileKey) - if err != nil { - return err - } - updateCount, err := sqlResult.RowsAffected() - if err != nil { - return err - } - if updateCount == 0 { - return ErrNoRowsUpdate - } - - return nil -} - -func (m *defaultUserModel) DeleteSoft(ctx context.Context, session sqlx.Session, data *User) error { - data.DelState = globalkey.DelStateYes - data.DeleteTime = time.Now() - if err := m.UpdateWithVersion(ctx, session, data); err != nil { - return errors.Wrapf(errors.New("delete soft failed "), "UserModel delete err : %+v", err) - } - return nil -} - -func (m *defaultUserModel) FindSum(ctx context.Context, builder squirrel.SelectBuilder, field string) (float64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindSum Least One Field"), "FindSum Least One Field") - } - - builder = builder.Columns("IFNULL(SUM(" + field + "),0)") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp float64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultUserModel) FindCount(ctx context.Context, builder squirrel.SelectBuilder, field string) (int64, error) { - - if len(field) == 0 { - return 0, errors.Wrapf(errors.New("FindCount Least One Field"), "FindCount Least One Field") - } - - builder = builder.Columns("COUNT(" + field + ")") - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return 0, err - } - - var resp int64 - err = m.QueryRowNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return 0, err - } -} - -func (m *defaultUserModel) FindAll(ctx context.Context, builder squirrel.SelectBuilder, orderBy string) ([]*User, error) { - - builder = builder.Columns(userRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).ToSql() - if err != nil { - return nil, err - } - - var resp []*User - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, error) { - - builder = builder.Columns(userRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*User - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserModel) FindPageListByPageWithTotal(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*User, int64, error) { - - total, err := m.FindCount(ctx, builder, "id") - if err != nil { - return nil, 0, err - } - - builder = builder.Columns(userRows) - - if orderBy == "" { - builder = builder.OrderBy("id DESC") - } else { - builder = builder.OrderBy(orderBy) - } - - if page < 1 { - page = 1 - } - offset := (page - 1) * pageSize - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, total, err - } - - var resp []*User - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, total, nil - default: - return nil, total, err - } -} - -func (m *defaultUserModel) FindPageListByIdDESC(ctx context.Context, builder squirrel.SelectBuilder, preMinId, pageSize int64) ([]*User, error) { - - builder = builder.Columns(userRows) - - if preMinId > 0 { - builder = builder.Where(" id < ? ", preMinId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id DESC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*User - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserModel) FindPageListByIdASC(ctx context.Context, builder squirrel.SelectBuilder, preMaxId, pageSize int64) ([]*User, error) { - - builder = builder.Columns(userRows) - - if preMaxId > 0 { - builder = builder.Where(" id > ? ", preMaxId) - } - - query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).OrderBy("id ASC").Limit(uint64(pageSize)).ToSql() - if err != nil { - return nil, err - } - - var resp []*User - err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...) - switch err { - case nil: - return resp, nil - default: - return nil, err - } -} - -func (m *defaultUserModel) Trans(ctx context.Context, fn func(ctx context.Context, session sqlx.Session) error) error { - - return m.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error { - return fn(ctx, session) - }) - -} - -func (m *defaultUserModel) SelectBuilder() squirrel.SelectBuilder { - return squirrel.Select().From(m.table) -} -func (m *defaultUserModel) Delete(ctx context.Context, session sqlx.Session, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) - userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - if session != nil { - return session.ExecCtx(ctx, query, id) - } - return conn.ExecCtx(ctx, query, id) - }, userIdKey, userMobileKey) - return err -} -func (m *defaultUserModel) formatPrimary(primary interface{}) string { - return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary) -} -func (m *defaultUserModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error { - query := fmt.Sprintf("select %s from %s where `id` = ? and del_state = ? limit 1", userRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary, globalkey.DelStateNo) -} - -func (m *defaultUserModel) tableName() string { - return m.table -} diff --git a/app/user/model/vars.go b/app/user/model/vars.go deleted file mode 100644 index 4723f1b..0000000 --- a/app/user/model/vars.go +++ /dev/null @@ -1,15 +0,0 @@ -package model - -import ( - "errors" - "github.com/zeromicro/go-zero/core/stores/sqlx" -) - -var ErrNotFound = sqlx.ErrNotFound -var ErrNoRowsUpdate = errors.New("update db no rows change") - -var UserAuthTypeAppMobile string = "app_mobile" //平台内部 -var UserAuthTypeAppWechat string = "app_wechat" //微信小程序 -var UserAuthTypeH5Mobile string = "h5_mobile" -var UserAuthTypeWxMini string = "wx_mini" -var UserAuthTypeWxOfficialAccount string = "wx_official_account" diff --git a/common/xerr/errCode.go b/common/xerr/errCode.go index 747ce99..ade3e6f 100644 --- a/common/xerr/errCode.go +++ b/common/xerr/errCode.go @@ -15,4 +15,6 @@ const DB_UPDATE_AFFECTED_ZERO_ERROR uint32 = 100006 const PARAM_VERIFICATION_ERROR uint32 = 100007 const CUSTOM_ERROR uint32 = 100008 -//用户模块 +const LOGIN_FAILED uint32 = 200001 +const LOGIC_QUERY_WAIT uint32 = 200002 +const LOGIC_QUERY_ERROR uint32 = 200003 diff --git a/deploy/script/genModel.ps1 b/deploy/script/genModel.ps1 new file mode 100644 index 0000000..fcec7be --- /dev/null +++ b/deploy/script/genModel.ps1 @@ -0,0 +1,25 @@ +# 使用方法: +# .\genModel.ps1 user user +# .\genModel.ps1 user user_auth +# 再将 .\genModel 下的文件剪切到对应服务的 model 目录里面,记得改 package +# goctl model mysql datasource -url="qnc:5vg67b3UNHu8@tcp(127.0.0.1:20001)/qnc" -table="product" -dir="./model" --home="../template" -cache=true --style=goZero +param ( + [string]$database, + [string]$tables +) + +# 生成的表名 +$modeldir = "./genModel" +$templateDir = Join-Path -Path (Resolve-Path "$PSScriptRoot/..") -ChildPath "template" +# 数据库配置 +$host = "127.0.0.1" +$port = "20001" +$dbname = "$database" +$username = "qnc" +$passwd = "5vg67b3UNHu8" + +Write-Output "开始创建库:$dbname 的表:$tables" + +# 执行 goctl 命令生成 model +$command = "goctl model mysql datasource -url=`"$username`:$passwd`@tcp($host`:$port)/$dbname`" -table=`"$tables`" -dir=`"$modeldir`" --home=`"$templateDir`" -cache=true --style=goZero" +Invoke-Expression $command diff --git a/deploy/sql/order.sql b/deploy/sql/order.sql new file mode 100644 index 0000000..8824ce2 --- /dev/null +++ b/deploy/sql/order.sql @@ -0,0 +1,25 @@ +CREATE TABLE `order` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `order_no` varchar(32) NOT NULL COMMENT '自生成的订单号', + `user_id` bigint NOT NULL COMMENT '用户ID', + `product_id` bigint NOT NULL COMMENT '产品ID(软关联到产品表)', + `payment_platform` enum('alipay', 'wechat', 'other') NOT NULL COMMENT '支付平台(支付宝、微信、其他)', + `payment_scene` enum('app', 'h5', 'mini_program', 'public_account') NOT NULL COMMENT '支付场景(App、H5、微信小程序、公众号)', + `platform_order_id` varchar(64) DEFAULT NULL COMMENT '支付平台订单号', + `amount` decimal(10, 2) NOT NULL COMMENT '支付金额', + `status` enum('pending', 'paid', 'failed', 'refunded', 'closed') NOT NULL DEFAULT 'pending' COMMENT '支付状态', + `del_state` tinyint NOT NULL DEFAULT '0' COMMENT '删除状态', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `pay_time` datetime DEFAULT NULL COMMENT '支付时间', + `refund_time` datetime DEFAULT NULL COMMENT '退款时间', + `close_time` datetime DEFAULT NULL COMMENT '订单关闭时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_order_no` (`order_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_product_id` (`product_id`), + KEY `idx_payment_platform` (`payment_platform`), + KEY `idx_payment_scene` (`payment_scene`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='订单表'; diff --git a/deploy/sql/product.sql b/deploy/sql/product.sql new file mode 100644 index 0000000..bfa6d08 --- /dev/null +++ b/deploy/sql/product.sql @@ -0,0 +1,99 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for product +-- ---------------------------- +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT '0' COMMENT '删除状态', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', + `product_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '服务名', + `product_en` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '英文名', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '描述', + `notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '备注', + `cost_price` DECIMAL(10, 2) NOT NULL DEFAULT '1.00' COMMENT '成本', + `sell_price` DECIMAL(10, 2) NOT NULL DEFAULT '1.00' COMMENT '售价', + PRIMARY KEY (`id`) + UNIQUE KEY `unique_product_name` (`product_name`), + UNIQUE KEY `unique_product_en` (`product_en`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='产品表'; + +-- ---------------------------- +-- Records for product +-- ---------------------------- +INSERT INTO `product` (`product_name`, `product_en`, `description`, `notes`, `cost_price`, `sell_price`) VALUES + ('背景调查', 'backgroundchecklogic', '', '', 1, 1), + ('企业报告', 'companyinfologic', '', '', 1, 1), + ('家政服务', 'homeservicelogic', '', '', 1, 1), + ('婚姻状态', 'marriagelogic', '', '', 1, 1), + ('贷前背调', 'preloanbackgroundchecklogic', '', '', 1, 1), + ('租赁服务', 'rentalinfologic', '', '', 1, 1), + ('个人风险评估', 'riskassessmentlogic', '', '', 1, 1); + +SET FOREIGN_KEY_CHECKS = 1; + + + +-- ---------------------------- +-- Table structure for feature +-- ---------------------------- +DROP TABLE IF EXISTS `feature`; +CREATE TABLE `feature` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT '0' COMMENT '删除状态', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', + `api_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'API标识', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '描述', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_api_id` (`api_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='功能表'; + +-- ---------------------------- +-- Table structure for product_feature +-- ---------------------------- +DROP TABLE IF EXISTS `product_feature`; +CREATE TABLE `product_feature` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `product_id` bigint NOT NULL COMMENT '产品ID', + `feature_id` bigint NOT NULL COMMENT '功能ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + `del_state` tinyint NOT NULL DEFAULT '0' COMMENT '删除状态', + `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_product_feature` (`product_id`, `feature_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='产品与功能关联表'; + +-- ---------------------------- +-- Records for feature +-- ---------------------------- +INSERT INTO `feature` (`api_id`, `name`) VALUES + ('G09SC02', '单人婚姻'), + ('G27BJ05', '借贷意向'), + ('G28BJ05', '借贷行为'), + ('G26BJ05', '特殊名单'), + ('G34BJ03', '个人不良'), + ('G35SC01', '个人涉诉'), + ('G05HZ01', '股东人企关系'); + +-- ---------------------------- +-- 插入每个产品与每个功能的对应关系 +-- ---------------------------- + +INSERT INTO `product_feature` (`product_id`, `feature_id`) +SELECT + p.id AS product_id, + f.id AS feature_id +FROM + product p + CROSS JOIN + feature f; \ No newline at end of file diff --git a/deploy/sql/query.sql b/deploy/sql/query.sql new file mode 100644 index 0000000..2d73255 --- /dev/null +++ b/deploy/sql/query.sql @@ -0,0 +1,18 @@ +CREATE TABLE `query` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `order_id` BIGINT NOT NULL COMMENT '订单ID(软关联到订单表)', + `user_id` BIGINT NOT NULL COMMENT '用户ID(直接关联到用户)', + `product_id` BIGINT NOT NULL COMMENT '产品ID(直接关联到产品)', + `query_params` TEXT NOT NULL COMMENT '查询params数据', + `query_data` LONGTEXT COMMENT '查询结果数据', + `query_state` ENUM('pending', 'success', 'failed') NOT NULL DEFAULT 'pending' COMMENT '查询状态', + `del_state` TINYINT NOT NULL DEFAULT '0' COMMENT '删除状态', + `version` BIGINT NOT NULL DEFAULT '0' COMMENT '版本号', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` DATETIME DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_order_id` (`order_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_product_id` (`product_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='查询结果表,存储关联订单的查询数据'; diff --git a/deploy/sql/user.sql b/deploy/sql/user.sql index f50d4a4..5f9aed8 100644 --- a/deploy/sql/user.sql +++ b/deploy/sql/user.sql @@ -9,15 +9,15 @@ CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', `del_state` tinyint NOT NULL DEFAULT '0', `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', `mobile` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', - `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), - UNIQUE KEY `idx_mobile` (`mobile`) + UNIQUE KEY `unique_mobile` (`mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; -- ---------------------------- @@ -28,15 +28,15 @@ CREATE TABLE `user_auth` ( `id` bigint NOT NULL AUTO_INCREMENT, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', `del_state` tinyint NOT NULL DEFAULT '0', `version` bigint NOT NULL DEFAULT '0' COMMENT '版本号', `user_id` bigint NOT NULL DEFAULT '0', `auth_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '平台唯一id', `auth_type` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '平台类型', PRIMARY KEY (`id`), - UNIQUE KEY `idx_type_key` (`auth_type`,`auth_key`) USING BTREE, - UNIQUE KEY `idx_userId_key` (`user_id`,`auth_type`) + UNIQUE KEY `unique_type_key` (`auth_type`,`auth_key`) USING BTREE, + UNIQUE KEY `unique_userId_key` (`user_id`,`auth_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户授权表'; SET FOREIGN_KEY_CHECKS = 1; diff --git a/deploy/template/model/insert.tpl b/deploy/template/model/insert.tpl index 832e588..d8060e8 100644 --- a/deploy/template/model/insert.tpl +++ b/deploy/template/model/insert.tpl @@ -1,6 +1,5 @@ func (m *default{{.upperStartCamelObject}}Model) Insert(ctx context.Context,session sqlx.Session, data *{{.upperStartCamelObject}}) (sql.Result,error) { - data.DeleteTime = time.Unix(0,0) data.DelState = globalkey.DelStateNo {{if .withCache}}{{.keys}} return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { diff --git a/deploy/template/model/update.tpl b/deploy/template/model/update.tpl index 14e6618..b01e30f 100644 --- a/deploy/template/model/update.tpl +++ b/deploy/template/model/update.tpl @@ -65,7 +65,7 @@ func (m *default{{.upperStartCamelObject}}Model) UpdateWithVersion(ctx context.C func (m *default{{.upperStartCamelObject}}Model) DeleteSoft(ctx context.Context,session sqlx.Session,data *{{.upperStartCamelObject}}) error { data.DelState = globalkey.DelStateYes - data.DeleteTime = time.Now() + data.DeleteTime = sql.NullTime{Time: time.Now(), Valid: true} if err:= m.UpdateWithVersion(ctx,session, data);err!= nil{ return errors.Wrapf(errors.New("delete soft failed "),"{{.upperStartCamelObject}}Model delete err : %+v",err) } diff --git a/docker-compose-env.yml b/docker-compose-env.yml index c0eab6b..32c249e 100644 --- a/docker-compose-env.yml +++ b/docker-compose-env.yml @@ -31,8 +31,8 @@ services: restart: always networks: - qnc_net + - 1panel-network - #redis容器 - Redis container redis: image: redis:7.4.0 container_name: qnc_redis @@ -50,8 +50,22 @@ services: networks: - qnc_net + asynqmon: + image: hibiken/asynqmon:latest + container_name: qnc_asynqmon + ports: + - "20003:8080" + command: + - '--redis-addr=qnc_redis:6379' + - '--redis-password=3m3WsgyCKWqz' + restart: always + networks: + - qnc_net + depends_on: + - redis networks: qnc_net: driver: bridge - + 1panel-network: + external: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 1928f1e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: '3' - -services: - qnc_user_api: - image: cosmtrek/air - container_name: qnc_user_api - environment: - - TZ=Asia/Shanghai - working_dir: /app # 将工作目录设置为根目录 - ports: - - 31001:8888 - volumes: - - .:/app # 将项目根目录挂载到容器根目录 - entrypoint: air -c ./app/user/cmd/api/.air.toml - privileged: true - restart: always - networks: - - qnc_net - - -networks: - qnc_net: - driver: bridge - diff --git a/go.mod b/go.mod index d9aa60d..907bbc2 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,12 @@ require ( github.com/alibabacloud-go/tea-utils/v2 v2.0.7 github.com/go-playground/validator/v10 v10.22.1 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/jinzhu/copier v0.4.0 github.com/pkg/errors v0.9.1 github.com/shopspring/decimal v1.4.0 + github.com/smartwalle/alipay/v3 v3.2.23 github.com/sony/sonyflake v1.2.0 + github.com/wechatpay-apiv3/wechatpay-go v0.2.20 github.com/zeromicro/go-zero v1.7.3 google.golang.org/grpc v1.67.1 ) @@ -40,7 +43,7 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/jinzhu/copier v0.4.0 // indirect + github.com/hibiken/asynq v0.25.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -52,13 +55,22 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/panjf2000/ants/v2 v2.10.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/smartwalle/ncrypto v1.0.4 // indirect + github.com/smartwalle/ngx v1.0.9 // indirect + github.com/smartwalle/nsign v1.0.9 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect @@ -74,8 +86,10 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/protobuf v1.35.1 // indirect diff --git a/go.sum b/go.sum index e9005d1..48cd053 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= @@ -134,6 +136,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hibiken/asynq v0.25.0 h1:VCPyRRrrjFChsTSI8x5OCPu51MlEz6Rk+1p0kHKnZug= +github.com/hibiken/asynq v0.25.0/go.mod h1:DYQ1etBEl2Y+uSkqFElGYbk3M0ujLVwCfWE+TlvxtEk= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -173,6 +177,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= +github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -192,10 +198,20 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE= +github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= +github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= +github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= +github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= +github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= +github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E= +github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -203,6 +219,8 @@ github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -214,12 +232,23 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/wechatpay-apiv3/wechatpay-go v0.2.20 h1:gS8oFn1bHGnyapR2Zb4aqTV6l4kJWgbtqjCq6k1L9DQ= +github.com/wechatpay-apiv3/wechatpay-go v0.2.20/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -301,6 +330,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -339,6 +371,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/modd.conf b/modd.conf deleted file mode 100644 index d629e87..0000000 --- a/modd.conf +++ /dev/null @@ -1,5 +0,0 @@ -#user -app/user/cmd/api/**/*.go { - prep: go build -o data/server/user-api -v app/user/cmd/api/user.go - daemon +sigkill: ./data/server/user-api -f app/user/cmd/api/etc/user.yaml -} diff --git a/pkg/lzkit/crypto/crypto.go b/pkg/lzkit/crypto/crypto.go new file mode 100644 index 0000000..65c840d --- /dev/null +++ b/pkg/lzkit/crypto/crypto.go @@ -0,0 +1,105 @@ +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "io" +) + +// PKCS7填充 +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// 去除PKCS7填充 +func PKCS7UnPadding(origData []byte) ([]byte, error) { + length := len(origData) + if length == 0 { + return nil, errors.New("input data error") + } + unpadding := int(origData[length-1]) + if unpadding > length { + return nil, errors.New("unpadding size is invalid") + } + + // 检查填充字节是否一致 + for i := 0; i < unpadding; i++ { + if origData[length-1-i] != byte(unpadding) { + return nil, errors.New("invalid padding") + } + } + + return origData[:(length - unpadding)], nil +} + +// AES CBC模式加密,Base64传入传出 +func AesEncrypt(plainText, key []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + blockSize := block.BlockSize() + plainText = PKCS7Padding(plainText, blockSize) + + cipherText := make([]byte, blockSize+len(plainText)) + iv := cipherText[:blockSize] // 使用前blockSize字节作为IV + _, err = io.ReadFull(rand.Reader, iv) + if err != nil { + return "", err + } + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(cipherText[blockSize:], plainText) + + return base64.StdEncoding.EncodeToString(cipherText), nil +} + +// AES CBC模式解密,Base64传入传出 +func AesDecrypt(cipherTextBase64 string, key []byte) ([]byte, error) { + cipherText, err := base64.StdEncoding.DecodeString(cipherTextBase64) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + blockSize := block.BlockSize() + if len(cipherText) < blockSize { + return nil, errors.New("ciphertext too short") + } + + iv := cipherText[:blockSize] + cipherText = cipherText[blockSize:] + + if len(cipherText)%blockSize != 0 { + return nil, errors.New("ciphertext is not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(cipherText, cipherText) + + plainText, err := PKCS7UnPadding(cipherText) + if err != nil { + return nil, err + } + + return plainText, nil +} + +// Md5Encrypt 用于对传入的message进行MD5加密 +func Md5Encrypt(message string) string { + hash := md5.New() + hash.Write([]byte(message)) // 将字符串转换为字节切片并写入 + return hex.EncodeToString(hash.Sum(nil)) // 将哈希值转换为16进制字符串并返回 +} diff --git a/pkg/lzkit/crypto/generate.go b/pkg/lzkit/crypto/generate.go new file mode 100644 index 0000000..2d91c6d --- /dev/null +++ b/pkg/lzkit/crypto/generate.go @@ -0,0 +1,63 @@ +package crypto + +import ( + "crypto/rand" + "encoding/hex" + "io" + mathrand "math/rand" + "strconv" + "time" +) + +// 生成AES-128密钥的函数,符合市面规范 +func GenerateSecretKey() (string, error) { + key := make([]byte, 16) // 16字节密钥 + _, err := io.ReadFull(rand.Reader, key) + if err != nil { + return "", err + } + return hex.EncodeToString(key), nil +} + +func GenerateSecretId() (string, error) { + // 创建一个字节数组,用于存储随机数据 + bytes := make([]byte, 8) // 因为每个字节表示两个16进制字符 + + // 读取随机字节到数组中 + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + + // 将字节数组转换为16进制字符串 + return hex.EncodeToString(bytes), nil +} + +// GenerateTransactionID 生成16位数的交易单号 +func GenerateTransactionID() string { + length := 16 + // 获取当前时间戳 + timestamp := time.Now().UnixNano() + + // 转换为字符串 + timeStr := strconv.FormatInt(timestamp, 10) + + // 生成随机数 + mathrand.Seed(time.Now().UnixNano()) + randomPart := strconv.Itoa(mathrand.Intn(1000000)) + + // 组合时间戳和随机数 + combined := timeStr + randomPart + + // 如果长度超出指定值,则截断;如果不够,则填充随机字符 + if len(combined) >= length { + return combined[:length] + } + + // 如果长度不够,填充0 + for len(combined) < length { + combined += strconv.Itoa(mathrand.Intn(10)) // 填充随机数 + } + + return combined +} diff --git a/pkg/lzkit/crypto/west_crypto.go b/pkg/lzkit/crypto/west_crypto.go new file mode 100644 index 0000000..71d8a41 --- /dev/null +++ b/pkg/lzkit/crypto/west_crypto.go @@ -0,0 +1,150 @@ +package crypto + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + "encoding/base64" +) + +const ( + KEY_SIZE = 16 // AES-128, 16 bytes +) + +// Encrypt encrypts the given data using AES encryption in ECB mode with PKCS5 padding +func WestDexEncrypt(data, secretKey string) (string, error) { + key := generateAESKey(KEY_SIZE*8, []byte(secretKey)) + ciphertext, err := aesEncrypt([]byte(data), key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt decrypts the given base64-encoded string using AES encryption in ECB mode with PKCS5 padding +func WestDexDecrypt(encodedData, secretKey string) ([]byte, error) { + ciphertext, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + return nil, err + } + key := generateAESKey(KEY_SIZE*8, []byte(secretKey)) + plaintext, err := aesDecrypt(ciphertext, key) + if err != nil { + return nil, err + } + return plaintext, nil +} + +// generateAESKey generates a key for AES encryption using a SHA-1 based PRNG +func generateAESKey(length int, password []byte) []byte { + h := sha1.New() + h.Write(password) + state := h.Sum(nil) + + keyBytes := make([]byte, 0, length/8) + for len(keyBytes) < length/8 { + h := sha1.New() + h.Write(state) + state = h.Sum(nil) + keyBytes = append(keyBytes, state...) + } + + return keyBytes[:length/8] +} + +// aesEncrypt encrypts plaintext using AES in ECB mode with PKCS5 padding +func aesEncrypt(plaintext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + paddedPlaintext := pkcs5Padding(plaintext, block.BlockSize()) + ciphertext := make([]byte, len(paddedPlaintext)) + mode := newECBEncrypter(block) + mode.CryptBlocks(ciphertext, paddedPlaintext) + return ciphertext, nil +} + +// aesDecrypt decrypts ciphertext using AES in ECB mode with PKCS5 padding +func aesDecrypt(ciphertext, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + plaintext := make([]byte, len(ciphertext)) + mode := newECBDecrypter(block) + mode.CryptBlocks(plaintext, ciphertext) + return pkcs5Unpadding(plaintext), nil +} + +// pkcs5Padding pads the input to a multiple of the block size using PKCS5 padding +func pkcs5Padding(src []byte, blockSize int) []byte { + padding := blockSize - len(src)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padtext...) +} + +// pkcs5Unpadding removes PKCS5 padding from the input +func pkcs5Unpadding(src []byte) []byte { + length := len(src) + unpadding := int(src[length-1]) + return src[:(length - unpadding)] +} + +// ECB mode encryption/decryption +type ecb struct { + b cipher.Block + blockSize int +} + +func newECB(b cipher.Block) *ecb { + return &ecb{ + b: b, + blockSize: b.BlockSize(), + } +} + +type ecbEncrypter ecb + +func newECBEncrypter(b cipher.Block) cipher.BlockMode { + return (*ecbEncrypter)(newECB(b)) +} + +func (x *ecbEncrypter) BlockSize() int { return x.blockSize } + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Encrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +type ecbDecrypter ecb + +func newECBDecrypter(b cipher.Block) cipher.BlockMode { + return (*ecbDecrypter)(newECB(b)) +} + +func (x *ecbDecrypter) BlockSize() int { return x.blockSize } + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("crypto/cipher: input not full blocks") + } + if len(dst) < len(src) { + panic("crypto/cipher: output smaller than input") + } + for len(src) > 0 { + x.b.Decrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} diff --git a/pkg/lzkit/delay/ProgressiveDelay.go b/pkg/lzkit/delay/ProgressiveDelay.go new file mode 100644 index 0000000..7d2e657 --- /dev/null +++ b/pkg/lzkit/delay/ProgressiveDelay.go @@ -0,0 +1,65 @@ +package delay + +import ( + "errors" + "fmt" + "time" +) + +// ProgressiveDelay 用于管理渐进式延迟策略 +type ProgressiveDelay struct { + initialDelay time.Duration // 初始延迟时间 + growthFactor float64 // 延迟增长因子 (例如 1.5) + maxDelay time.Duration // 最大延迟时间 + maxRetryDuration time.Duration // 最大重试时间 + currentDelay time.Duration // 当前延迟时间 + startTime time.Time // 重试开始时间 +} + +// New 创建一个新的渐进式延迟对象 +func New(initialDelay, maxDelay, maxRetryDuration time.Duration, growthFactor float64) (*ProgressiveDelay, error) { + // 参数校验 + if initialDelay <= 0 { + return nil, errors.New("initialDelay must be greater than zero") + } + if maxDelay <= 0 { + return nil, errors.New("maxDelay must be greater than zero") + } + if maxRetryDuration <= 0 { + return nil, errors.New("maxRetryDuration must be greater than zero") + } + if growthFactor <= 1.0 { + return nil, errors.New("growthFactor must be greater than 1") + } + + // 初始化并返回 + return &ProgressiveDelay{ + initialDelay: initialDelay, + maxDelay: maxDelay, + maxRetryDuration: maxRetryDuration, + growthFactor: growthFactor, + currentDelay: initialDelay, + startTime: time.Now(), + }, nil +} + +// NextDelay 计算并返回下次的延迟时间 +func (pd *ProgressiveDelay) NextDelay() (time.Duration, error) { + // 检查最大重试时间是否已过 + if time.Since(pd.startTime) > pd.maxRetryDuration { + return 0, fmt.Errorf("最大重试时间超过限制: %v", pd.maxRetryDuration) + } + + // 返回当前延迟时间 + delay := pd.currentDelay + + // 计算下一个延迟时间并更新 currentDelay + pd.currentDelay = time.Duration(float64(pd.currentDelay) * pd.growthFactor) + + // 如果下次延迟超过最大延迟时间,限制在最大值 + if pd.currentDelay > pd.maxDelay { + pd.currentDelay = pd.maxDelay + } + + return delay, nil +} diff --git a/pkg/lzkit/lzUtils/sqlutls.go b/pkg/lzkit/lzUtils/sqlutls.go new file mode 100644 index 0000000..8e960e7 --- /dev/null +++ b/pkg/lzkit/lzUtils/sqlutls.go @@ -0,0 +1,38 @@ +package lzUtils + +import ( + "database/sql" + "time" +) + +// StringToNullString 将 string 转换为 sql.NullString +func StringToNullString(s string) sql.NullString { + return sql.NullString{ + String: s, + Valid: s != "", + } +} + +// NullStringToString 将 sql.NullString 转换为 string +func NullStringToString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} + +// TimeToNullTime 将 time.Time 转换为 sql.NullTime +func TimeToNullTime(t time.Time) sql.NullTime { + return sql.NullTime{ + Time: t, + Valid: !t.IsZero(), // 仅当 t 不是零值时才设置为有效 + } +} + +// NullTimeToTime 将 sql.NullTime 转换为 time.Time +func NullTimeToTime(nt sql.NullTime) time.Time { + if nt.Valid { + return nt.Time + } + return time.Time{} // 返回零值时间 +} diff --git a/pkg/lzkit/lzUtils/utils.go b/pkg/lzkit/lzUtils/utils.go new file mode 100644 index 0000000..69a9aca --- /dev/null +++ b/pkg/lzkit/lzUtils/utils.go @@ -0,0 +1,15 @@ +package lzUtils + +import "fmt" + +// ToWechatAmount 将金额从元转换为微信支付 SDK 需要的分(int64 类型) +func ToWechatAmount(amount float64) int64 { + // 将金额从元转换为分,并四舍五入 + return int64(amount*100 + 0.5) +} + +// ToAlipayAmount 将金额从元转换为支付宝支付 SDK 需要的字符串格式,保留两位小数 +func ToAlipayAmount(amount float64) string { + // 格式化为字符串,保留两位小数 + return fmt.Sprintf("%.2f", amount) +} diff --git a/pkg/lzkit/validator/validator.go b/pkg/lzkit/validator/validator.go index 706092d..15e17b9 100644 --- a/pkg/lzkit/validator/validator.go +++ b/pkg/lzkit/validator/validator.go @@ -47,6 +47,9 @@ func init() { if err := validate.RegisterValidation("password", validatePassword); err != nil { panic(fmt.Sprintf("注册 password 验证器时发生错误: %v", err)) } + if err := validate.RegisterValidation("payMethod", validatePayMethod); err != nil { + panic(fmt.Sprintf("注册 payMethod 验证器时发生错误: %v", err)) + } } @@ -152,3 +155,20 @@ func validatePassword(fl validator.FieldLevel) bool { return true } + +// 支付方式 +func validatePayMethod(fl validator.FieldLevel) bool { + payMethod := fl.Field().String() + + if payMethod == "" { + return true // 如果为空,认为是有效的 + } + + validTypes := map[string]bool{ + "alipay": true, // 中国电信 + "wechatpay": true, // 中国移动 + } + + return validTypes[payMethod] + +} diff --git a/test/test.go b/test/test.go new file mode 100644 index 0000000..8aded0f --- /dev/null +++ b/test/test.go @@ -0,0 +1,148 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/tidwall/gjson" + "sync" + "time" +) + +func processG35SC01Response(resp []byte) ([]byte, error) { + // 第一步:提取外层的 data 字段 + dataResult := gjson.GetBytes(resp, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("外层 data 字段不存在") + } + + // 第二步:解析外层 data 的 JSON 字符串 + var outerDataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.String()), &outerDataMap); err != nil { + return nil, fmt.Errorf("解析外层 data 字段失败: %v", err) + } + + // 第三步:提取内层的 data 字段 + innerData, ok := outerDataMap["data"].(string) + if !ok { + return nil, fmt.Errorf("内层 data 字段不存在或类型错误") + } + + // 第四步:解析内层 data 的 JSON 字符串 + var finalDataMap map[string]interface{} + if err := json.Unmarshal([]byte(innerData), &finalDataMap); err != nil { + return nil, fmt.Errorf("解析内层 data 字段失败: %v", err) + } + + // 将最终的 JSON 对象编码为字节数组返回 + finalDataBytes, err := json.Marshal(finalDataMap) + if err != nil { + return nil, fmt.Errorf("编码最终的 JSON 对象失败: %v", err) + } + + return finalDataBytes, nil +} +func main() { + a := map[string]interface{}{ + "data": "{\"msg\":\"接口调用成功\",\"data\":\"{\\\"preservation\\\":{},\\\"crc\\\":41615638,\\\"cases_tree\\\":{},\\\"administrative\\\":{},\\\"civil\\\":{},\\\"count\\\":{\\\"money_jie_total\\\":0,\\\"count_total\\\":5,\\\"larq_stat\\\":\\\"2013(1),2016(4)\\\",\\\"area_stat\\\":\\\"广西壮族自治区(5)\\\",\\\"money_jie_beigao\\\":0,\\\"count_jie_total\\\":5,\\\"money_jie_other\\\":0,\\\"count_wei_total\\\":0,\\\"count_jie_beigao\\\":4,\\\"money_yuangao\\\":0,\\\"money_beigao\\\":0,\\\"ay_stat\\\":\\\"未知(5)\\\",\\\"count_wei_other\\\":0,\\\"count_wei_beigao\\\":0,\\\"count_wei_yuangao\\\":0,\\\"money_other\\\":0,\\\"count_yuangao\\\":1,\\\"money_wei_yuangao\\\":0,\\\"money_jie_yuangao\\\":0,\\\"money_wei_beigao\\\":0,\\\"count_jie_yuangao\\\":1,\\\"count_other\\\":0,\\\"count_jie_other\\\":0,\\\"count_beigao\\\":4,\\\"money_wei_total\\\":0,\\\"money_wei_other\\\":0,\\\"money_total\\\":0,\\\"jafs_stat\\\":\\\"执行完毕(5)\\\"},\\\"implement\\\":{\\\"cases\\\":[{\\\"d_jarq\\\":\\\"2013-04-18\\\",\\\"n_jaay\\\":\\\"未知\\\",\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_ah\\\":\\\"(2013)城中执字第66号\\\",\\\"c_ssdy\\\":\\\"广西壮族自治区\\\",\\\"n_jafs\\\":\\\"执行完毕\\\",\\\"n_ajbs\\\":\\\"a77a247bf43b60846393460d5d0c5a16\\\",\\\"n_jbfy_cj\\\":\\\"基层法院\\\",\\\"c_id\\\":\\\"686a6371ca4af42d2a3a8207e7bb57be\\\",\\\"n_crc\\\":514109936,\\\"n_ajlx\\\":\\\"首次执行\\\",\\\"n_ajjzjd\\\":\\\"已结案\\\",\\\"c_dsrxx\\\":[{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"伍艳红\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"庞汉章\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"曹智\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"李健\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"李强\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"梁辉\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"中国农业银行股份有限公司柳州城中支行\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"}],\\\"n_laay\\\":\\\"未知\\\",\\\"n_jbfy\\\":\\\"柳州市城中区人民法院\\\",\\\"d_larq\\\":\\\"2013-02-27\\\"},{\\\"d_jarq\\\":\\\"2016-12-20\\\",\\\"n_jaay\\\":\\\"民事\\\",\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_gkws_glah\\\":\\\"(2016)桂0107民初1800号\\\",\\\"c_ah\\\":\\\"(2016)桂0107执1708号\\\",\\\"c_gkws_pjjg\\\":\\\"查封被执行人广西鼎铭房地产开发有限公司名下所有的位于南宁市西乡塘区秀灵路7号鼎盛国际B座单元1028号商铺,期限为三年。\\\",\\\"c_ssdy\\\":\\\"广西壮族自治区\\\",\\\"c_gkws_dsr\\\":\\\"申请执行人张海婷,女,1977年6月7日出生,汉族,住所地位于江苏省泰兴市。被执行人广西鼎铭房地产开发有限公司,住所地位于南宁市西乡塘区科园大道31号高新苑29栋06号。法定代表人李强,董事长。\\\",\\\"n_jafs\\\":\\\"执行完毕\\\",\\\"n_ajbs\\\":\\\"5ac3742e7b01d827596138397a183bc8\\\",\\\"n_jbfy_cj\\\":\\\"基层法院\\\",\\\"c_id\\\":\\\"565dc891866688bf1cad480d1abf4c00\\\",\\\"n_sqzxbdje\\\":1947919,\\\"n_crc\\\":606433511,\\\"n_ajlx\\\":\\\"首次执行\\\",\\\"n_ajjzjd\\\":\\\"已结案\\\",\\\"c_dsrxx\\\":[{\\\"n_ssdw\\\":\\\"其他\\\",\\\"c_mc\\\":\\\"张海婷\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"张海婷\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"李强\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"}],\\\"n_laay\\\":\\\"民事\\\",\\\"n_jbfy\\\":\\\"南宁市西乡塘区人民法院\\\",\\\"d_larq\\\":\\\"2016-10-25\\\",\\\"c_gkws_id\\\":\\\"e1c1ca9a-e48a-4fb2-ba51-228965d31e39\\\"},{\\\"d_jarq\\\":\\\"2016-12-20\\\",\\\"n_jaay\\\":\\\"民事\\\",\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_ah\\\":\\\"(2016)桂0107执1710号\\\",\\\"c_gkws_pjjg\\\":\\\"终结本次执行程序。终结本次执行程序后,如申请执行人发现被执行人有财产可供执行或原不具备执行条件的财产现已具备执行条件的,可就尚未实现的债权向本院申请恢复执行。申请执行人提出恢复执行申请不受申请执行期间的限制。对尚在查封、冻结期间的被执行人财产,查封、冻结期间届满后,申请执行人需要续行查封、冻结的,应当在查封、冻结期间届满前十五日内,向本院提出续行查封、冻结的书面申请,逾期不提出申请的,由申请执行人承担相应财产损失的风险或申请法律责任。本裁定送达后即发生法律效力。\\\",\\\"c_ssdy\\\":\\\"广西壮族自治区\\\",\\\"c_gkws_dsr\\\":\\\"申请执行人张海婷,女,1977年6月7日出生,住所地江苏省泰兴市。被执行人广西鼎铭房地产开发有限公司,住所地南宁市西乡塘区科园大道31号高新苑29栋06号。法定代表人李强。\\\",\\\"n_jafs\\\":\\\"执行完毕\\\",\\\"n_ajbs\\\":\\\"021d2380c9950416f7bd983f05219cc6\\\",\\\"n_jbfy_cj\\\":\\\"基层法院\\\",\\\"c_id\\\":\\\"eaccfca0a102cb365322b76ea6303e03\\\",\\\"n_sqzxbdje\\\":1130985.9,\\\"n_crc\\\":2061482197,\\\"n_ajlx\\\":\\\"首次执行\\\",\\\"n_ajjzjd\\\":\\\"已结案\\\",\\\"c_dsrxx\\\":[{\\\"n_ssdw\\\":\\\"其他\\\",\\\"c_mc\\\":\\\"张海婷\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"张海婷\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"李强\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"}],\\\"n_laay\\\":\\\"民事\\\",\\\"n_jbfy\\\":\\\"南宁市西乡塘区人民法院\\\",\\\"d_larq\\\":\\\"2016-10-25\\\",\\\"c_gkws_id\\\":\\\"86cb5b7f-6b46-4bce-9ee1-a93a0039aa4f\\\"},{\\\"d_jarq\\\":\\\"2016-12-21\\\",\\\"n_jaay\\\":\\\"民事\\\",\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_ah\\\":\\\"(2016)桂0107执1869号\\\",\\\"c_gkws_pjjg\\\":\\\"终结本次执行程序。终结本次执行程序后,如申请执行人发现被执行人有财产可供执行或原不具备执行条件的财产现已具备执行条件的,可就尚未实现的债权向本院申请恢复执行。申请执行人提出恢复执行申请不受申请执行期间的限制。对尚在查封、冻结期间的被执行人财产,查封、冻结期间届满后,申请执行人需要续行查封、冻结的,应当在查封、冻结期间届满前十五日内,向本院提出续行查封、冻结的书面申请,逾期不提出申请的,由申请执行人承担相应财产损失的风险或申请法律责任。本裁定送达后即发生法律效力。\\\",\\\"c_ssdy\\\":\\\"广西壮族自治区\\\",\\\"c_gkws_dsr\\\":\\\"申请执行人中国农业银行股份有限公司南宁友爱支行,地址南宁市友爱南路41号商住楼一层。法定代表人刘杰。被执行人梁玉芬,女,1973年02月10日出生,地址南宁市。被执行人吴华,男,1971年04月02日出生,地址南宁市。被执行人广西鼎铭房地产开发有限公司,地址南宁市西乡塘区科园大道31号高新苑29栋06号。法定代表人李强。\\\",\\\"n_jafs\\\":\\\"执行完毕\\\",\\\"n_ajbs\\\":\\\"6634a3e3f074e5456947271023459150\\\",\\\"n_jbfy_cj\\\":\\\"基层法院\\\",\\\"c_id\\\":\\\"e691fe40a1ac30b8484bd4fa19c0af85\\\",\\\"n_sqzxbdje\\\":175923.69,\\\"n_crc\\\":2194376894,\\\"n_ajlx\\\":\\\"首次执行\\\",\\\"n_ajjzjd\\\":\\\"已结案\\\",\\\"c_dsrxx\\\":[{\\\"n_ssdw\\\":\\\"其他\\\",\\\"c_mc\\\":\\\"中国农业银行股份有限公司南宁友爱支行\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"中国农业银行股份有限公司南宁友爱支行\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"吴华\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"李强\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"梁玉芬\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"}],\\\"n_laay\\\":\\\"民事\\\",\\\"n_jbfy\\\":\\\"南宁市西乡塘区人民法院\\\",\\\"d_larq\\\":\\\"2016-11-14\\\",\\\"c_gkws_id\\\":\\\"2fe3d400-471b-456b-a8a5-a939003ff785\\\"},{\\\"d_jarq\\\":\\\"2016-12-21\\\",\\\"n_jaay\\\":\\\"民事\\\",\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_ah\\\":\\\"(2016)桂0107执1871号\\\",\\\"c_gkws_pjjg\\\":\\\"终结本次执行程序。终结本次执行程序后,如申请执行人发现被执行人有财产可供执行或原不具备执行条件的财产现已具备执行条件的,可就尚未实现的债权向本院申请恢复执行。申请执行人提出恢复执行申请不受申请执行期间的限制。对尚在查封、冻结期间的被执行人财产,查封、冻结期间届满后,申请执行人需要续行查封、冻结的,应当在查封、冻结期间届满前十五日内,向本院提出续行查封、冻结的书面申请,逾期不提出申请的,由申请执行人承担相应财产损失的风险或申请法律责任。本裁定送达后即发生法律效力。\\\",\\\"c_ssdy\\\":\\\"广西壮族自治区\\\",\\\"c_gkws_dsr\\\":\\\"申请执行人中国农业银行股份有限公司南宁友爱支行,地址南宁市友爱南路41号商住楼一层。法定代表人刘杰。被执行人唐觉荣,男,1982年11月21日出生,地址南宁市。被执行人广西鼎铭房地产开发有限公司,地址南宁市西乡塘区科园大道31号高新苑29栋06号。法定代表人李强。\\\",\\\"n_jafs\\\":\\\"执行完毕\\\",\\\"n_ajbs\\\":\\\"07c9ef9256bb9cb96031d83ce207122c\\\",\\\"n_jbfy_cj\\\":\\\"基层法院\\\",\\\"c_id\\\":\\\"3061de19534438ebecf50ed03e6b536f\\\",\\\"n_sqzxbdje\\\":138564.6,\\\"n_crc\\\":594016140,\\\"n_ajlx\\\":\\\"首次执行\\\",\\\"n_ajjzjd\\\":\\\"已结案\\\",\\\"c_dsrxx\\\":[{\\\"n_ssdw\\\":\\\"其他\\\",\\\"c_mc\\\":\\\"中国农业银行股份有限公司南宁友爱支行\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"申请执行人\\\",\\\"c_mc\\\":\\\"中国农业银行股份有限公司南宁友爱支行\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"唐觉荣\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"李强\\\",\\\"n_dsrlx\\\":\\\"自然人\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"},{\\\"n_ssdw\\\":\\\"被执行人\\\",\\\"c_mc\\\":\\\"广西鼎铭房地产开发有限公司\\\",\\\"n_dsrlx\\\":\\\"企业组织\\\"}],\\\"n_laay\\\":\\\"民事\\\",\\\"n_jbfy\\\":\\\"南宁市西乡塘区人民法院\\\",\\\"d_larq\\\":\\\"2016-11-14\\\",\\\"c_gkws_id\\\":\\\"3e497ad6-41e7-4a3d-9745-a93900418792\\\"}],\\\"count\\\":{\\\"money_jie_total\\\":0,\\\"count_total\\\":5,\\\"larq_stat\\\":\\\"2013(1),2016(4)\\\",\\\"area_stat\\\":\\\"广西壮族自治区(5)\\\",\\\"money_jie_beigao\\\":0,\\\"count_jie_total\\\":5,\\\"money_jie_other\\\":0,\\\"count_wei_total\\\":0,\\\"count_jie_beigao\\\":4,\\\"money_yuangao\\\":0,\\\"money_beigao\\\":0,\\\"ay_stat\\\":\\\"未知(5)\\\",\\\"count_wei_other\\\":0,\\\"count_wei_beigao\\\":0,\\\"count_wei_yuangao\\\":0,\\\"money_other\\\":0,\\\"count_yuangao\\\":1,\\\"money_wei_yuangao\\\":0,\\\"money_jie_yuangao\\\":0,\\\"money_wei_beigao\\\":0,\\\"count_jie_yuangao\\\":1,\\\"count_other\\\":0,\\\"count_jie_other\\\":0,\\\"count_beigao\\\":4,\\\"money_wei_total\\\":0,\\\"money_wei_other\\\":0,\\\"money_total\\\":0,\\\"jafs_stat\\\":\\\"执行完毕(5)\\\"}},\\\"criminal\\\":{},\\\"bankrupt\\\":{}}\"}", + "message": "调用成功", + "resultCode": "200", + "success": true, + } + marshal, err := json.Marshal(a) + if err != nil { + fmt.Println(err) + return + } + resp, err := processG35SC01Response(marshal) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(resp)) + +} +func (w *WestDexService) ProcessRequests(data interface{}, requests []types.WestDexServiceRequestParams) ([]byte, error) { + var wg sync.WaitGroup + responseData := make([]APIResponseData, len(requests)) // 使用 APIResponseData 结构存储响应数据 + var errorsCh []error + mutex := sync.Mutex{} + encryptedFields, err := w.EncryptStructFields(data) + if err != nil { + return nil, fmt.Errorf("西部请求, 生成请求数据失败: %+v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) // 创建 context + defer cancel() // 确保函数结束时取消 context + + errorLimit := 3 + errorCount := 0 // 记录错误次数 + + // 并发处理每个请求 + for i, req := range requests { + wg.Add(1) + go func(i int, req types.WestDexServiceRequestParams) { + defer wg.Done() + + select { + case <-ctx.Done(): // 检查 context 是否已取消 + return + default: + } + + // 将加密后的数据映射成 API 请求格式 + apiRequest := w.MapStructToAPIRequest(encryptedFields, req.FieldMapping, "data") + resp, callApiErr := w.CallAPI(req.ApiID, apiRequest) + timestamp := time.Now().Format(time.RFC3339) + var processedResp []byte + if resp != nil { + processedResp = processResponse(resp, req.ApiID) + } + if callApiErr != nil { + mutex.Lock() + defer mutex.Unlock() + + errorsCh = append(errorsCh, fmt.Errorf("西部请求, 请求失败: %+v", callApiErr)) + errorCount++ + + // 如果错误次数超过限制,取消 context + if errorCount >= errorLimit { + cancel() + } + + // 存储失败的响应 + responseData[i] = APIResponseData{ + ApiID: req.ApiID, + Error: callApiErr.Error(), + Data: processedResp, + Success: false, + Timestamp: timestamp, + } + return + } + + // 存储成功的响应数据,使用 RawMessage 来存储原始数据 + mutex.Lock() + responseData[i] = APIResponseData{ + ApiID: req.ApiID, + Data: processedResp, // 保持原始数据 + Success: true, + Timestamp: timestamp, + } + mutex.Unlock() + }(i, req) + } + + wg.Wait() // 等待所有 goroutine 完成 + + if errorCount >= errorLimit { + return nil, fmt.Errorf("请求失败次数超过 %d 次: %+v", errorLimit, errorsCh) + } + + // 将 responseData 转换为 JSON + combinedResponse, err := json.Marshal(responseData) + if err != nil { + return nil, fmt.Errorf("响应数据转 JSON 失败: %+v", err) + } + + return combinedResponse, nil +}