三端用户手机号联通,增加临时用户

This commit is contained in:
2025-06-17 23:46:37 +08:00
parent b98ad2af2c
commit f1f0c5dbd2
58 changed files with 1540 additions and 1252 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"qnc-server/app/main/api/internal/config"
"qnc-server/app/main/model"
"qnc-server/pkg/lzkit/lzUtils"
"strconv"
"sync/atomic"
@@ -101,13 +102,13 @@ func (a *AliPayService) CreateAlipayOrder(ctx context.Context, amount float64, s
// 根据 ctx 中的 platform 判断平台
platform, platformOk := ctx.Value("platform").(string)
if !platformOk {
return "", fmt.Errorf("无的支付平台: %s", platform)
return "", fmt.Errorf("支付平台不存在: %s", platform)
}
switch platform {
case "app":
case model.PlatformApp:
// 调用App支付的创建方法
return a.CreateAlipayAppOrder(amount, subject, outTradeNo)
case "h5":
case model.PlatformH5:
// 调用H5支付的创建方法并传入 returnUrl
return a.CreateAlipayH5Order(amount, subject, outTradeNo)
default:

View File

@@ -0,0 +1,173 @@
package service
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"github.com/fogleman/gg"
"github.com/skip2/go-qrcode"
"github.com/zeromicro/go-zero/core/logx"
)
type ImageService struct {
baseImagePath string
}
func NewImageService() *ImageService {
return &ImageService{
baseImagePath: "static/images", // 原图存放目录
}
}
// ProcessImageWithQRCode 处理图片,在中间添加二维码
func (s *ImageService) ProcessImageWithQRCode(qrcodeType, qrcodeUrl string) ([]byte, string, error) {
// 1. 根据qrcodeType确定使用哪张背景图
var backgroundImageName string
switch qrcodeType {
case "promote":
backgroundImageName = "tg_qrcode_1.jpg"
case "invitation":
backgroundImageName = "yq_qrcode_1.png"
default:
backgroundImageName = "tg_qrcode_1.jpg" // 默认使用第一张图片
}
// 2. 读取原图
originalImagePath := filepath.Join(s.baseImagePath, backgroundImageName)
originalImage, err := s.loadImage(originalImagePath)
if err != nil {
logx.Errorf("加载原图失败: %v, 图片路径: %s", err, originalImagePath)
return nil, "", fmt.Errorf("加载原图失败: %v", err)
}
// 3. 获取原图尺寸
bounds := originalImage.Bounds()
imgWidth := bounds.Dx()
imgHeight := bounds.Dy()
// 4. 创建绘图上下文
dc := gg.NewContext(imgWidth, imgHeight)
// 5. 绘制原图作为背景
dc.DrawImageAnchored(originalImage, imgWidth/2, imgHeight/2, 0.5, 0.5)
// 6. 生成二维码(去掉白边)
qrCode, err := qrcode.New(qrcodeUrl, qrcode.Medium)
if err != nil {
logx.Errorf("生成二维码失败: %v, 二维码内容: %s", err, qrcodeUrl)
return nil, "", fmt.Errorf("生成二维码失败: %v", err)
}
// 禁用二维码边框,去掉白边
qrCode.DisableBorder = true
// 7. 根据二维码类型设置不同的尺寸和位置
var qrSize int
var qrX, qrY int
switch qrcodeType {
case "promote":
// promote类型精确设置二维码尺寸
qrSize = 280 // 固定尺寸280px
// 左下角位置:距左边和底边留一些边距
qrX = 192 // 距左边180px
qrY = imgHeight - qrSize - 190 // 距底边100px
case "invitation":
// invitation类型精确设置二维码尺寸
qrSize = 360 // 固定尺寸320px
// 中间偏上位置
qrX = (imgWidth - qrSize) / 2 // 水平居中
qrY = 555 // 垂直位置200px
default:
// 默认promote样式
qrSize = 280 // 固定尺寸280px
qrX = 200 // 距左边180px
qrY = imgHeight - qrSize - 200 // 距底边100px
}
// 8. 生成指定尺寸的二维码图片
qrCodeImage := qrCode.Image(qrSize)
// 9. 直接绘制二维码(不添加背景)
dc.DrawImageAnchored(qrCodeImage, qrX+qrSize/2, qrY+qrSize/2, 0.5, 0.5)
// 11. 输出为字节数组
var buf bytes.Buffer
err = png.Encode(&buf, dc.Image())
if err != nil {
logx.Errorf("编码图片失败: %v", err)
return nil, "", fmt.Errorf("编码图片失败: %v", err)
}
logx.Infof("成功生成带二维码的图片,类型: %s, 二维码内容: %s, 图片尺寸: %dx%d, 二维码尺寸: %dx%d, 位置: (%d,%d)",
qrcodeType, qrcodeUrl, imgWidth, imgHeight, qrSize, qrSize, qrX, qrY)
return buf.Bytes(), "image/png", nil
}
// loadImage 加载图片文件
func (s *ImageService) loadImage(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
// 尝试解码PNG
img, err := png.Decode(file)
if err != nil {
// 如果PNG解码失败重新打开文件尝试JPEG
file.Close()
file, err = os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, err = jpeg.Decode(file)
if err != nil {
// 如果还是失败,使用通用解码器
file.Close()
file, err = os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err = image.Decode(file)
if err != nil {
return nil, err
}
}
}
return img, nil
}
// GetSupportedImageTypes 获取支持的图片类型列表
func (s *ImageService) GetSupportedImageTypes() []string {
return []string{"promote", "invitation"}
}
// CheckImageExists 检查指定类型的背景图是否存在
func (s *ImageService) CheckImageExists(qrcodeType string) bool {
var backgroundImageName string
switch qrcodeType {
case "promote":
backgroundImageName = "tg_qrcode_1.jpg"
case "invitation":
backgroundImageName = "yq_qrcode_1.png"
default:
backgroundImageName = "tg_qrcode_1.jpg"
}
imagePath := filepath.Join(s.baseImagePath, backgroundImageName)
_, err := os.Stat(imagePath)
return err == nil
}

View File

@@ -2,22 +2,34 @@ package service
import (
"context"
"database/sql"
"qnc-server/app/main/api/internal/config"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
jwtx "qnc-server/common/jwt"
"qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type UserService struct {
Config *config.Config
userModel model.UserModel
userAuthModel model.UserAuthModel
userTempModel model.UserTempModel
agentModel model.AgentModel
}
// NewUserService 创建UserService实例
func NewUserService(userModel model.UserModel, userAuthModel model.UserAuthModel) *UserService {
func NewUserService(config *config.Config, userModel model.UserModel, userAuthModel model.UserAuthModel, userTempModel model.UserTempModel, agentModel model.AgentModel) *UserService {
return &UserService{
Config: config,
userModel: userModel,
userAuthModel: userAuthModel,
userTempModel: userTempModel,
agentModel: agentModel,
}
}
@@ -63,3 +75,217 @@ func (s *UserService) RegisterUUIDUser(ctx context.Context) (int64, error) {
return userId, nil
}
// generalUserToken 生成用户token
func (s *UserService) GeneralUserToken(ctx context.Context, userID int64) (string, error) {
platform, err := ctxdata.GetPlatformFromCtx(ctx)
if err != nil {
return "", err
}
var isAgent int64
var agentID int64
var userType int64
user, err := s.userModel.FindOne(ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", err
}
if user != nil {
userID = user.Id
userType = model.UserTypeNormal
agent, err := s.agentModel.FindOneByUserId(ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", err
}
if agent != nil {
agentID = agent.Id
isAgent = model.AgentStatusYes
}
} else {
userTemp, err := s.userTempModel.FindOne(ctx, userID)
if err != nil {
return "", err
}
if userTemp != nil {
userID = userTemp.Id
userType = model.UserTypeTemp
}
}
token, generaErr := jwtx.GenerateJwtToken(jwtx.JwtClaims{
UserId: userID,
AgentId: agentID,
Platform: platform,
UserType: userType,
IsAgent: isAgent,
}, s.Config.JwtAuth.AccessSecret, s.Config.JwtAuth.AccessExpire)
if generaErr != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新token, 生成token失败 : %d", userID)
}
return token, nil
}
// RegisterUser 注册用户返回用户ID
// 传入手机号自动注册如果ctx存在临时用户则临时用户转为正式用户
func (s *UserService) RegisterUser(ctx context.Context, mobile string) (int64, error) {
claims, err := ctxdata.GetClaimsFromCtx(ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return 0, err
}
user, err := s.userModel.FindOneByMobile(ctx, sql.NullString{String: mobile, Valid: true})
if err != nil && !errors.Is(err, model.ErrNotFound) {
return 0, err
}
if user != nil {
return 0, errors.New("用户已注册")
}
// 普通注册
if claims == nil {
var userId int64
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
user := &model.User{
Mobile: sql.NullString{String: mobile, Valid: true},
}
result, err := s.userModel.Insert(ctx, session, user)
if err != nil {
return err
}
userId, err = result.LastInsertId()
if err != nil {
return err
}
s.userAuthModel.Insert(ctx, session, &model.UserAuth{
UserId: userId,
AuthType: model.UserAuthTypeMobile,
AuthKey: mobile,
})
return nil
})
if err != nil {
return 0, err
}
return userId, nil
}
// 双重判断是否已经注册
if claims.UserType == model.UserTypeNormal {
return 0, errors.New("用户已注册")
}
var userId int64
// 临时转正式注册
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
user := &model.User{
Mobile: sql.NullString{String: mobile, Valid: true},
}
result, err := s.userModel.Insert(ctx, session, user)
if err != nil {
return err
}
userId, err = result.LastInsertId()
if err != nil {
return err
}
_, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{
UserId: userId,
AuthType: model.UserAuthTypeMobile,
AuthKey: mobile,
})
if err != nil {
return err
}
err = s.TempUserBindUser(ctx, session, userId)
if err != nil {
return err
}
return nil
})
if err != nil {
return 0, err
}
return userId, nil
}
// TempUserBindUser 临时用户绑定用户
func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID int64) error {
claims, err := ctxdata.GetClaimsFromCtx(ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return err
}
if claims == nil || claims.UserType != model.UserTypeTemp {
return errors.New("无临时用户")
}
userTemp, err := s.userTempModel.FindOne(ctx, claims.UserId)
if err != nil {
return err
}
userAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, userTemp.AuthType, userTemp.AuthKey)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return err
}
if userAuth != nil {
return errors.New("临时用户已注册")
}
if session == nil {
err := s.userAuthModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
_, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{
UserId: normalUserID,
AuthType: userTemp.AuthType,
AuthKey: userTemp.AuthKey,
})
if err != nil {
return err
}
err = s.userTempModel.DeleteSoft(ctx, session, userTemp)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
} else {
_, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{
UserId: normalUserID,
AuthType: userTemp.AuthType,
AuthKey: userTemp.AuthKey,
})
if err != nil {
return err
}
err = s.userTempModel.DeleteSoft(ctx, session, userTemp)
if err != nil {
return err
}
return nil
}
}
// _bak_RegisterUUIDUser 注册UUID用户返回用户ID
func (s *UserService) _bak_RegisterUUIDUser(ctx context.Context) error {
// 生成UUID
uuidStr, err := s.GenerateUUIDUserId(ctx)
if err != nil {
return err
}
err = s.userTempModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 创建用户临时记录
userTemp := &model.UserTemp{
AuthType: model.UserAuthTypeUUID,
AuthKey: uuidStr,
}
_, err := s.userTempModel.Insert(ctx, session, userTemp)
return err
})
if err != nil {
return err
}
return nil
}

View File

@@ -43,7 +43,7 @@ const (
)
type WechatPayService struct {
config config.WxpayConfig
config config.Config
wechatClient *core.Client
notifyHandler *notify.Handler
userAuthModel model.UserAuthModel
@@ -96,7 +96,7 @@ func newWechatPayServiceWithPlatformCert(c config.Config, userAuthModel model.Us
logx.Infof("微信支付客户端初始化成功(平台证书方式)")
return &WechatPayService{
config: c.Wxpay,
config: c,
wechatClient: client,
notifyHandler: notifyHandler,
userAuthModel: userAuthModel,
@@ -147,7 +147,7 @@ func newWechatPayServiceWithWxPayPubKey(c config.Config, userAuthModel model.Use
logx.Infof("微信支付客户端初始化成功(微信支付公钥方式)")
return &WechatPayService{
config: c.Wxpay,
config: c,
wechatClient: client,
notifyHandler: notifyHandler,
userAuthModel: userAuthModel,
@@ -160,11 +160,11 @@ func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount floa
// 构建支付请求参数
payRequest := app.PrepayRequest{
Appid: core.String(w.config.AppID),
Mchid: core.String(w.config.MchID),
Appid: core.String(w.config.Wxpay.AppID),
Mchid: core.String(w.config.Wxpay.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.NotifyUrl),
NotifyUrl: core.String(w.config.Wxpay.NotifyUrl),
Amount: &app.Amount{
Total: core.Int64(totalAmount),
},
@@ -189,11 +189,41 @@ func (w *WechatPayService) CreateWechatMiniProgramOrder(ctx context.Context, amo
// 构建支付请求参数
payRequest := jsapi.PrepayRequest{
Appid: core.String(w.config.AppID),
Mchid: core.String(w.config.MchID),
Appid: core.String(w.config.WechatMini.AppID),
Mchid: core.String(w.config.Wxpay.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.NotifyUrl),
NotifyUrl: core.String(w.config.Wxpay.NotifyUrl),
Amount: &jsapi.Amount{
Total: core.Int64(totalAmount),
},
Payer: &jsapi.Payer{
Openid: core.String(openid), // 用户的 OpenID通过前端传入
}}
// 初始化 AppApiService
svc := jsapi.JsapiApiService{Client: w.wechatClient}
// 发起预支付请求
resp, result, err := svc.PrepayWithRequestPayment(ctx, payRequest)
if err != nil {
return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode)
}
// 返回预支付交易会话标识
return resp, nil
}
// CreateWechatH5Order 创建微信H5支付订单
func (w *WechatPayService) CreateWechatH5Order(ctx context.Context, amount float64, description string, outTradeNo string, openid string) (interface{}, error) {
totalAmount := lzUtils.ToWechatAmount(amount)
// 构建支付请求参数
payRequest := jsapi.PrepayRequest{
Appid: core.String(w.config.WechatH5.AppID),
Mchid: core.String(w.config.Wxpay.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.Wxpay.NotifyUrl),
Amount: &jsapi.Amount{
Total: core.Int64(totalAmount),
},
@@ -222,12 +252,12 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64
var err error
switch platform {
case "mp-weixin":
case model.PlatformWxMini:
userID, getUidErr := ctxdata.GetUidFromCtx(ctx)
if getUidErr != nil {
return "", getUidErr
}
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMini)
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID)
if findAuthModelErr != nil {
return "", findAuthModelErr
}
@@ -235,20 +265,20 @@ func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64
if err != nil {
return "", err
}
case "h5-weixin":
case model.PlatformWxH5:
userID, getUidErr := ctxdata.GetUidFromCtx(ctx)
if getUidErr != nil {
return "", getUidErr
}
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5)
userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID)
if findAuthModelErr != nil {
return "", findAuthModelErr
}
prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey)
prepayData, err = w.CreateWechatH5Order(ctx, amount, description, outTradeNo, userAuthModel.AuthKey)
if err != nil {
return "", err
}
case "app":
case model.PlatformApp:
// 如果是 APP 平台,调用 APP 支付订单创建
prepayData, err = w.CreateWechatAppOrder(ctx, amount, description, outTradeNo)
default:
@@ -292,7 +322,7 @@ func (w *WechatPayService) QueryOrderStatus(ctx context.Context, transactionID s
// 调用 QueryOrderById 方法查询订单状态
resp, result, err := svc.QueryOrderById(ctx, jsapi.QueryOrderByIdRequest{
TransactionId: core.String(transactionID),
Mchid: core.String(w.config.MchID),
Mchid: core.String(w.config.Wxpay.MchID),
})
if err != nil {
return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode)
@@ -314,7 +344,7 @@ func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string,
resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{
OutTradeNo: core.String(outTradeNo),
OutRefundNo: core.String(outRefundNo),
NotifyUrl: core.String(w.config.RefundNotifyUrl),
NotifyUrl: core.String(w.config.Wxpay.RefundNotifyUrl),
Amount: &refunddomestic.AmountReq{
Currency: core.String("CNY"),
Refund: core.Int64(lzUtils.ToWechatAmount(refundAmount)),