diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 87585cb..63d04c2 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -123,6 +123,14 @@ type ( GetInviteLinkResp { InviteLink string `json:"invite_link"` // 邀请链接 } + // 生成邀请海报请求 + GenerateInvitePosterReq { + InviteLink string `form:"invite_link"` // 邀请链接(用于生成二维码) + } + // 生成邀请海报响应 + GenerateInvitePosterResp { + PosterUrl string `json:"poster_url"` // 海报图片URL(base64编码的data URL) + } // 获取代理等级特权信息 GetLevelPrivilegeResp { Levels []LevelPrivilegeItem `json:"levels"` @@ -250,6 +258,10 @@ service main { @handler GetInviteLink get /invite_link (GetInviteLinkReq) returns (GetInviteLinkResp) + // 生成邀请海报(带二维码的图片) + @handler GenerateInvitePoster + get /invite/poster (GenerateInvitePosterReq) returns (GenerateInvitePosterResp) + // 获取代理等级特权信息 @handler GetLevelPrivilege get /level/privilege returns (GetLevelPrivilegeResp) diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index afc4f93..36b688d 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -47,6 +47,7 @@ type ( Id string `json:"id"` PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式) PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"` + Code string `json:"code,optional"` // 微信小程序/H5授权码,用于自动绑定微信账号(当用户未绑定微信时) } PaymentResp { PrepayData interface{} `json:"prepay_data"` diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index 51f0505..46bdd52 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -67,10 +67,8 @@ WechatH5: AppID: "wx442ee1ac1ee75917" AppSecret: "c80474909db42f63913b7a307b3bee17" WechatMini: - AppID: "wx781abb66b3368963" # 小程序的AppID - AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret - TycAppID: "wxe74617f3dd56c196" - TycAppSecret: "c8207e54aef5689b2a7c1f91ed7ae8a0" + AppID: "wx5bacc94add2da981" # 小程序的AppID + AppSecret: "48a2c1e8ff1b7d4c0ff82fbefa64d2d0" # 小程序的AppSecret Query: ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 AdminConfig: diff --git a/app/main/api/internal/handler/agent/generateinviteposterhandler.go b/app/main/api/internal/handler/agent/generateinviteposterhandler.go new file mode 100644 index 0000000..65d8d80 --- /dev/null +++ b/app/main/api/internal/handler/agent/generateinviteposterhandler.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package agent + +import ( + "net/http" + + "qnc-server/app/main/api/internal/logic/agent" + "qnc-server/app/main/api/internal/svc" + "qnc-server/app/main/api/internal/types" + "qnc-server/common/result" + "qnc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GenerateInvitePosterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GenerateInvitePosterReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGenerateInvitePosterLogic(r.Context(), svcCtx) + resp, err := l.GenerateInvitePoster(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 59f6328..9885b3e 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -646,6 +646,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/info", Handler: agent.GetAgentInfoHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/invite/poster", + Handler: agent.GenerateInvitePosterHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/invite_code/delete", diff --git a/app/main/api/internal/logic/agent/generateinviteposterlogic.go b/app/main/api/internal/logic/agent/generateinviteposterlogic.go new file mode 100644 index 0000000..e7598df --- /dev/null +++ b/app/main/api/internal/logic/agent/generateinviteposterlogic.go @@ -0,0 +1,52 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package agent + +import ( + "context" + "encoding/base64" + "fmt" + + "qnc-server/app/main/api/internal/svc" + "qnc-server/app/main/api/internal/types" + "qnc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GenerateInvitePosterLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGenerateInvitePosterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateInvitePosterLogic { + return &GenerateInvitePosterLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GenerateInvitePosterLogic) GenerateInvitePoster(req *types.GenerateInvitePosterReq) (resp *types.GenerateInvitePosterResp, err error) { + if req.InviteLink == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("邀请链接不能为空"), "") + } + + // 调用ImageService生成海报 + imageData, mimeType, err := l.svcCtx.ImageService.ProcessImageWithQRCode("invitation", req.InviteLink) + if err != nil { + l.Errorf("生成邀请海报失败: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成海报失败: %v", err) + } + + // 将图片数据转换为base64编码的data URL + base64Data := base64.StdEncoding.EncodeToString(imageData) + posterUrl := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data) + + return &types.GenerateInvitePosterResp{ + PosterUrl: posterUrl, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getinvitelinklogic.go b/app/main/api/internal/logic/agent/getinvitelinklogic.go index 645bc60..ed956bd 100644 --- a/app/main/api/internal/logic/agent/getinvitelinklogic.go +++ b/app/main/api/internal/logic/agent/getinvitelinklogic.go @@ -4,15 +4,15 @@ import ( "context" "database/sql" "fmt" - "strconv" - "strings" - "time" "qnc-server/app/main/model" "qnc-server/common/ctxdata" "qnc-server/common/globalkey" "qnc-server/common/tool" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/crypto" + "strconv" + "strings" + "time" "github.com/pkg/errors" @@ -124,7 +124,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo // 先查询是否已存在短链 existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}, 2, globalkey.DelStateNo) if err == nil && existingShortLink != nil { - // 已存在短链,直接返回 + // 已存在短链,检查 target_path 是否需要更新 + if existingShortLink.TargetPath != targetPath { + // target_path 已变化,更新短链记录 + oldTargetPath := existingShortLink.TargetPath + existingShortLink.TargetPath = targetPath + if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingShortLink); updateErr != nil { + l.Errorf("更新短链 target_path 失败: %v", updateErr) + // 即使更新失败,也返回旧短链,避免影响用户体验 + } else { + l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingShortLink.ShortCode, oldTargetPath, targetPath) + } + } return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil } @@ -136,6 +147,18 @@ func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCo if inviteCodeId == "" && inviteCode != "" { existingByCode, err2 := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeTypeDelState(l.ctx, sql.NullString{String: inviteCode, Valid: true}, 2, globalkey.DelStateNo) if err2 == nil && existingByCode != nil { + // 已存在短链,检查 target_path 是否需要更新 + if existingByCode.TargetPath != targetPath { + // target_path 已变化,更新短链记录 + oldTargetPath := existingByCode.TargetPath + existingByCode.TargetPath = targetPath + if updateErr := l.svcCtx.AgentShortLinkModel.UpdateWithVersion(l.ctx, nil, existingByCode); updateErr != nil { + l.Errorf("更新短链 target_path 失败: %v", updateErr) + // 即使更新失败,也返回旧短链,避免影响用户体验 + } else { + l.Infof("短链 target_path 已更新: shortCode=%s, old=%s, new=%s", existingByCode.ShortCode, oldTargetPath, targetPath) + } + } return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingByCode.ShortCode), nil } if err2 != nil && !errors.Is(err2, model.ErrNotFound) { diff --git a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go index 2ffe859..976c7f4 100644 --- a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go +++ b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go @@ -5,13 +5,13 @@ import ( "database/sql" "fmt" "os" - "strconv" - "time" "qnc-server/app/main/model" "qnc-server/common/ctxdata" "qnc-server/common/globalkey" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/crypto" + "strconv" + "time" "github.com/google/uuid" "github.com/pkg/errors" diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index 070d15a..abea7c4 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -6,15 +6,15 @@ import ( "encoding/json" "fmt" "os" - "strconv" - "strings" - "time" "qnc-server/app/main/api/internal/svc" "qnc-server/app/main/api/internal/types" "qnc-server/app/main/model" "qnc-server/common/ctxdata" "qnc-server/common/xerr" "qnc-server/pkg/lzkit/lzUtils" + "strconv" + "strings" + "time" "github.com/google/uuid" "github.com/pkg/errors" @@ -35,6 +35,9 @@ type PaymentTypeResp struct { orderID string // 订单ID,用于开发环境测试支付模式 } +// enableDevTestPayment 测试支付功能开关,设置为 true 以启用测试支付功能 +const enableDevTestPayment = false + func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic { return &PaymentLogic{ Logger: logx.WithContext(ctx), @@ -50,8 +53,8 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, // 检查是否为开发环境的测试支付模式 env := os.Getenv("ENV") - isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty") - isEmptyReportMode := env == "development" && req.PayMethod == "test_empty" + isDevTestPayment := enableDevTestPayment && env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty") + isEmptyReportMode := enableDevTestPayment && env == "development" && req.PayMethod == "test_empty" l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { switch req.PayType { @@ -95,7 +98,7 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, // 正常支付流程 var createOrderErr error if req.PayMethod == "wechat" { - prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) + prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, req.Code) } else if req.PayMethod == "alipay" { prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) } else if req.PayMethod == "appleiap" { diff --git a/app/main/api/internal/logic/user/wxminiauthlogic.go b/app/main/api/internal/logic/user/wxminiauthlogic.go index 96cc74b..65a6492 100644 --- a/app/main/api/internal/logic/user/wxminiauthlogic.go +++ b/app/main/api/internal/logic/user/wxminiauthlogic.go @@ -118,7 +118,37 @@ func (l *WxMiniAuthLogic) GetSessionKey(code string) (*SessionKeyResp, error) { // 检查微信返回的错误码 if sessionKeyResp.ErrCode != 0 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + // 针对不同的微信错误码返回更明确的错误信息 + var errMsg string + switch sessionKeyResp.ErrCode { + case 40029: + // code 无效(已使用、已过期或格式错误) + errMsg = "微信授权码无效或已过期,请重新打开小程序" + l.Errorf("微信code无效: errcode=%d, errmsg=%s, code前6位=%s", + sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg, + func() string { + if len(code) > 6 { + return code[:6] + "..." + } + return code + }()) + case 40013: + // 无效的 AppID + errMsg = "小程序配置错误,请联系管理员" + l.Errorf("微信AppID无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) + case 40125: + // 无效的 AppSecret + errMsg = "小程序配置错误,请联系管理员" + l.Errorf("微信AppSecret无效: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) + case 45011: + // 频率限制 + errMsg = "请求过于频繁,请稍后再试" + l.Errorf("微信API频率限制: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) + default: + errMsg = fmt.Sprintf("微信授权失败: %s", sessionKeyResp.ErrMsg) + l.Errorf("微信接口返回错误: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) + } + return nil, errors.Wrapf(xerr.NewErrMsg(errMsg), "微信接口返回错误: errcode=%d, errmsg=%s", sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) } diff --git a/app/main/api/internal/service/alipayService.go b/app/main/api/internal/service/alipayService.go index 92a2e32..531902a 100644 --- a/app/main/api/internal/service/alipayService.go +++ b/app/main/api/internal/service/alipayService.go @@ -6,12 +6,12 @@ import ( "encoding/hex" "fmt" "net/http" - "strconv" - "sync/atomic" - "time" "qnc-server/app/main/api/internal/config" "qnc-server/app/main/model" "qnc-server/pkg/lzkit/lzUtils" + "strconv" + "sync/atomic" + "time" "github.com/smartwalle/alipay/v3" ) diff --git a/app/main/api/internal/service/wechatpayService.go b/app/main/api/internal/service/wechatpayService.go index fbf7f3a..f8b4168 100644 --- a/app/main/api/internal/service/wechatpayService.go +++ b/app/main/api/internal/service/wechatpayService.go @@ -2,15 +2,19 @@ package service import ( "context" + "encoding/json" + "errors" "fmt" + "io" "net/http" - "strconv" - "time" "qnc-server/app/main/api/internal/config" "qnc-server/app/main/model" "qnc-server/common/ctxdata" "qnc-server/pkg/lzkit/lzUtils" + "strconv" + "time" + "github.com/google/uuid" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" @@ -251,9 +255,16 @@ func (w *WechatPayService) CreateWechatH5Order(ctx context.Context, amount float } // CreateWechatOrder 创建微信支付订单(集成 APP、H5、小程序) -func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) { - // 根据 ctx 中的 platform 判断平台 - platform := ctx.Value("platform").(string) +func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string, code string) (interface{}, error) { + // 根据 ctx 中的 platform 判断平台(请求头 X-Platform: wxmini / wxh5 / app) + platformValue := ctx.Value("platform") + if platformValue == nil { + return "", fmt.Errorf("平台信息不存在,请检查请求头 X-Platform") + } + platform, ok := platformValue.(string) + if !ok { + return "", fmt.Errorf("平台信息格式错误") + } var prepayData interface{} var err error @@ -266,7 +277,30 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64 } userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID) if findAuthModelErr != nil { - return "", findAuthModelErr + if errors.Is(findAuthModelErr, model.ErrNotFound) { + // 用户未绑定微信,尝试通过 code 自动绑定 + if code == "" { + return "", fmt.Errorf("用户未绑定微信小程序账号,请先完成微信登录或提供授权码") + } + // 通过 code 获取 OpenID + openid, getOpenidErr := w.getWechatMiniOpenID(ctx, code) + if getOpenidErr != nil { + return "", fmt.Errorf("获取微信小程序OpenID失败: %v", getOpenidErr) + } + // 自动绑定微信账号 + bindErr := w.bindWechatAuth(ctx, userID, model.UserAuthTypeWxMiniOpenID, openid) + if bindErr != nil { + return "", fmt.Errorf("绑定微信小程序账号失败: %v", bindErr) + } + // 重新查询绑定后的认证信息 + userAuthModel, findAuthModelErr = w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID) + if findAuthModelErr != nil { + return "", fmt.Errorf("查询绑定后的微信认证信息失败: %v", findAuthModelErr) + } + logx.Infof("用户 %s 已自动绑定微信小程序账号,OpenID: %s", userID, openid) + } else { + return "", fmt.Errorf("查询用户微信认证信息失败: %v", findAuthModelErr) + } } prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) if err != nil { @@ -279,7 +313,30 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64 } userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID) if findAuthModelErr != nil { - return "", findAuthModelErr + if errors.Is(findAuthModelErr, model.ErrNotFound) { + // 用户未绑定微信,尝试通过 code 自动绑定 + if code == "" { + return "", fmt.Errorf("用户未绑定微信H5账号,请先完成微信登录或提供授权码") + } + // 通过 code 获取 OpenID + openid, getOpenidErr := w.getWechatH5OpenID(ctx, code) + if getOpenidErr != nil { + return "", fmt.Errorf("获取微信H5 OpenID失败: %v", getOpenidErr) + } + // 自动绑定微信账号 + bindErr := w.bindWechatAuth(ctx, userID, model.UserAuthTypeWxh5OpenID, openid) + if bindErr != nil { + return "", fmt.Errorf("绑定微信H5账号失败: %v", bindErr) + } + // 重新查询绑定后的认证信息 + userAuthModel, findAuthModelErr = w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID) + if findAuthModelErr != nil { + return "", fmt.Errorf("查询绑定后的微信认证信息失败: %v", findAuthModelErr) + } + logx.Infof("用户 %s 已自动绑定微信H5账号,OpenID: %s", userID, openid) + } else { + return "", fmt.Errorf("查询用户微信认证信息失败: %v", findAuthModelErr) + } } prepayData, err = w.CreateWechatH5Order(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) if err != nil { @@ -384,3 +441,112 @@ func (w *WechatPayService) GenerateOutTradeNo() string { return combined } + +// getWechatMiniOpenID 通过 code 获取微信小程序 OpenID +func (w *WechatPayService) getWechatMiniOpenID(ctx context.Context, code string) (string, error) { + appID := w.config.WechatMini.AppID + appSecret := w.config.WechatMini.AppSecret + url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appID, appSecret, code) + + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("请求微信API失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + var data struct { + Openid string `json:"openid"` + ErrCode int `json:"errcode,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` + } + + if err := json.Unmarshal(body, &data); err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + + if data.ErrCode != 0 { + return "", fmt.Errorf("微信API返回错误: errcode=%d, errmsg=%s", data.ErrCode, data.ErrMsg) + } + + if data.Openid == "" { + return "", fmt.Errorf("openid为空") + } + + return data.Openid, nil +} + +// getWechatH5OpenID 通过 code 获取微信H5 OpenID +func (w *WechatPayService) getWechatH5OpenID(ctx context.Context, code string) (string, error) { + appID := w.config.WechatH5.AppID + appSecret := w.config.WechatH5.AppSecret + url := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appID, appSecret, code) + + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("请求微信API失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + var data struct { + Openid string `json:"openid"` + ErrCode int `json:"errcode,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` + } + + if err := json.Unmarshal(body, &data); err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + + if data.ErrCode != 0 { + return "", fmt.Errorf("微信API返回错误: errcode=%d, errmsg=%s", data.ErrCode, data.ErrMsg) + } + + if data.Openid == "" { + return "", fmt.Errorf("openid为空") + } + + return data.Openid, nil +} + +// bindWechatAuth 绑定微信认证信息到用户账号 +func (w *WechatPayService) bindWechatAuth(ctx context.Context, userID string, authType string, openid string) error { + // 检查该 OpenID 是否已被其他用户绑定 + existingAuth, err := w.userAuthModel.FindOneByAuthTypeAuthKey(ctx, authType, openid) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return fmt.Errorf("查询认证信息失败: %v", err) + } + + if existingAuth != nil { + // 如果 OpenID 已被其他用户绑定,检查是否是当前用户 + if existingAuth.UserId != userID { + return fmt.Errorf("该微信账号已被其他用户绑定") + } + // 如果已经是当前用户绑定的,直接返回成功 + return nil + } + + // 创建新的认证记录 + userAuth := &model.UserAuth{ + Id: uuid.NewString(), + UserId: userID, + AuthType: authType, + AuthKey: openid, + } + + _, err = w.userAuthModel.Insert(ctx, nil, userAuth) + if err != nil { + return fmt.Errorf("创建认证记录失败: %v", err) + } + + return nil +} diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 607edfa..33507df 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1294,6 +1294,14 @@ type GenerateInviteCodeResp struct { Codes []string `json:"codes"` // 生成的邀请码列表 } +type GenerateInvitePosterReq struct { + InviteLink string `form:"invite_link"` // 邀请链接(用于生成二维码) +} + +type GenerateInvitePosterResp struct { + PosterUrl string `json:"poster_url"` // 海报图片URL(base64编码的data URL) +} + type GetAuthorizationDocumentByOrderReq struct { OrderId string `json:"orderId" validate:"required"` // 订单ID } @@ -1782,6 +1790,7 @@ type PaymentReq struct { Id string `json:"id"` PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式) PayType string `json:"pay_type" validate:"required,oneof=query agent_vip agent_upgrade"` + Code string `json:"code,optional"` // 微信小程序/H5授权码,用于自动绑定微信账号(当用户未绑定微信时) } type PaymentResp struct { diff --git a/pkg/lzkit/validator/validator.go b/pkg/lzkit/validator/validator.go index 594696f..3ab7424 100644 --- a/pkg/lzkit/validator/validator.go +++ b/pkg/lzkit/validator/validator.go @@ -104,7 +104,6 @@ func validDate(fl validator.FieldLevel) bool { return matched } - // 自定义身份证校验(增强版) // 校验规则: // 1. 格式:18位,前6位地区码(首位不为0),7-14位出生日期,15-17位顺序码,18位校验码 @@ -216,8 +215,12 @@ func validatePayMethod(fl validator.FieldLevel) bool { } validTypes := map[string]bool{ - "alipay": true, // 中国电信 - "wechatpay": true, // 中国移动 + "alipay": true, + "wechat": true, + "wechatpay": true, + "appleiap": true, + "test": true, + "test_empty": true, } return validTypes[payMethod]