diff --git a/MP_verify_dbfX2r5hEeBPV3qp.txt b/MP_verify_dbfX2r5hEeBPV3qp.txt new file mode 100644 index 0000000..07bb86d --- /dev/null +++ b/MP_verify_dbfX2r5hEeBPV3qp.txt @@ -0,0 +1 @@ +dbfX2r5hEeBPV3qp \ No newline at end of file diff --git a/Makefile b/Makefile index 7cdee24..f95b409 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: frontend build tar image: frontend docker-build save-image -TAG=v1.0 +TAG=v1.0.3 frontend: cd ui && yarn run build diff --git a/cls b/cls index a69c6e9..ff10626 100644 Binary files a/cls and b/cls differ diff --git a/cls-h5.v1.0.tar.gz b/cls-h5.v1.0.tar.gz index 7adaeed..c6a1052 100644 Binary files a/cls-h5.v1.0.tar.gz and b/cls-h5.v1.0.tar.gz differ diff --git a/cmd/run.go b/cmd/run.go index 6f86bb9..5e8c94d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -25,6 +25,7 @@ func runServer(*cli.Context) error { config.NewSmsService, config.NewInternalClient, config.NewJWTAuthMiddleware, + config.NewPayService, ), // 基础服务 fx.Options(captchafx.Module), fx.Invoke( diff --git a/config/app_config.go b/config/app_config.go index 13ddc3f..e8f3c2a 100644 --- a/config/app_config.go +++ b/config/app_config.go @@ -16,6 +16,7 @@ type AppConfig struct { MysqlConfig MysqlConfig `mapstructure:"mysql"` TagsConfig TagsConfig `mapstructure:"tags"` RedisConfig RedisConfig `mapstructure:"redis"` + WeChatConfig WeChatConfig `mapstructure:"wechat"` } type TagsConfig struct { @@ -78,6 +79,16 @@ type RedisConfig struct { DB int `mapstructure:"db"` } +type WeChatConfig struct { + MchId string `mapstructure:"mchid"` + MchCertificateSerialNumber string `mapstructure:"mchCertificateSerialNumber"` + MchAPIv3Key string `mapstructure:"mchAPIv3Key"` + PublicCertPath string `mapstructure:"publicCertPath"` + PrivateCertPath string `mapstructure:"privateCertPath"` + AppId string `mapstructure:"appId"` + AppSecret string `mapstructure:"appSecret"` +} + func NewAppConfig() *AppConfig { viper.SetConfigName("config") viper.SetConfigType("yaml") diff --git a/config/base_providers.go b/config/base_providers.go index 5de92b2..62c0346 100644 --- a/config/base_providers.go +++ b/config/base_providers.go @@ -1,6 +1,8 @@ package config import ( + "cls/internal/infrastructure/payment/wechat_pay" + "cls/internal/infrastructure/wechat" Ihttp "cls/pkg/http" "cls/pkg/logger" "cls/pkg/sms" @@ -8,6 +10,7 @@ import ( "cls/pkg/xorm_engine" "cls/ui" "context" + "encoding/json" "fmt" _ "github.com/go-sql-driver/mysql" "net/http" @@ -35,6 +38,67 @@ func NewGinEngine(lc fx.Lifecycle, auth *auth_middleware.AuthMiddleware, appConf } handler := gin.New() handler.Use(gin.Recovery()) + handler.GET("/MP_verify_dbfX2r5hEeBPV3qp.txt", func(c *gin.Context) { + c.File("MP_verify_dbfX2r5hEeBPV3qp.txt") + }) + handler.GET("/callback_test", func(c *gin.Context) { + o := "oC8UY6Tc_qFZ33JDFZPzyqL_ZqnU" + token := auth.HandleCallback(o) + p := fmt.Sprintf("/?token=%s", token) + fmt.Println(p) + c.Redirect(http.StatusFound, p) + }) + + handler.GET("/callback", func(c *gin.Context) { + code := c.Query("code") + if code == "" { + c.String(http.StatusBadRequest, "Missing code") + return + } + fmt.Println("get code done", code) + url := fmt.Sprintf( + "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", + appConfig.WeChatConfig.AppId, + appConfig.WeChatConfig.AppSecret, + code, + ) + + resp, err := http.Get(url) + if err != nil { + c.String(http.StatusInternalServerError, "Request WeChat failed: %v", err) + return + } + defer resp.Body.Close() + + var res struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + OpenID string `json:"openid"` + Scope string `json:"scope"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + } + + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + c.String(http.StatusInternalServerError, "Failed to decode response") + return + } + fmt.Printf("%+v\n", res) + if res.ErrCode != 0 { + c.String(http.StatusInternalServerError, "WeChat error: %s", res.ErrMsg) + return + } + + // 🎯 获取到 openid + openid := res.OpenID + fmt.Println("get openid,", openid) + fmt.Println("openid:", openid) + token := auth.HandleCallback(openid) + fmt.Println(token) + c.Redirect(http.StatusFound, fmt.Sprintf("/?token=%s", token)) + //c.String(http.StatusFound, token) + }) handler.Use(auth.Handle()) handler.Use(middleware.ServeRoot("/", "dist", &ui.RESOURCE)) handler.NoRoute(func(context *gin.Context) { @@ -193,3 +257,17 @@ func NewInternalClient() *Ihttp.Client { func NewJWTAuthMiddleware(appConfig *AppConfig, log logger.New, redis redis.Cmdable) *auth_middleware.AuthMiddleware { return auth_middleware.NewAuthMiddleware(appConfig.JwtConfig.Secret, nil, redis, log) } + +func NewPayService(config *AppConfig) *wechat_pay.PayService { + return wechat_pay.NewPayService(config.WeChatConfig.MchId, + config.WeChatConfig.AppId, + config.WeChatConfig.MchCertificateSerialNumber, + config.WeChatConfig.MchAPIv3Key, + config.WeChatConfig.PrivateCertPath, + config.WeChatConfig.PublicCertPath, + ) +} + +func NewWechatService(config *AppConfig) *wechat.WechatService { + return wechat.NewWechatService(config.WeChatConfig.AppId, config.WeChatConfig.AppSecret) +} diff --git a/go.mod b/go.mod index 6b77cb8..d486bce 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1135 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1115 // indirect github.com/ugorji/go/codec v1.2.7 // indirect + github.com/wechatpay-apiv3/wechatpay-go v0.2.20 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/otel v1.11.2 // indirect go.opentelemetry.io/otel/trace v1.11.2 // indirect diff --git a/go.sum b/go.sum index a67b72c..9613d9b 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,7 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -582,6 +583,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +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/wenlng/go-captcha-assets v1.0.5 h1:TL+31Qe/kJwcuYyU+jHedjSTZnMu1XKgktKL++lH9Js= github.com/wenlng/go-captcha-assets v1.0.5/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= github.com/wenlng/go-captcha/v2 v2.0.3 h1:QTZ39/gVDisPSgvL9O2X2HbTuj5P/z8QsdGB/aayg9c= diff --git a/internal/application/article/service.go b/internal/application/article/service.go index ad9d942..feac48b 100644 --- a/internal/application/article/service.go +++ b/internal/application/article/service.go @@ -90,9 +90,12 @@ func (a *ArticleService) Find(ePhone string, page *page.Page, searchParams map[s classCode := class_type_reverse[class] if classCode != 0 { conds = append(conds, builder.Eq{"type": classCode}) + conds = append(conds, builder.And(builder.Gt{"ctime": time.Now().AddDate(0, 0, -10).Unix()}).And(builder.Eq{"is_free": 0})) + } + } else { + conds = append(conds, builder.And(builder.Gt{"ctime": time.Now().AddDate(0, 0, -2).Unix()}).And(builder.Eq{"is_free": 0})) } - conds = append(conds, builder.And(builder.Gt{"ctime": time.Now().AddDate(0, 0, -2).Unix()}).And(builder.Eq{"is_free": 0})) articles := make([]*article.LianV1Article, 0) page.Content = &articles err = a.repo.Find(page, conds) @@ -244,9 +247,12 @@ func (a *ArticleService) FindFree(page *page.Page, searchParams map[string]strin classCode := class_type_reverse[class] if classCode != 0 { conds = append(conds, builder.Eq{"type": classCode}) + conds = append(conds, builder.And(builder.Lt{"ctime": time.Now().AddDate(0, 0, -10).Unix()}).And(builder.Eq{"is_free": 1})) + } + } else { + conds = append(conds, builder.And(builder.Lt{"ctime": time.Now().AddDate(0, 0, -2).Unix()}).And(builder.Eq{"is_free": 1})) } - conds = append(conds, builder.And(builder.Lt{"ctime": time.Now().AddDate(0, 0, -2).Unix()}).And(builder.Eq{"is_free": 1})) articles := make([]*article.LianV1Article, 0) page.Content = &articles err := a.repo.Find(page, conds) @@ -295,28 +301,28 @@ func (a *ArticleService) Detail(userPhone string, id uint64) (*ArticleDto, error } articleDto := &ArticleDto{} - if p == nil { + if p.Id < 1 { articleDto.Unlock = false return articleDto, nil } - article, err := a.repo.GetArticleById(id) + aInfo, err := a.repo.GetArticleById(id) if err != nil { a.log.Error(err) return nil, err } - t := time.Unix(article.Ctime, 0) - m, g := a.getMainGrowthBoard(article.Stocks) + t := time.Unix(aInfo.Ctime, 0) + m, g := a.getMainGrowthBoard(aInfo.Stocks) return &ArticleDto{ - EventId: article.Id, - Title: article.Title, - Class: class_type[article.Type], + EventId: aInfo.Id, + Title: aInfo.Title, + Class: class_type[aInfo.Type], ReleaseDate: t.Format("2006-01-02"), ReleaseTime: t.Format("15:04"), - Stocks: article.Stocks, - Brief: article.Brief, + Stocks: aInfo.Stocks, + Brief: aInfo.Brief, MainBoard: m, GrowthBoard: g, - Content: article.Content, + Content: aInfo.Content, Unlock: true, }, nil } diff --git a/internal/application/auth/captcha_service.go b/internal/application/auth/captcha_service.go index addadda..943e489 100644 --- a/internal/application/auth/captcha_service.go +++ b/internal/application/auth/captcha_service.go @@ -226,10 +226,7 @@ func abs(n int) int { } // GenerateSmsCaptcha 生成短信验证码 -func (c *CaptchaService) GenerateSmsCaptcha(username string, phone string) (*SmsCaptchaResp, error) { - if username == "" { - return nil, errors.New("用户标识不能为空") - } +func (c *CaptchaService) GenerateSmsCaptcha(phone string) (*SmsCaptchaResp, error) { if phone == "" { return nil, errors.New("手机号不能为空") } @@ -258,7 +255,7 @@ func (c *CaptchaService) GenerateSmsCaptcha(username string, phone string) (*Sms ResetAfter: int(t), }, errors.New("发送过于频繁,请稍后再试") } - + //code := strings.Join(strings.Split(phone, "")[:6], "") // 3. 生成验证码 code := c.generateSmsCode() // 4. 存储验证码 diff --git a/internal/application/auth/service.go b/internal/application/auth/service.go index 5851cc2..72f4725 100644 --- a/internal/application/auth/service.go +++ b/internal/application/auth/service.go @@ -3,6 +3,7 @@ package auth import ( "cls/internal/application/crypto" "cls/internal/domain/auth" + "errors" "cls/pkg/logger" ) @@ -30,13 +31,16 @@ func (a *AuthService) GetJWTSecretKey() string { return a.jwtSecretKey } -func (a *AuthService) LoginByCaptcha(phone string) (string, error) { +func (a *AuthService) LoginByCaptcha(phone, openid string) (string, error) { + if openid == "" { + return "", errors.New("openid为空") + } ePhone, err := a.phoneEncryption.Encrypt(phone) if err != nil { a.log.Error(err.Error()) return "", err } - token, err := a.auth.LoginByCaptcha(ePhone) + token, err := a.auth.LoginByCaptcha(ePhone, openid) if err != nil { return "", err } diff --git a/internal/application/crypto/phone_test.go b/internal/application/crypto/phone_test.go index f5fda79..57d92f6 100644 --- a/internal/application/crypto/phone_test.go +++ b/internal/application/crypto/phone_test.go @@ -4,7 +4,7 @@ import "testing" func TestPhoneEncode(t *testing.T) { ser := NewPhoneEncryptionService() - v, _ := ser.Encrypt("17782351006") + v, _ := ser.Encrypt("15585160327") t.Log(v) } diff --git a/internal/application/order/dto.go b/internal/application/order/dto.go index 47d52ca..a194973 100644 --- a/internal/application/order/dto.go +++ b/internal/application/order/dto.go @@ -22,6 +22,7 @@ type CreateOrderRequest struct { Amount int64 `json:"amount"` // 订单金额 Duration int `json:"duration"` // 购买时长 Description string `json:"description"` // 商品描述 + CouponId uint64 `json:"coupon"` //优惠券 } // OrderResponse 订单响应 diff --git a/internal/application/order/service.go b/internal/application/order/service.go index 807a79a..ff1dce3 100644 --- a/internal/application/order/service.go +++ b/internal/application/order/service.go @@ -1,53 +1,284 @@ package order import ( + "bytes" dto "cls/internal/application/payment" + "cls/internal/domain/article" + "cls/internal/domain/column" + "cls/internal/domain/coupon" "cls/internal/domain/order" "cls/internal/domain/payment" + "cls/internal/domain/price" + "cls/internal/domain/user" + "cls/internal/infrastructure/payment/wechat_pay" "cls/pkg/logger" + "errors" "fmt" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" "time" + + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" ) // OrderService 订单应用服务 type OrderService struct { - repo order.AggregateRepository - log logger.Logger + orderRepo order.OrderRepository + repo order.AggregateRepository + articleRepo article.ArticleRepository + columnRepo column.ColumnRepository + priceRepo price.PriceRepository + payService *wechat_pay.PayService + userRepo user.UserRepository + couponRepo coupon.CouponRepository + log logger.Logger } // NewOrderService 创建订单应用服务 -func NewOrderService(repo order.AggregateRepository, log logger.New) *OrderService { +func NewOrderService(repo order.AggregateRepository, articleRepo article.ArticleRepository, + priceRepo price.PriceRepository, userRepo user.UserRepository, + couponRepo coupon.CouponRepository, payService *wechat_pay.PayService, + orderRepo order.OrderRepository, + columnRepo column.ColumnRepository, log logger.New) *OrderService { return &OrderService{ - repo: repo, - log: log("cls:service:order"), + repo: repo, + articleRepo: articleRepo, + priceRepo: priceRepo, + columnRepo: columnRepo, + payService: payService, + orderRepo: orderRepo, + userRepo: userRepo, + couponRepo: couponRepo, + log: log("cls:service:order"), } } // CreateOrder 创建订单 -func (s *OrderService) CreateOrder(req *CreateOrderRequest) (*OrderResponse, error) { +func (s *OrderService) CreateOrder(req *CreateOrderRequest, ePhone string) (*jsapi.PrepayWithRequestPaymentResponse, error) { + fmt.Println("=============") + fmt.Println(req) + uInfo, err := s.userRepo.FindByPhone(ePhone) + if err != nil { + s.log.Error(err) + return nil, err + } + + if uInfo.Openid == "" { + s.log.Error(fmt.Sprintf("用户【%d】openid为空", req.UserID)) + return nil, errors.New("") + } + + oInfo, err := s.orderRepo.GetPendingOrder(req.TargetID, req.Type, uInfo.Id) + if err != nil { + s.log.Error(err) + return nil, err + } + if oInfo.ID > 0 { + s.log.Errorf("存在相同订单【%d】【%d】", req.TargetID, req.Type) + return nil, errors.New("存在相同订单") + } + + priceInfo, err := s.priceRepo.FindByTargetID(req.TargetID, price.PriceType(req.Type)) + if err != nil { + s.log.Error(err) + return nil, err + } + + fmt.Println("==============") + fmt.Printf("%+v\n", priceInfo) + fmt.Println("==============") + + descripton := &bytes.Buffer{} + descripton.Write([]byte("上海路程-")) + if req.Type == order.OrderTypeArticle { + //购买文章,校验文章 + req.Amount = priceInfo.Amount + descripton.Write([]byte("文章消费")) + } else { + //购买专栏,校验专栏 + + switch req.Duration { + case 1: + priceInfo.Amount = priceInfo.OneMonthPrice + case 3: + priceInfo.Amount = priceInfo.ThreeMonthsPrice + case 6: + priceInfo.Amount = priceInfo.SixMonthsPrice + case 12: + priceInfo.Amount = priceInfo.OneYearPrice + } + cInfo, err := s.columnRepo.FindByID(req.TargetID) + if err != nil { + s.log.Error(err) + return nil, err + } + + descripton.Write([]byte(fmt.Sprintf("订购专栏【%s】%d个月", cInfo.Title, req.Duration))) + } + fmt.Println("==============") + fmt.Printf("%+v\n", req) + fmt.Println("==============") + + couponInfo := &coupon.Coupon{ID: req.CouponId} + if req.CouponId != 0 { + fmt.Println("使用优惠券") + //使用优惠券 + couponInfo, err = s.couponRepo.GetByID(req.CouponId) + if err != nil { + s.log.Error(err) + return nil, err + } + + couponInfo.UserID = uInfo.Id + couponInfo.UsedAt = time.Now() + fmt.Printf("%+v\n", couponInfo) + if couponInfo.CanUse(req.Amount) { + fmt.Println("can use") + req.Amount = priceInfo.GetFinalAmountWitCoupon(couponInfo.Value) + } else { + fmt.Println("cann't use it") + } + + couponInfo.Status = coupon.CouponStatusUsed + } else { + fmt.Println("不使用优惠券") + req.Amount = priceInfo.GetFinalAmount() + } + fmt.Printf("%+v\n", req) + + if req.Amount <= 0 { + s.log.Error("价格小于0元") + return nil, errors.New("") + } // 创建订单 o := &order.Order{ - OrderNo: generateOrderNo(), - UserID: req.UserID, + OrderNo: s.generateOrderNo(), + UserID: uInfo.Id, TargetID: req.TargetID, + Coupon: req.CouponId, Type: req.Type, Amount: req.Amount, Duration: req.Duration, Status: order.OrderStatusPending, Description: req.Description, } - // 创建聚合根 aggregate := order.NewOrderAggregate(o) + err = aggregate.CreatePayment(payment.PaymentTypeWechat) + if err != nil { + s.log.Error(err) + return nil, err + } + + aggregate.Coupon = couponInfo + + resp, err := s.payService.CreatePayment(&wechat_pay.PaymentInfo{ + Description: descripton.String(), + Attach: "", + OutTradeNo: o.OrderNo, + AmountTotal: req.Amount, + OpenId: uInfo.Openid, + }) + + if err != nil { + s.log.Error(err) + return nil, err + } // 保存聚合根 - if err := s.repo.Save(aggregate); err != nil { + if err = s.repo.Save(aggregate); err != nil { s.log.Error(err) return nil, err } - return nil, nil + return resp, nil +} + +func (s *OrderService) CancelOrder(req *CreateOrderRequest, ePhone string) error { + uInfo, err := s.userRepo.FindByPhone(ePhone) + if err != nil { + s.log.Error(err) + return err + } + + oInfo, err := s.orderRepo.GetPendingOrder(req.TargetID, req.Type, uInfo.Id) + if err != nil { + s.log.Error(err) + return err + } + + if oInfo.ID < 1 { + s.log.Errorf("不存在的产品订购信息[%d][%d]", req.TargetID, req.Type) + return errors.New("") + } + + agg, err := s.repo.GetByOrderNo(oInfo.OrderNo) + if err != nil { + s.log.Error(err) + return err + } + + err = agg.HandlePaymentFailed("H5端取消支付") + if err != nil { + s.log.Error(err) + return err + } + + err = s.repo.PayFailed(agg) + if err != nil { + s.log.Error(err) + + } + return err +} + +func (s *OrderService) OrderNotify(transaction *payments.Transaction) error { + s.log.Info("订单回调======>") + s.log.Infof("%+v\n", *transaction) + aggregate, err := s.repo.GetByOrderNo(*transaction.OutTradeNo) + if err != nil { + s.log.Error(err) + return err + } + + if *transaction.Amount.Total != aggregate.Order.Amount { + err = aggregate.HandlePaymentFailed(fmt.Sprintf("订单金额不匹配 %+v", transaction)) + if err != nil { + s.log.Error(err) + return err + } + err = s.repo.PayFailed(aggregate) + if err != nil { + s.log.Error(err) + return err + } + } + + if *transaction.TradeState == "SUCCESS" { + err = aggregate.HandlePaymentSuccess(*transaction.TransactionId, fmt.Sprintf("%+v", transaction)) + if err != nil { + s.log.Error(err) + return err + } + err = s.repo.PaySuccess(aggregate) + if err != nil { + s.log.Error(err) + return err + } + } else { + err = aggregate.HandlePaymentFailed(fmt.Sprintf("%+v", transaction)) + if err != nil { + s.log.Error(err) + return err + } + err = s.repo.PayFailed(aggregate) + if err != nil { + s.log.Error(err) + return err + } + } + + return nil } // CreatePayment 创建支付订单 @@ -83,6 +314,6 @@ func (s *OrderService) DeleteOrder(id uint64) error { } // generateOrderNo 生成订单号 -func generateOrderNo() string { - return fmt.Sprintf("%d%d", time.Now().UnixNano()/1e6, time.Now().Unix()%1000) +func (s *OrderService) generateOrderNo() string { + return fmt.Sprintf("LCPAY%d%d", time.Now().UnixNano()/1e6, time.Now().Unix()%1000) } diff --git a/internal/application/payment/service.go b/internal/application/payment/service.go index 16d28be..aeb8ed8 100644 --- a/internal/application/payment/service.go +++ b/internal/application/payment/service.go @@ -128,3 +128,6 @@ func (s *PaymentService) DeleteOrder(id uint64) error { } // CreatePayment 创建支付订单 +func (s *PaymentService) CreatePayment() error { + return nil +} diff --git a/internal/application/purchase/service.go b/internal/application/purchase/service.go index 6312235..8868779 100644 --- a/internal/application/purchase/service.go +++ b/internal/application/purchase/service.go @@ -6,19 +6,19 @@ import ( ) // Service 购买记录应用服务 -type Service struct { +type PurchaseService struct { repo purchase.Repository } // NewService 创建购买记录应用服务 -func NewService(repo purchase.Repository) *Service { - return &Service{ +func NewPurchaseService(repo purchase.Repository) *PurchaseService { + return &PurchaseService{ repo: repo, } } // CreatePurchase 创建购买记录 -func (s *Service) CreatePurchase(userId, contentId uint64, contentType purchase.ContentType, price float64) error { +func (s *PurchaseService) CreatePurchase(userId, contentId uint64, contentType purchase.ContentType, price float64) error { p := &purchase.Purchase{} var err error diff --git a/internal/application/wechat/dto.go b/internal/application/wechat/dto.go new file mode 100644 index 0000000..9da997d --- /dev/null +++ b/internal/application/wechat/dto.go @@ -0,0 +1,8 @@ +package wechat + +type WxJsConfigRes struct { + AppId string `json:"appId"` + Timestamp string `json:"timestamp"` + NonceStr string `json:"nonceStr"` + Signature string `json:"signature"` +} diff --git a/internal/application/wechat/service.go b/internal/application/wechat/service.go new file mode 100644 index 0000000..7f6efad --- /dev/null +++ b/internal/application/wechat/service.go @@ -0,0 +1,31 @@ +package wechat + +import ( + "cls/internal/infrastructure/wechat" + "cls/pkg/logger" +) + +type WechatService struct { + log logger.Logger + ws *wechat.WechatService +} + +func NewWechatService(log logger.New, ws *wechat.WechatService) *WechatService { + return &WechatService{ + log: log("cls:service:wechatService"), + ws: ws, + } +} + +func (w *WechatService) GetOpenId(code string) (string, error) { + openid, err := w.ws.GetOpenID(code) + if err != nil { + w.log.Error(err) + + } + return openid, err +} + +func (w *WechatService) GetWxJsConfig() (*WxJsConfigRes, error) { + return nil, nil +} diff --git a/internal/domain/auth/entity.go b/internal/domain/auth/entity.go index 3fa43b2..9018259 100644 --- a/internal/domain/auth/entity.go +++ b/internal/domain/auth/entity.go @@ -2,5 +2,5 @@ package auth type AuthRepository interface { LoginByPassword(phone, password string) (string, error) - LoginByCaptcha(phone string) (string, error) + LoginByCaptcha(phone, openid string) (string, error) } diff --git a/internal/domain/coupon/entity.go b/internal/domain/coupon/entity.go index 6f774c8..a98b5ac 100644 --- a/internal/domain/coupon/entity.go +++ b/internal/domain/coupon/entity.go @@ -14,6 +14,7 @@ const ( type CouponStatus int const ( + CouponStatusBefore CouponStatus = 0 // 未到使用时间 CouponStatusNormal CouponStatus = 1 // 正常 CouponStatusUsed CouponStatus = 2 // 已使用 CouponStatusExpired CouponStatus = 3 // 已过期 @@ -39,7 +40,30 @@ type Coupon struct { DeletedAt time.Time `xorm:"deleted"` // 删除时间 } -// TableName 指定表名 +func (c *Coupon) BeforeInsert() { + c.UpdateStatus() +} + +func (c *Coupon) AfterLoad() { + c.UpdateStatus() +} + +func (c *Coupon) UpdateStatus() { + if c.ID > 0 { + if c.IsUsed() || c.IsDisabled() { + return + } + if c.IsExpired() { + c.Status = CouponStatusExpired + return + } + if c.IsNotStarted() { + c.Status = CouponStatusBefore + return + } + c.Status = CouponStatusNormal + } +} // IsValid 检查优惠券是否有效 func (c *Coupon) IsValid() bool { @@ -65,3 +89,8 @@ func (c *Coupon) IsUsed() bool { func (c *Coupon) IsDisabled() bool { return c.Status == CouponStatusDisabled } + +func (c *Coupon) CanUse(amount int64) bool { + c.UpdateStatus() + return c.Status == CouponStatusNormal && c.MinAmount <= amount +} diff --git a/internal/domain/order/aggregate.go b/internal/domain/order/aggregate.go index 6b21370..c6ba3ef 100644 --- a/internal/domain/order/aggregate.go +++ b/internal/domain/order/aggregate.go @@ -1,6 +1,7 @@ package order import ( + "cls/internal/domain/coupon" "cls/internal/domain/payment" "cls/internal/domain/purchase" "errors" @@ -12,6 +13,7 @@ type OrderAggregate struct { Order *Order Payment *payment.Payment Purchase *purchase.Purchase + Coupon *coupon.Coupon } // NewOrderAggregate 创建订单聚合根 @@ -59,13 +61,14 @@ func (a *OrderAggregate) HandlePaymentSuccess(transactionID string, notifyData s // 创建购买记录 a.Purchase = &purchase.Purchase{ - UserId: a.Order.UserID, - ContentId: a.Order.TargetID, - ContentType: purchase.ContentType(a.Order.Type), - Price: float64(a.Order.Amount) / 100, // 转换为元 - Duration: a.Order.Duration, - ExpiredAt: time.Now().AddDate(0, a.Order.Duration, 0), - Status: 1, // 有效状态 + UserId: a.Order.UserID, + ContentId: a.Order.TargetID, + ContentType: purchase.ContentType(a.Order.Type), + Price: float64(a.Order.Amount) / 100, // 转换为元 + Duration: a.Order.Duration, + ContentSource: purchase.ContentSourceBuy, + ExpiredAt: time.Now().AddDate(0, a.Order.Duration, 0), + Status: 1, // 有效状态 } return nil @@ -87,6 +90,11 @@ func (a *OrderAggregate) HandlePaymentFailed(notifyData string) error { // 更新订单状态 a.Order.Status = OrderStatusCanceled + if a.Coupon != nil { + if a.Coupon.ID > 0 { + a.Coupon.Status = coupon.CouponStatusNormal + } + } return nil } diff --git a/internal/domain/order/aggregate_repository.go b/internal/domain/order/aggregate_repository.go index 02c50a0..cc0ed17 100644 --- a/internal/domain/order/aggregate_repository.go +++ b/internal/domain/order/aggregate_repository.go @@ -6,6 +6,10 @@ import "cls/pkg/util/page" type AggregateRepository interface { // Save 保存订单聚合根 Save(aggregate *OrderAggregate) error + // PaySuccess 支付成功 + PaySuccess(aggregate *OrderAggregate) error + // PayFailed 支付失败 + PayFailed(aggregate *OrderAggregate) error // GetByOrderNo 根据订单号获取订单聚合根 GetByOrderNo(orderNo string) (*OrderAggregate, error) // GetByID 根据ID获取订单聚合根 diff --git a/internal/domain/order/entity.go b/internal/domain/order/entity.go index 44e5db0..1ecf5ab 100644 --- a/internal/domain/order/entity.go +++ b/internal/domain/order/entity.go @@ -22,18 +22,19 @@ const ( // Order 订单实体 type Order struct { - ID uint64 `gorm:"primarykey" json:"id"` // 订单ID - OrderNo string `gorm:"uniqueIndex" json:"orderNo"` // 订单编号 - UserID uint64 `json:"userId"` // 用户ID - TargetID uint64 `json:"targetId"` // 商品ID(文章ID或专栏ID) - Type OrderType `json:"type"` // 订单类型(文章/专栏) - Amount int64 `json:"amount"` // 订单金额(分) - Duration int `json:"duration"` // 购买时长(月) - Status OrderStatus `json:"status"` // 订单状态 - Description string `json:"description"` // 商品描述 - CreatedAt time.Time `json:"createdAt"` // 创建时间 - UpdatedAt time.Time `json:"updatedAt"` // 更新时间 - DeletedAt *time.Time `gorm:"index" json:"deletedAt"` // 删除时间 + ID uint64 `xorm:"pk autoincr 'id'" ` // 支付ID + OrderNo string `xorm:"unique"` // 订单编号 + UserID uint64 `json:"userId"` // 用户ID + TargetID uint64 `json:"targetId"` // 商品ID(文章ID或专栏ID) + Type OrderType `json:"type"` // 订单类型(文章/专栏) + Amount int64 `json:"amount"` // 订单金额(分) + Duration int `json:"duration"` // 购买时长(月) + Coupon uint64 //优惠券 + Status OrderStatus `json:"status"` // 订单状态 + Description string `json:"description"` // 商品描述 + CreatedAt time.Time `xorm:"creaed" ` // 创建时间 + UpdatedAt time.Time `xorm:"updated" ` // 更新时间 + DeletedAt *time.Time `xorm:"deleted"` // 删除时间 } // TableName 指定表名 diff --git a/internal/domain/order/repository.go b/internal/domain/order/repository.go index 8348ff1..52fe9d1 100644 --- a/internal/domain/order/repository.go +++ b/internal/domain/order/repository.go @@ -22,4 +22,7 @@ type OrderRepository interface { // Delete 删除订单 Delete(id uint64) error + + // GetPendingOrder 获取待支付订单 + GetPendingOrder(targetId uint64, t OrderType, uid uint64) (*Order, error) } diff --git a/internal/domain/payment/entity.go b/internal/domain/payment/entity.go index acce724..5abe17f 100644 --- a/internal/domain/payment/entity.go +++ b/internal/domain/payment/entity.go @@ -22,19 +22,19 @@ const ( // Payment 支付订单实体 type Payment struct { - ID uint64 `gorm:"primarykey" json:"id"` // 支付ID - OrderNo string `gorm:"uniqueIndex" json:"orderNo"` // 订单编号 - TransactionID string `json:"transactionId"` // 第三方交易号 - UserID uint64 `json:"userId"` // 用户ID - TargetID uint64 `json:"targetId"` // 商品ID - Type PaymentType `json:"type"` // 支付类型 - Amount int64 `json:"amount"` // 支付金额 - Status PaymentStatus `json:"status"` // 支付状态 - Description string `json:"description"` // 支付描述 - NotifyData string `json:"notifyData"` // 支付回调数据 - CreatedAt time.Time `json:"createdAt"` // 创建时间 - UpdatedAt time.Time `json:"updatedAt"` // 更新时间 - DeletedAt *time.Time `gorm:"index" json:"deletedAt"` // 删除时间 + ID uint64 `xorm:"pk autoincr 'id'" ` // 支付ID + OrderNo string `xorm:"unique"` // 订单编号 + TransactionID string // 第三方交易号 + UserID uint64 // 用户ID + TargetID uint64 // 商品ID + Type PaymentType // 支付类型 + Amount int64 // 支付金额 + Status PaymentStatus // 支付状态 + Description string `xorm:"varchar(500)"` // 支付描述 + NotifyData string `xorm:"varchar(1000) notnull "` // 支付回调数据 + CreatedAt time.Time `xorm:"creaed" ` // 创建时间 + UpdatedAt time.Time `xorm:"updated" ` // 更新时间 + DeletedAt *time.Time `xorm:"deleted"` // 删除时间 } // TableName 指定表名 diff --git a/internal/domain/price/entity.go b/internal/domain/price/entity.go index 9f6f941..71186c9 100644 --- a/internal/domain/price/entity.go +++ b/internal/domain/price/entity.go @@ -1,6 +1,8 @@ package price -import "time" +import ( + "time" +) // PriceType 价格类型 type PriceType int8 @@ -48,3 +50,26 @@ func NewPrice(targetID uint64, priceType PriceType, amount int64, adminID uint64 Discount: 0.3, } } + +func (p *Price) GetFinalAmount() int64 { + if p.Amount <= 10 { + return p.Amount + } + if p.Discount <= 0 || p.Discount > 1.0 { + // 折扣无效,返回原价(你也可以返回 0,视业务而定) + return p.Amount + } + // float32 → float64,避免精度损失 + + final := float32(p.Amount) * p.Discount + return int64(final) +} + +func (p *Price) GetFinalAmountWitCoupon(value int64) int64 { + amount := p.GetFinalAmount() + final := amount - value + if final < 0 { + return 0 + } + return final +} diff --git a/internal/domain/price/entity_test.go b/internal/domain/price/entity_test.go new file mode 100644 index 0000000..8fcd628 --- /dev/null +++ b/internal/domain/price/entity_test.go @@ -0,0 +1,9 @@ +package price + +import "testing" + +func TestGetFinalAmount(t *testing.T) { + p := &Price{Amount: 11, Discount: 0.7} + v := p.GetFinalAmount() + t.Log(v) +} diff --git a/internal/domain/user/entity.go b/internal/domain/user/entity.go index 71cd2c9..b031849 100644 --- a/internal/domain/user/entity.go +++ b/internal/domain/user/entity.go @@ -26,6 +26,7 @@ type User struct { Id uint64 `xorm:"pk autoincr 'id'" ` Username string `xorm:"varchar(50) 'username'"` Phone string `xorm:"varchar(60) unique 'phone'" ` + Openid string // 密码相关 Password string `xorm:"varchar(60) 'password'" ` // 使用 bcrypt 加密存储,长度60 Salt string `xorm:"varchar(32) 'salt'" ` // 密码加密盐值 diff --git a/internal/infrastructure/middleware/auth/config.go b/internal/infrastructure/middleware/auth/config.go index 3321d4d..015a24f 100644 --- a/internal/infrastructure/middleware/auth/config.go +++ b/internal/infrastructure/middleware/auth/config.go @@ -8,18 +8,45 @@ type Config struct { func DefaultConfig() *Config { return &Config{ SkipPaths: []string{ - "/api/auth/login", - "/api/auth/register", - "/api/auth/image-captcha", - "/api/auth/sms-captcha", - "/api/auth/login/sms", - "/api/auth/login/password", - "/api/auth/captcha/sms", - "/api/auth/verify-image", - "/api/auth/verify-sms", - "/api/article/all", - "/api/user/guest", + "/api/user/profile", + "/api/user/disable", + "/api/user/enable", + "/api/purchase/save", + "/api/purchase/user", + "/api/purchase/content", + "/api/order/create", + "/api/order/order_notify/create", + "/api/order/create", + "/api/coupon/get", + "/api/coupon/create", + "/api/article/unlock", + "/api/article/unlock-article", }, TokenKey: "Authorization", } } + +//func DefaultConfig() *Config { +// return &Config{ +// SkipPaths: []string{ +// "/", +// "/home", +// "/home/special-column", +// "/home/article-detail", +// "/mine", +// "/mine/login", +// "/callback", //openid 回调接口 +// "/callback_test", //openid 回调测试接口 +// "/api/order/order_notify", //微信支付回调接口 +// "/api/auth/image-captcha", +// "/api/auth/sms-captcha", +// "/api/auth/login-captcha", +// "/api/auth/login-password", +// "/api/auth/captcha/sms", +// "/api/auth/verify-image", +// "/api/auth/verify-sms", +// "/api/article/all", +// }, +// TokenKey: "Authorization", +// } +//} diff --git a/internal/infrastructure/middleware/auth/middleware.go b/internal/infrastructure/middleware/auth/middleware.go index b1bc8aa..fd43ecc 100644 --- a/internal/infrastructure/middleware/auth/middleware.go +++ b/internal/infrastructure/middleware/auth/middleware.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v4" "github.com/redis/go-redis/v9" + "net/http" "time" ) @@ -19,6 +20,7 @@ const ( type Claims struct { Username string `json:"username"` // 加密后的手机号,空表示游客 GuestId string `json:"guest_id"` // 游客ID,用于标识未登录用户 + Openid string `json:"openid"` jwt.RegisteredClaims } @@ -43,69 +45,43 @@ func NewAuthMiddleware(secretKey string, config *Config, redis redis.Cmdable, lo } } +func (m *AuthMiddleware) HandleCallback(openid string) string { + t, _, err := m.generateGuestToken(openid) + if err != nil { + m.log.Error(err) + return "" + } + return t +} + // Handle 处理认证逻辑 func (m *AuthMiddleware) Handle() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path + // 获取token token := m.ExtractToken(c) // 如果没有token,创建临时游客token if token == "" { - newToken, guestId, err := m.generateGuestToken() - if err != nil { - c.JSON(500, gin.H{"error": "生成临时token失败"}) - c.Abort() - return - } - // 设置token到响应头 - c.Header(m.config.TokenKey, "Bearer "+newToken) - - // 设置 claims 到上下文 - mapClaims := map[string]interface{}{ - "guest_id": guestId, - } - c.Set(JWTClaimsKey, mapClaims) - c.Set("is_guest", true) - c.Set("guest_id", guestId) - - // 如果是跳过认证的路径,直接继续 - if m.shouldSkip(path) { + if !m.shouldSkip(path) { c.Next() return + } else { + c.AbortWithStatus(http.StatusUnauthorized) + return } - - c.Next() - return } // 验证已有token claims, err := m.validateToken(token) if err != nil { - // token无效,创建新的临时游客token - newToken, guestId, err := m.generateGuestToken() - if err != nil { - c.JSON(500, gin.H{"error": "生成临时token失败"}) - c.Abort() - return - } - c.Header(m.config.TokenKey, "Bearer "+newToken) - - // 设置 claims 到上下文 - mapClaims := map[string]interface{}{ - "guest_id": guestId, - } - c.Set(JWTClaimsKey, mapClaims) - c.Set("is_guest", true) - c.Set("guest_id", guestId) - - // 如果是跳过认证的路径,直接继续 - if m.shouldSkip(path) { + if !m.shouldSkip(path) { c.Next() return + } else { + c.AbortWithStatus(http.StatusUnauthorized) + return } - - c.Next() - return } // token有效,根据phone判断是否为游客 @@ -120,6 +96,9 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc { mapClaims["username"] = claims.Username c.Set("username", claims.Username) } + mapClaims["openid"] = claims.Openid + c.Set("openid", claims.Openid) + c.Set(JWTClaimsKey, mapClaims) c.Set("is_guest", isGuest) @@ -134,11 +113,17 @@ func (m *AuthMiddleware) Handle() gin.HandlerFunc { } // generateGuestToken 生成游客token,返回token字符串和游客ID -func (m *AuthMiddleware) generateGuestToken() (string, string, error) { +func (m *AuthMiddleware) generateGuestToken(openid ...string) (string, string, error) { guestId := generateGuestId() + o := "" + fmt.Println("have openid ?,", openid) + if len(openid) != 0 { + o = openid[0] + } claims := &Claims{ Username: "", GuestId: guestId, + Openid: o, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), @@ -158,10 +143,11 @@ func (m *AuthMiddleware) generateGuestToken() (string, string, error) { } // GenerateUserToken 生成用户token -func (m *AuthMiddleware) GenerateUserToken(encryptedPhone string) (string, error) { +func (m *AuthMiddleware) GenerateUserToken(encryptedPhone string, openid string) (string, error) { claims := &Claims{ Username: encryptedPhone, GuestId: "", // 登录用户不需要游客ID + Openid: openid, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * 24 * time.Hour)), // 30天过期 IssuedAt: jwt.NewNumericDate(time.Now()), diff --git a/internal/infrastructure/payment/wechat/api.go b/internal/infrastructure/payment/wechat/api.go deleted file mode 100644 index 6bc7edb..0000000 --- a/internal/infrastructure/payment/wechat/api.go +++ /dev/null @@ -1,167 +0,0 @@ -package wechat - -import ( - "crypto/md5" - "crypto/rand" - "encoding/hex" - "encoding/xml" - "fmt" - "io" - "net/http" - "reflect" - "sort" - "strings" - "time" -) - -// APIClient 微信支付API客户端 -type APIClient struct { - config Config - client *http.Client -} - -// NewAPIClient 创建微信支付API客户端 -func NewAPIClient(config Config) *APIClient { - return &APIClient{ - config: config, - client: &http.Client{ - Timeout: 10 * time.Second, - }, - } -} - -// UnifiedOrderRequest 统一下单请求参数 -type UnifiedOrderRequest struct { - AppID string `xml:"appid"` // 公众号ID - MchID string `xml:"mch_id"` // 商户号 - NonceStr string `xml:"nonce_str"` // 随机字符串 - Body string `xml:"body"` // 商品描述 - OutTradeNo string `xml:"out_trade_no"` // 商户订单号 - TotalFee string `xml:"total_fee"` // 订单金额 - SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP - NotifyURL string `xml:"notify_url"` // 通知地址 - TradeType string `xml:"trade_type"` // 交易类型 - Sign string `xml:"sign"` // 签名 -} - -// UnifiedOrderResponse 统一下单响应参数 -type UnifiedOrderResponse struct { - ReturnCode string `xml:"return_code"` // 返回状态码 - ReturnMsg string `xml:"return_msg"` // 返回信息 - ResultCode string `xml:"result_code"` // 业务结果 - ErrCode string `xml:"err_code"` // 错误代码 - ErrCodeDes string `xml:"err_code_des"` // 错误代码描述 - PrepayID string `xml:"prepay_id"` // 预支付交易会话标识 - TradeType string `xml:"trade_type"` // 交易类型 - CodeURL string `xml:"code_url"` // 二维码链接 - Sign string `xml:"sign"` // 签名 -} - -// UnifiedOrder 统一下单 -func (c *APIClient) UnifiedOrder(req *UnifiedOrderRequest) (*UnifiedOrderResponse, error) { - // 设置公共参数 - req.AppID = c.config.AppID - req.MchID = c.config.MchID - req.NonceStr = generateNonceStr() - req.SpbillCreateIP = "127.0.0.1" // 客户端IP - req.NotifyURL = c.config.NotifyURL - req.TradeType = "NATIVE" // 生成二维码支付链接 - - // 生成签名 - req.Sign = c.generateSign(req) - - // 构建XML请求 - xmlData := buildXML(req) - - // 发送请求 - resp, err := c.client.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", strings.NewReader(xmlData)) - if err != nil { - return nil, fmt.Errorf("请求微信支付接口失败: %v", err) - } - defer resp.Body.Close() - - // 读取响应 - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("读取响应失败: %v", err) - } - - // 解析响应 - var result UnifiedOrderResponse - if err := xml.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("解析响应失败: %v", err) - } - - // 验证签名 - if !c.verifySign(&result) { - return nil, fmt.Errorf("签名验证失败") - } - - return &result, nil -} - -// generateSign 生成签名 -func (c *APIClient) generateSign(v interface{}) string { - // 将结构体转换为map - params := make(map[string]string) - val := reflect.ValueOf(v).Elem() - typ := val.Type() - for i := 0; i < val.NumField(); i++ { - field := typ.Field(i) - tag := field.Tag.Get("xml") - if tag == "" || tag == "sign" { - continue - } - params[tag] = fmt.Sprintf("%v", val.Field(i).Interface()) - } - - // 按键排序 - var keys []string - for k := range params { - if k != "sign" && params[k] != "" { - keys = append(keys, k) - } - } - sort.Strings(keys) - - // 拼接字符串 - var builder strings.Builder - for i, k := range keys { - if i > 0 { - builder.WriteString("&") - } - builder.WriteString(k) - builder.WriteString("=") - builder.WriteString(params[k]) - } - builder.WriteString("&key=") - builder.WriteString(c.config.APIKey) - - // MD5加密 - h := md5.New() - h.Write([]byte(builder.String())) - return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) -} - -// verifySign 验证签名 -func (c *APIClient) verifySign(v interface{}) bool { - // TODO: 实现签名验证逻辑 - return true -} - -// generateNonceStr 生成随机字符串 -func generateNonceStr() string { - const length = 32 - bytes := make([]byte, length) - rand.Read(bytes) - return hex.EncodeToString(bytes) -} - -// buildXML 构建XML请求 -func buildXML(v interface{}) string { - data, err := xml.Marshal(v) - if err != nil { - return "" - } - return string(data) -} diff --git a/internal/infrastructure/payment/wechat/service.go b/internal/infrastructure/payment/wechat/service.go deleted file mode 100644 index aada905..0000000 --- a/internal/infrastructure/payment/wechat/service.go +++ /dev/null @@ -1,138 +0,0 @@ -package wechat - -import ( - "cls/internal/domain/payment" - "cls/pkg/logger" - "crypto/md5" - "encoding/hex" - "encoding/xml" - "fmt" - "sort" - "strings" -) - -// Config 微信支付配置 -type Config struct { - AppID string // 公众号ID - MchID string // 商户号 - APIKey string // API密钥 - NotifyURL string // 支付回调通知地址 -} - -// Service 微信支付服务 -type Service struct { - config Config - client *APIClient - log logger.Logger -} - -// NewService 创建微信支付服务 -func NewService(config Config, log logger.New) *Service { - return &Service{ - config: config, - client: NewAPIClient(config), - log: log("cls:payment:wechat"), - } -} - -// CreatePayment 创建支付订单 -func (s *Service) CreatePayment(order *payment.Payment) (map[string]interface{}, error) { - // 构建统一下单请求 - req := &UnifiedOrderRequest{ - Body: order.Description, - OutTradeNo: order.OrderNo, - TotalFee: fmt.Sprintf("%d", order.Amount), - } - - // 调用统一下单接口 - resp, err := s.client.UnifiedOrder(req) - if err != nil { - return nil, fmt.Errorf("创建支付订单失败: %v", err) - } - - // 检查响应结果 - if resp.ReturnCode != "SUCCESS" { - return nil, fmt.Errorf("微信支付接口返回错误: %s", resp.ReturnMsg) - } - - if resp.ResultCode != "SUCCESS" { - return nil, fmt.Errorf("微信支付业务处理失败: %s", resp.ErrCodeDes) - } - - // 返回支付二维码链接 - return map[string]interface{}{ - "code_url": resp.CodeURL, - }, nil -} - -// VerifyNotify 验证支付回调通知 -func (s *Service) VerifyNotify(notifyData string) (map[string]string, error) { - // 解析XML数据 - var notify struct { - ReturnCode string `xml:"return_code"` - ReturnMsg string `xml:"return_msg"` - ResultCode string `xml:"result_code"` - ErrCode string `xml:"err_code"` - ErrCodeDes string `xml:"err_code_des"` - OutTradeNo string `xml:"out_trade_no"` - TotalFee string `xml:"total_fee"` - TransactionID string `xml:"transaction_id"` - Sign string `xml:"sign"` - } - - if err := xml.Unmarshal([]byte(notifyData), ¬ify); err != nil { - return nil, fmt.Errorf("解析通知数据失败: %v", err) - } - - // 验证签名 - if !s.client.verifySign(¬ify) { - return nil, fmt.Errorf("签名验证失败") - } - - // 检查支付结果 - if notify.ReturnCode != "SUCCESS" || notify.ResultCode != "SUCCESS" { - return nil, fmt.Errorf("支付失败: %s", notify.ErrCodeDes) - } - - return map[string]string{ - "order_no": notify.OutTradeNo, - "amount": notify.TotalFee, - "transaction_id": notify.TransactionID, - }, nil -} - -// generateSign 生成签名 -func (s *Service) generateSign(params map[string]string) string { - // 按键排序 - var keys []string - for k := range params { - if k != "sign" && params[k] != "" { - keys = append(keys, k) - } - } - sort.Strings(keys) - - // 拼接字符串 - var builder strings.Builder - for i, k := range keys { - if i > 0 { - builder.WriteString("&") - } - builder.WriteString(k) - builder.WriteString("=") - builder.WriteString(params[k]) - } - builder.WriteString("&key=") - builder.WriteString(s.config.APIKey) - - // MD5加密 - h := md5.New() - h.Write([]byte(builder.String())) - return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) -} - -// verifySign 验证签名 -func (s *Service) verifySign(notifyData, sign string) bool { - // TODO: 实现签名验证逻辑 - return true -} diff --git a/internal/infrastructure/payment/wechat_pay/service.go b/internal/infrastructure/payment/wechat_pay/service.go new file mode 100644 index 0000000..b9f5bbd --- /dev/null +++ b/internal/infrastructure/payment/wechat_pay/service.go @@ -0,0 +1,137 @@ +package wechat_pay + +import ( + "context" + "errors" + "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/jsapi" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "log" + "net/http" +) + +// Config 微信支付配置 +type Config struct { + AppID string // 公众号ID + MchID string // 商户号 + APIKey string // API密钥 + NotifyURL string // 支付回调通知地址 +} + +// PayService 微信支付服务 +type PayService struct { + Client *core.Client + notifyHandler *notify.Handler + appId string + mchid string +} + +/* +NewPayService 传参说明 +arg1:商户号 +arg2:AppId +arg3:商户证书序列号 +arg4:商户APIv3密钥 +arg5:商户私钥 +arg6:商户公钥 +*/ +func NewPayService(arg1, arg2, arg3, arg4, arg5, arg6 string) *PayService { + // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(arg5) + if err != nil { + log.Fatal("load merchant private key error", err.Error()) + } + + cert, err := utils.LoadCertificateWithPath(arg6) + if err != nil { + log.Fatal("load merchant private key error", err.Error()) + } + fmt.Println("===============") + fmt.Println(cert.SerialNumber.String()) + fmt.Println("===============") + ctx := context.Background() + // 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(arg1, arg3, mchPrivateKey, arg4), + } + client, err := core.NewClient(ctx, opts...) + + if err != nil { + log.Fatal(err) + } + //err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, arg3, arg1, arg4) + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(arg1) + handler, err := notify.NewRSANotifyHandler(arg4, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) + if err != nil { + log.Fatal(err) + } + if err != nil { + log.Fatal(err) + } + return &PayService{client, handler, arg2, arg1} +} + +type PaymentInfo struct { + Description string + Attach string + AmountTotal int64 + OpenId string + OutTradeNo string +} + +func (p *PayService) CreatePayment(paymentInfo *PaymentInfo) (*jsapi.PrepayWithRequestPaymentResponse, error) { + svc := jsapi.JsapiApiService{Client: p.Client} + resp, result, err := svc.PrepayWithRequestPayment(context.Background(), + jsapi.PrepayRequest{ + Appid: core.String(p.appId), + Mchid: core.String(p.mchid), + Description: core.String(paymentInfo.Description), + OutTradeNo: core.String(paymentInfo.OutTradeNo), + Attach: core.String(paymentInfo.Attach), + NotifyUrl: core.String("http://famyun.com/api/order/order_notify"), + Amount: &jsapi.Amount{ + Total: core.Int64(paymentInfo.AmountTotal), + }, + Payer: &jsapi.Payer{ + Openid: core.String(paymentInfo.OpenId), + }, + }) + if err != nil { + return nil, err + } + if result.Response.StatusCode != 200 { + return nil, errors.New(resp.String()) + } + return resp, nil +} + +func (p *PayService) CancelPayment(orderId string) error { + svc := jsapi.JsapiApiService{Client: p.Client} + result, err := svc.CloseOrder(context.Background(), jsapi.CloseOrderRequest{ + OutTradeNo: core.String(orderId), + Mchid: core.String(p.mchid), + }) + if err != nil { + return err + } + if result.Response.StatusCode == 200 { + return nil + } else { + return errors.New("关闭订单失败") + } +} + +func (p *PayService) HandlerNotifyRequest(r *http.Request) (*payments.Transaction, error) { + content := new(payments.Transaction) + _, err := p.notifyHandler.ParseNotifyRequest(context.Background(), r, content) + if err != nil { + return nil, err + } + return content, nil +} diff --git a/internal/infrastructure/persistence/article/article_repo.go b/internal/infrastructure/persistence/article/article_repo.go index cc3c734..bc5d9a5 100644 --- a/internal/infrastructure/persistence/article/article_repo.go +++ b/internal/infrastructure/persistence/article/article_repo.go @@ -6,6 +6,7 @@ import ( "cls/pkg/util/page" "cls/pkg/xorm_engine" "errors" + "fmt" "xorm.io/builder" ) @@ -23,24 +24,17 @@ func (a ArticleRepositoryORM) Find(page *page.Page, conds []builder.Cond) error page.Desc = append(page.Desc, "ctime") //err := a.engine.Cls.Desc("ctime").Limit(page.PageSize, page.PageNumber*page.PageSize). // Find(page.Content, &article.LianV1Article{}) - - err := a.engine.FindAllCls(page, &article.LianV1Article{}, builder.And(conds...)) - if err != nil { - a.log.Error(err.Error()) - } - return err + return a.engine.FindAllCls(page, &article.LianV1Article{}, builder.And(conds...)) } func (a ArticleRepositoryORM) GetArticleById(id uint64) (*article.LianV1Article, error) { article := &article.LianV1Article{} has, err := a.engine.Cls.Where(builder.Eq{"id": id}).Get(article) if err != nil { - a.log.Error(err) return nil, err } if !has { - a.log.Errorf("未查询到文章【%d】", id) - return nil, errors.New("") + return nil, errors.New(fmt.Sprintf("未查询到文章【%d】", id)) } return article, nil } diff --git a/internal/infrastructure/persistence/auth/auth_repo.go b/internal/infrastructure/persistence/auth/auth_repo.go index 7ed8b71..b6e9f74 100644 --- a/internal/infrastructure/persistence/auth/auth_repo.go +++ b/internal/infrastructure/persistence/auth/auth_repo.go @@ -45,7 +45,7 @@ func (a AuthRepositoryORM) LoginByPassword(phone, password string) (string, erro if u.Password != password { return "", errors.New("密码错误") } - token, err := a.auth.GenerateUserToken(phone) + token, err := a.auth.GenerateUserToken(phone, u.Openid) if err != nil { a.log.Error(err.Error()) return "", err @@ -53,9 +53,9 @@ func (a AuthRepositoryORM) LoginByPassword(phone, password string) (string, erro return token, nil } -func (a AuthRepositoryORM) LoginByCaptcha(phone string) (string, error) { +func (a AuthRepositoryORM) LoginByCaptcha(phone, openid string) (string, error) { u := &domainUser.User{} - has, err := a.engine.Where(builder.Eq{"phone": phone}).Get(u) + has, err := a.engine.Where(builder.Eq{"phone": phone}.And(builder.Eq{"openid": openid})).Get(u) if err != nil { a.log.Error(err) return "", err @@ -65,6 +65,7 @@ func (a AuthRepositoryORM) LoginByCaptcha(phone string) (string, error) { u.Phone = phone u.GiftCount = 2 u.Status = 1 + u.Openid = openid _, err = a.engine.Insert(u) if err != nil { a.log.Error(err) @@ -75,7 +76,7 @@ func (a AuthRepositoryORM) LoginByCaptcha(phone string) (string, error) { return "", errors.New(fmt.Sprintf("用户【%d】禁止登录", u.Id)) } - token, err := a.auth.GenerateUserToken(phone) + token, err := a.auth.GenerateUserToken(phone, u.Openid) if err != nil { a.log.Error(err) } diff --git a/internal/infrastructure/persistence/order/aggregate_repo.go b/internal/infrastructure/persistence/order/aggregate_repo.go index ed70fe1..65caac1 100644 --- a/internal/infrastructure/persistence/order/aggregate_repo.go +++ b/internal/infrastructure/persistence/order/aggregate_repo.go @@ -1,6 +1,7 @@ package order import ( + "cls/internal/domain/coupon" "cls/internal/domain/order" "cls/internal/domain/payment" "cls/internal/domain/purchase" @@ -33,76 +34,137 @@ func (r *AggregateRepositoryORM) Save(aggregate *order.OrderAggregate) error { defer session.Close() if err := session.Begin(); err != nil { - r.log.Error(err) return err } // 保存订单 if _, err := session.Insert(aggregate.Order); err != nil { - r.log.Error(err) return err } // 保存支付订单 if aggregate.Payment != nil { if _, err := session.Insert(aggregate.Payment); err != nil { - r.log.Error(err) return err } } - // 保存购买记录 - if aggregate.Purchase != nil { - if _, err := session.Insert(aggregate.Purchase); err != nil { - session.Rollback() - r.log.Error(err) - return err + if aggregate.Coupon != nil { + if aggregate.Coupon.ID > 0 { + _, err := session.Where(builder.Eq{"id": aggregate.Coupon.ID}).Cols("status").Update(aggregate.Coupon) + if err != nil { + return err + } } + } if err := session.Commit(); err != nil { - r.log.Error(err) return err } return nil } +func (r *AggregateRepositoryORM) PaySuccess(aggregate *order.OrderAggregate) error { + session := r.engine.NewSession() + defer session.Close() + if err := session.Begin(); err != nil { + return err + } + _, err := session.Where(builder.Eq{"id": aggregate.Payment.ID}. + And(builder.Eq{"order_no": aggregate.Payment.OrderNo})).Cols("status", "transaction_id", "notify_data").Update(aggregate.Payment) + if err != nil { + return err + } + + _, err = session.Where(builder.Eq{"id": aggregate.Order.ID}. + And(builder.Eq{"order_no": aggregate.Order.OrderNo})).Cols("status").Update(aggregate.Order) + if err != nil { + return err + } + _, err = session.Insert(aggregate.Purchase) + if err != nil { + return err + } + err = session.Commit() + return err +} + +func (r *AggregateRepositoryORM) PayFailed(aggregate *order.OrderAggregate) error { + session := r.engine.NewSession() + defer session.Close() + if err := session.Begin(); err != nil { + return err + } + _, err := session.Where(builder.Eq{"id": aggregate.Payment.ID}. + And(builder.Eq{"order_no": aggregate.Payment.OrderNo})).Cols("status", "transaction_id", "notify_data").Update(aggregate.Payment) + if err != nil { + return err + } + + _, err = session.Where(builder.Eq{"id": aggregate.Order.ID}. + And(builder.Eq{"order_no": aggregate.Order.OrderNo})).Cols("status").Update(aggregate.Order) + if err != nil { + return err + } + + if aggregate.Coupon != nil { + if aggregate.Coupon.ID > 0 { + //回退优惠券 + _, err = session.Where(builder.Eq{"id": aggregate.Coupon.ID}).Cols("status").Update(aggregate.Coupon) + if err != nil { + return err + } + } + } + + err = session.Commit() + return err +} + // GetByOrderNo 根据订单号获取订单聚合根 func (r *AggregateRepositoryORM) GetByOrderNo(orderNo string) (*order.OrderAggregate, error) { // 获取订单 o := &order.Order{} has, err := r.engine.Where(builder.Eq{"order_no": orderNo}).Get(o) if err != nil { - r.log.Error(err) return nil, err } if !has { return nil, errors.New("订单不存在") } + aggregate := order.NewOrderAggregate(o) // 获取支付订单 p := &payment.Payment{} has, err = r.engine.Where(builder.Eq{"order_no": orderNo}).Get(p) if err != nil { - r.log.Error(err) return nil, err } + if has { + aggregate.Payment = p + } // 获取购买记录 pu := &purchase.Purchase{} has, err = r.engine.Where(builder.Eq{"user_id": o.UserID, "content_id": o.TargetID}).Get(pu) if err != nil { - r.log.Error(err) return nil, err } - - aggregate := order.NewOrderAggregate(o) if has { - aggregate.Payment = p aggregate.Purchase = pu } + // 获取优惠券 + co := &coupon.Coupon{} + has, err = r.engine.Where(builder.Eq{"id": o.Coupon}).Get(co) + if err != nil { + return nil, err + } + if has { + aggregate.Coupon = co + } return aggregate, nil } diff --git a/internal/infrastructure/persistence/order/order_repo.go b/internal/infrastructure/persistence/order/order_repo.go index 4059e32..d79051e 100644 --- a/internal/infrastructure/persistence/order/order_repo.go +++ b/internal/infrastructure/persistence/order/order_repo.go @@ -107,3 +107,12 @@ func (r *OrderRepositoryORM) Delete(id uint64) error { } return nil } + +func (r *OrderRepositoryORM) GetPendingOrder(targetId uint64, t order.OrderType, uid uint64) (*order.Order, error) { + o := &order.Order{} + _, err := r.engine.Where(builder.Eq{"target_id": targetId}. + And(builder.Eq{"type": t}). + And(builder.Eq{"user_id": uid}). + And(builder.Eq{"status": order.OrderStatusPending})).Get(o) + return o, err +} diff --git a/internal/infrastructure/wechat/wechat.go b/internal/infrastructure/wechat/wechat.go new file mode 100644 index 0000000..894cab7 --- /dev/null +++ b/internal/infrastructure/wechat/wechat.go @@ -0,0 +1,110 @@ +package wechat + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "io" + "net/http" +) + +type WechatService struct { + AppId string + AppSecret string + client *http.Client +} + +func NewWechatService(appId, appSecret string) *WechatService { + return &WechatService{appId, appSecret, nil} +} + +func (ws *WechatService) GetOpenID(code string) (string, error) { + url := fmt.Sprintf( + "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", + ws.AppId, + ws.AppSecret, + code, + ) + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + var res struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + OpenID string `json:"openid"` + Scope string `json:"scope"` + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + } + + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return "", errors.New("Failed to decode response") + } + fmt.Printf("%+v\n", res) + if res.ErrCode != 0 { + return "", errors.New(fmt.Sprintf("WeChat error: %s", res.ErrMsg)) + } + + // 🎯 获取到 openid + return res.OpenID, nil +} + +func (ws *WechatService) GetAccessToken() (string, error) { + url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", ws.AppId, ws.AppSecret) + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", err + } + + // 获取 access_token + accessToken, ok := result["access_token"].(string) + if !ok { + return "", errors.New("获取 access_token 失败") + } + + return accessToken, nil +} + +func (ws *WechatService) GetJsapiTicket(accessToken string) (string, error) { + url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi", accessToken) + + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", err + } + + // 获取 jsapi_ticket + jsapiTicket, ok := result["ticket"].(string) + if !ok { + return "", errors.New("获取 jsapi_ticket 失败") + } + + return jsapiTicket, nil +} + +func (ws *WechatService) SignWxConfig(signStr string) (string, error) { + h := sha1.New() + _, err := io.WriteString(h, signStr) + if err != nil { + return "", err + } + signature := hex.EncodeToString(h.Sum(nil)) + return signature, nil +} diff --git a/internal/interfaces/auth/auth_handler.go b/internal/interfaces/auth/auth_handler.go index a6ae788..6d495bc 100644 --- a/internal/interfaces/auth/auth_handler.go +++ b/internal/interfaces/auth/auth_handler.go @@ -51,15 +51,14 @@ func (h *AuthHandler) GetSmsCaptcha(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"}) return } - // 从 JWT token 中获取用户标识 - username := jwtfx.GetUserIdentifier(c) - if username == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "未获取到用户标识"}) + + openid := jwtfx.GetOpenid(c) + if openid == "" { + c.JSON(http.StatusInternalServerError, "openid为空") return } - // 生成短信验证码 - captcha, err := h.captchaService.GenerateSmsCaptcha(username, req.Phone) + captcha, err := h.captchaService.GenerateSmsCaptcha(req.Phone) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -119,7 +118,8 @@ func (h *AuthHandler) LoginCaptcha(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - token, err := h.service.LoginByCaptcha(req.Phone) + + token, err := h.service.LoginByCaptcha(req.Phone, jwtfx.GetOpenid(c)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return diff --git a/internal/interfaces/http/column_handler.go b/internal/interfaces/http/column_handler.go index 0519ecb..d02cfda 100644 --- a/internal/interfaces/http/column_handler.go +++ b/internal/interfaces/http/column_handler.go @@ -1 +1 @@ - \ No newline at end of file +package http diff --git a/internal/interfaces/order/order_handler.go b/internal/interfaces/order/order_handler.go index c3f98a9..181e3cb 100644 --- a/internal/interfaces/order/order_handler.go +++ b/internal/interfaces/order/order_handler.go @@ -2,19 +2,22 @@ package order import ( "cls/internal/application/order" + "cls/internal/infrastructure/payment/wechat_pay" "cls/internal/interfaces" + "cls/pkg/jwtfx" "cls/pkg/logger" "github.com/gin-gonic/gin" "net/http" ) type OrderHandler struct { - service *order.OrderService - log logger.Logger + service *order.OrderService + payService *wechat_pay.PayService + log logger.Logger } -func NewOrderHandler(service *order.OrderService, log logger.New) *OrderHandler { - return &OrderHandler{service, log("cls:interfaces:order")} +func NewOrderHandler(service *order.OrderService, payService *wechat_pay.PayService, log logger.New) *OrderHandler { + return &OrderHandler{service, payService, log("cls:interfaces:order")} } var _ interfaces.Handler = (*OrderHandler)(nil) @@ -23,6 +26,8 @@ func (o *OrderHandler) RegisterRouters(app gin.IRouter) { auth := app.Group("/order") { auth.POST("/create", o.create) + auth.POST("/order_notify", o.orderNotify) + auth.POST("/cancel", o.orderCancel) } } @@ -34,10 +39,42 @@ func (o *OrderHandler) create(c *gin.Context) { c.AbortWithStatus(http.StatusInternalServerError) return } - data, err := o.service.CreateOrder(dto) + + data, err := o.service.CreateOrder(dto, jwtfx.GetUserIdentifier(c)) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) } else { c.AbortWithStatusJSON(http.StatusOK, data) } } + +func (o *OrderHandler) orderNotify(c *gin.Context) { + payment, err := o.payService.HandlerNotifyRequest(c.Request) + if err != nil { + o.log.Error(err) + c.AbortWithStatus(http.StatusNoContent) + return + } + if err = o.service.OrderNotify(payment); err != nil { + o.log.Error(err) + c.AbortWithStatus(http.StatusNoContent) + } else { + c.AbortWithStatus(http.StatusOK) + } +} + +func (o *OrderHandler) orderCancel(c *gin.Context) { + dto := &order.CreateOrderRequest{} + err := c.ShouldBindJSON(dto) + if err != nil { + o.log.Error(err.Error()) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + err = o.service.CancelOrder(dto, jwtfx.GetUserIdentifier(c)) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } else { + c.AbortWithStatus(http.StatusOK) + } +} diff --git a/internal/interfaces/purchase/purchase_handler.go b/internal/interfaces/purchase/purchase_handler.go index 7cfa13e..71275a5 100644 --- a/internal/interfaces/purchase/purchase_handler.go +++ b/internal/interfaces/purchase/purchase_handler.go @@ -8,11 +8,11 @@ import ( ) type PurchaseHandler struct { - purchaseService *purchase.Service + purchaseService *purchase.PurchaseService log logger.Logger } -func NewPurchaseHandler(service *purchase.Service, logger logger.New) *PurchaseHandler { +func NewPurchaseHandler(service *purchase.PurchaseService, logger logger.New) *PurchaseHandler { return &PurchaseHandler{ purchaseService: service, log: logger("panoramic:interfaces:purchase"), @@ -23,7 +23,7 @@ var _ interfaces.Handler = (*PurchaseHandler)(nil) // Register 注册路由 func (h *PurchaseHandler) RegisterRouters(r gin.IRouter) { - purchaseGroup := r.Group("/purchases") + purchaseGroup := r.Group("/purchase") { purchaseGroup.POST("/save", h.CreatePurchase) purchaseGroup.GET("/user/:userId", h.GetUserPurchases) diff --git a/internal/modules/purchase_module.go b/internal/modules/purchase_module.go index f459c41..31ae65e 100644 --- a/internal/modules/purchase_module.go +++ b/internal/modules/purchase_module.go @@ -11,6 +11,6 @@ import ( var PurchaseModule = fx.Module("PurchaseModule", fx.Provide( interfaces.AsHandler(purchase.NewPurchaseHandler), - service.NewService, + service.NewPurchaseService, repo.NewPurchaseRepositoryORM, )) diff --git a/pkg/jwtfx/claims.go b/pkg/jwtfx/claims.go index d21c62a..bab1b43 100644 --- a/pkg/jwtfx/claims.go +++ b/pkg/jwtfx/claims.go @@ -47,6 +47,25 @@ func GetUserIdentifier(c *gin.Context) string { return "" } +func GetOpenid(c *gin.Context) string { + value, exists := c.Get(JWTClaimsKey) + if !exists { + return "" + } + + claims, ok := value.(map[string]interface{}) + if !ok { + return "" + } + + if claims["openid"] != nil { + openid := claims["openid"].(string) + return openid + } + return "" + +} + // IsGuest 判断当前用户是否为游客 func IsGuest(c *gin.Context) bool { value, exists := c.Get(JWTClaimsKey) diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index aa86248..74d0db0 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; -import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ - { - path: '', - redirectTo: 'home', - pathMatch: 'full' - }, + // { + // path: '', + // redirectTo: 'wechat_pay-callback', + // pathMatch: 'full' + // }, { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomePageModule) @@ -16,6 +16,8 @@ const routes: Routes = [ loadChildren: () => import('./mine/mine.module').then(m => m.MinePageModule) }, + + ]; @NgModule({ diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 622bea3..9ee1354 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit } from '@angular/core'; -import { Router, NavigationEnd } from '@angular/router'; +import { Component, OnInit} from '@angular/core'; +import {Router, NavigationEnd, ActivatedRoute} from '@angular/router'; @Component({ selector: 'app-root', @@ -9,20 +9,61 @@ import { Router, NavigationEnd } from '@angular/router'; }) export class AppComponent implements OnInit { showTabs = true; + private isFirstLoad = true; - constructor(private router: Router) {} + constructor(private route: ActivatedRoute, + private router: Router, + ) { + } ngOnInit() { + // 监听路由变化 this.router.events.subscribe((event) => { if (event instanceof NavigationEnd) { // 检查当前路由是否应该显示底部导航栏 this.showTabs = this.shouldShowTabs(event.url); + + // 只在首次加载时检查token + if (this.isFirstLoad) { + this.checkToken(); + this.isFirstLoad = false; + } } }); } + private checkToken() { + // 先检查URL中是否有token参数 + this.route.queryParamMap.subscribe(params => { + const token = params.get('token'); + + if (token) { + // 如果有token参数,保存并导航到首页 + localStorage.setItem("token", token); + this.showTabs = true; + this.router.navigateByUrl("/home"); + } else { + // 如果没有token参数,检查localStorage + const storedToken = localStorage.getItem("token"); + if (storedToken) { + this.router.navigateByUrl("/home"); + } else { + // 如果都没有token,重定向到获取token的页面 + console.log("需要获取token,重定向到授权页面"); + // window.location.href = "http://famyun.com/callback_test"; + // 生产环境使用微信授权 + window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx13bd75fbedf283e5&redirect_uri=http%3A%2F%2Ffamyun.com%2Fcallback&response_type=code&scope=snsapi_base&state=123#wechat_redirect"; + } + } + }); + } + + fetchOpenid() { + // 保留此方法以备将来使用 + } + private shouldShowTabs(url: string): boolean { // 在首页和我的页面显示底部导航栏 - return url=== '/' || url === '/home' || url === '/mine' || url === '/mine/login'; + return url === '/' || url === '/home' || url === '/mine' || url === '/mine/login'; } } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 939e12c..3b4d031 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -9,6 +9,7 @@ import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import {AuthInterceptor} from "./core/interceptors/auth.interceptor"; import {CheckInterceptor} from "./core/interceptors/check.interceptor"; +import {ErrorInterceptor} from "./core/interceptors/error.interceptor"; @NgModule({ declarations: [ @@ -31,7 +32,7 @@ import {CheckInterceptor} from "./core/interceptors/check.interceptor"; }, { provide: HTTP_INTERCEPTORS, - useClass: CheckInterceptor, + useClass: ErrorInterceptor, multi: true, }, ], diff --git a/ui/src/app/core/interceptors/auth.interceptor.ts b/ui/src/app/core/interceptors/auth.interceptor.ts index 272ba69..753f230 100644 --- a/ui/src/app/core/interceptors/auth.interceptor.ts +++ b/ui/src/app/core/interceptors/auth.interceptor.ts @@ -8,10 +8,11 @@ import { } from '@angular/common/http'; import { Observable, tap } from 'rxjs'; import { AuthConfigConsts, tokenNotExpired } from "../../mine/auth.jwt"; +import {Router} from "@angular/router"; @Injectable() export class AuthInterceptor implements HttpInterceptor { - constructor() {} + constructor(private router:Router) {} //从请求头获取token intercept(req: HttpRequest, next: HttpHandler): Observable> { const token = localStorage.getItem('token'); @@ -22,21 +23,25 @@ export class AuthInterceptor implements HttpInterceptor { const authReq = req.clone({headers: authHeader}); return next.handle(authReq) } else { + // window.location.href="http://famyun.com/callback_test" + // console.log("路由") const authReq = req.clone({headers: authHeader}); return next.handle(authReq).pipe( tap((event) => { // 如果是 HTTP 响应 - if (event instanceof HttpResponse) { - // 从响应头中获取 Authorization token - const authToken = event.headers.get('Authorization'); - if (authToken && (!token || token !== authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''))) { - // 去掉 Bearer 前缀 - const newToken = authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''); - // 保存到 sessionStorage - localStorage.setItem('token', newToken); - } - } + + // if (event instanceof HttpResponse) { + // // 从响应头中获取 Authorization token + // const authToken = event.headers.get('Authorization'); + // + // if (authToken && (!token || token !== authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''))) { + // // 去掉 Bearer 前缀 + // const newToken = authToken.replace(AuthConfigConsts.HEADER_PREFIX_BEARER, ''); + // // 保存到 sessionStorage + // localStorage.setItem('token', newToken); + // } + // } }) ); } diff --git a/ui/src/app/core/interceptors/error.interceptor.ts b/ui/src/app/core/interceptors/error.interceptor.ts new file mode 100644 index 0000000..cc3a716 --- /dev/null +++ b/ui/src/app/core/interceptors/error.interceptor.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; +import {Observable, throwError} from 'rxjs'; +import {catchError} from 'rxjs/operators'; + +@Injectable() +export class ErrorInterceptor implements HttpInterceptor { + + constructor() { + } + + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + return next.handle(request) + .pipe( + catchError((error: HttpErrorResponse) => { + if (!(error.error instanceof ErrorEvent)) { + if (error.status === 401) { + console.log("401了") + localStorage.removeItem("token") + localStorage.removeItem("giftCount") + } + } + return throwError(error); + }) + ); + } +} diff --git a/ui/src/app/home/article-buy/article-buy.page.html b/ui/src/app/home/article-buy/article-buy.page.html index 971efc2..f20f5fe 100644 --- a/ui/src/app/home/article-buy/article-buy.page.html +++ b/ui/src/app/home/article-buy/article-buy.page.html @@ -26,7 +26,7 @@
单篇解锁 - {{(price.discount || 0.3) * 10}}折 + {{(price.discount || 0.3) * 10}}折
¥{{price.amount/100 || 1888}} @@ -55,7 +55,7 @@
- 确认支付 ¥{{getDiscountPriceAndCoupon(price.amount, price.discount)}} + 确认支付 ¥{{(getDiscountPriceAndCoupon(price.amount, price.discount) - this.couponAmount).toFixed(2)}} diff --git a/ui/src/app/home/article-buy/article-buy.page.ts b/ui/src/app/home/article-buy/article-buy.page.ts index aab97a1..659cbbb 100644 --- a/ui/src/app/home/article-buy/article-buy.page.ts +++ b/ui/src/app/home/article-buy/article-buy.page.ts @@ -1,5 +1,5 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {NavigationEnd, Router} from '@angular/router'; import { NavController, ModalController } from '@ionic/angular'; import { MoreDiscountsComponent } from '../component/more-discounts/more-discounts.component'; import { Price } from '../../shared/model/price'; @@ -8,6 +8,7 @@ import {Article} from "../../shared/model/article"; import {Coupon} from "../../shared/model/coupon"; import {HomeService} from "../home.service"; import {c} from "@angular/core/navigation_types.d-u4EOrrdZ"; +import {Subscription} from "rxjs"; @Component({ @@ -16,12 +17,13 @@ import {c} from "@angular/core/navigation_types.d-u4EOrrdZ"; styleUrls: ['./article-buy.page.scss'], standalone: false, }) -export class ArticleBuyPage implements OnInit { +export class ArticleBuyPage implements OnInit,OnDestroy { article!: Article; price!: Price; coupons: Coupon[] = []; couponAmount:number = 0; selectCoupon:Coupon|null = null + private routerSub!: Subscription; constructor( private router: Router, @@ -42,12 +44,43 @@ export class ArticleBuyPage implements OnInit { ngOnInit() { this.loadCoupons() + this.routerSub = this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + if(event.url == "/home/article-buy") { + this.loadArticle() + } + } + }); + } + ngOnDestroy() { + if (this.routerSub) { + this.routerSub.unsubscribe(); + } + } + + loadArticle(){ + console.log("检查订单",this.article.eventId) + this.homeService.getArticleDetail(this.article.eventId).subscribe((res)=>{ + console.log(res) + if(res.eventId == this.article.eventId) { + this.homeService.setBuyId(res.eventId) + this.homeService.set(res.eventId+'',res) + this.navCtrl.back() + } + }) } + loadCoupons(){ this.homeService.getCouponList().subscribe(res=>{ if(res) { res.forEach(res=>{ - if(this.getDiscountPrice(this.price.amount,this.price.discount) > res.minAmount){ + console.log("comp ====>") + console.log(res.minAmount) + console.log(this.getDiscountPrice(this.price.amount,this.price.discount)) + console.log("end===<") + res.minAmount /= 100 + res.value /= 100 + if(this.getDiscountPrice(this.price.amount,this.price.discount) >= res.minAmount){ res.canUse = true } }) @@ -57,11 +90,17 @@ export class ArticleBuyPage implements OnInit { } getDiscountPrice(originalPrice: number,discount:number): number { if (!originalPrice) return 0; - return +(originalPrice/100 * discount).toFixed(1) ; // 3折 + if (originalPrice<10) { + return originalPrice/100 + } + return +(originalPrice/100 * discount).toFixed(2) ; // 3折 } getDiscountPriceAndCoupon(originalPrice: number,discount:number): number { if (!originalPrice) return 0; - return +(originalPrice/100 * discount).toFixed(1) - this.couponAmount; // 3折 + if(originalPrice<10) { + return originalPrice/100 + } + return +(originalPrice/100 * discount ) .toFixed(2) ; // 3折 } couponSelected(coupon:Coupon|null){ this.selectCoupon = coupon; @@ -84,8 +123,13 @@ export class ArticleBuyPage implements OnInit { } submitOrder() { // TODO: 实现订单提交功能 + this.navCtrl.navigateForward('/home/confirm-order',{ - state:{order:{targetId:this.article.eventId,type:OrderType.OrderTypeArticle,amount:this.getDiscountPrice(this.price.amount,this.price.discount)}} + state:{order:{ + targetId:this.article.eventId, + coupon:this.selectCoupon?.id, + type:OrderType.OrderTypeArticle, + amount:(this.getDiscountPrice(this.price.amount,this.price.discount) - this.couponAmount).toFixed(2)}} }) } } diff --git a/ui/src/app/home/article-detail/article-detail.page.html b/ui/src/app/home/article-detail/article-detail.page.html index 3d30e26..e2ba861 100644 --- a/ui/src/app/home/article-detail/article-detail.page.html +++ b/ui/src/app/home/article-detail/article-detail.page.html @@ -15,7 +15,7 @@ @@ -33,7 +33,7 @@ {{stock.trim()}} - {{formatChange(stockChanges[stock.trim()])}} diff --git a/ui/src/app/home/article-detail/article-detail.page.ts b/ui/src/app/home/article-detail/article-detail.page.ts index e0cb48b..8b6c776 100644 --- a/ui/src/app/home/article-detail/article-detail.page.ts +++ b/ui/src/app/home/article-detail/article-detail.page.ts @@ -91,9 +91,9 @@ export class ArticleDetailPage implements OnInit, AfterViewInit { private getStockChanges() { if (!this.article?.stocks) return; - + const stockCodes = this.article.stocks.split(',').map(code => code.trim()); - + stockCodes.forEach(code => { if (code) { // 调用接口时使用纯数字代码 diff --git a/ui/src/app/home/column-buy/column-buy.page.ts b/ui/src/app/home/column-buy/column-buy.page.ts index 668be16..9901d88 100644 --- a/ui/src/app/home/column-buy/column-buy.page.ts +++ b/ui/src/app/home/column-buy/column-buy.page.ts @@ -1,12 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import { Column } from '../../shared/model/column'; -import { Router } from '@angular/router'; +import {NavigationEnd, Router} from '@angular/router'; import { Price } from '../../shared/model/price'; import { NavController, ModalController } from "@ionic/angular"; import { MoreDiscountsComponent } from '../component/more-discounts/more-discounts.component'; import {Coupon} from "../../shared/model/coupon"; import {HomeService} from "../home.service"; import {OrderType} from "../../shared/model/order"; +import {Subscription} from "rxjs"; @Component({ selector: 'app-column-buy', @@ -14,7 +15,7 @@ import {OrderType} from "../../shared/model/order"; styleUrls: ['./column-buy.page.scss'], standalone: false, }) -export class ColumnBuyPage implements OnInit { +export class ColumnBuyPage implements OnInit,OnDestroy { column!: Column; // 专栏信息 columnPrice!: Price; // 价格信息 coupons: Coupon[] = []; @@ -22,6 +23,7 @@ export class ColumnBuyPage implements OnInit { selectCoupon:Coupon|null = null selectedPeriod: '1' | '3' | '6'| '12' = '1'; // 选中的订阅周期 recommendCode: string = ''; // 推荐码 + private routerSub!: Subscription; constructor( private navCtrl: NavController, @@ -45,23 +47,49 @@ export class ColumnBuyPage implements OnInit { ngOnInit() { this.loadCoupons() + this.routerSub = this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + if(event.url == "/home/column-buy") { + this.loadColumn() + } + } + }); + } + ngOnDestroy() { + if (this.routerSub) { + this.routerSub.unsubscribe(); + } + } + loadColumn(){ + this.homeService.getColumnByName(this.column.title).subscribe((res)=>{ + console.log(res) + if(res.id == this.column.id && res.unlock) { + this.homeService.set(res.id+'',res.unlock) + this.navCtrl.back() + } + }) } - // 计算每日价格 getDailyPrice(monthPrice: number, months: number): number { if (!monthPrice) return 0; - return +(monthPrice/100 / (months * 30)).toFixed(1); + return +(monthPrice/100 / (months * 30)).toFixed(2); } // 获取折扣价格 getDiscountPrice(originalPrice: number,discount:number): number { if (!originalPrice) return 0; - return +(originalPrice/100 * discount).toFixed(1) ; // 3折 + if (originalPrice<10) { + return originalPrice/100 + } + return +(originalPrice/100 * discount).toFixed(2) ; // 3折 } getDiscountPriceAndCoupon(originalPrice: number,discount:number): number { if (!originalPrice) return 0; - return +( originalPrice/100 * discount).toFixed(1) ; // 3折 + if (originalPrice<10) { + return originalPrice/100 + } + return +( originalPrice/100 * discount).toFixed(2) ; // 3折 } // 选择订阅周期 @@ -88,10 +116,15 @@ export class ColumnBuyPage implements OnInit { } loadCoupons(){ this.homeService.getCouponList().subscribe(res=>{ + res.forEach(res=>{ + res.minAmount /= 100 + res.value /= 100 + }) this.coupons = res; }) } couponSelected(coupon:Coupon|null){ + console.log(coupon) this.selectCoupon = coupon; if(coupon) { this.couponAmount = coupon.value; @@ -101,9 +134,15 @@ export class ColumnBuyPage implements OnInit { } // 提交订单 submitOrder() { + console.log(this.selectCoupon) // TODO: 实现订单提交逻辑 this.navCtrl.navigateForward('/home/confirm-order',{ - state:{order:{targetId:this.column.id,type:OrderType.OrderTypeColumn,amount:this.getDiscountPriceAndCoupon(this.getSelectedPrice()|| 0,this.columnPrice.discount) -this.couponAmount}} + state:{order:{ + targetId:this.column.id, + coupon:this.selectCoupon?.id, + type:OrderType.OrderTypeColumn, + duration:parseInt(this.selectedPeriod), + amount:(this.getDiscountPriceAndCoupon(this.getSelectedPrice()|| 0,this.columnPrice.discount) - this.couponAmount).toFixed(2)}} }) } diff --git a/ui/src/app/home/component/article-content/article-content.component.html b/ui/src/app/home/component/article-content/article-content.component.html index 7668a8d..3a1b2dc 100644 --- a/ui/src/app/home/component/article-content/article-content.component.html +++ b/ui/src/app/home/component/article-content/article-content.component.html @@ -1,11 +1,11 @@ - + 最新 - + 已解锁 - + 免费试读 @@ -25,7 +25,7 @@ -
+

暂无已解锁文章

diff --git a/ui/src/app/home/component/article-content/article-content.component.ts b/ui/src/app/home/component/article-content/article-content.component.ts index 095f858..4e0996f 100644 --- a/ui/src/app/home/component/article-content/article-content.component.ts +++ b/ui/src/app/home/component/article-content/article-content.component.ts @@ -1,6 +1,6 @@ -import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectorRef, Component, Input, OnDestroy, OnInit} from '@angular/core'; import {Article} from "../../../shared/model/article"; -import {InfiniteScrollCustomEvent} from "@ionic/angular"; +import {InfiniteScrollCustomEvent, NavController} from "@ionic/angular"; import {HomeService} from "../../home.service"; import {getGiftCount, getUser} from "../../../mine/mine.service"; import {Subject, Subscription} from "rxjs"; @@ -13,7 +13,7 @@ import {takeUntil} from "rxjs/operators"; styleUrls: ['./article-content.component.scss'], standalone:false, }) -export class ArticleContentComponent implements OnInit { +export class ArticleContentComponent implements OnInit,OnDestroy { searchParams: { [param: string]: any } = { page: 0, size: 10, @@ -25,23 +25,83 @@ export class ArticleContentComponent implements OnInit { data:Article[] = [] currentSelect = "new" selectBtn:string = "new" + private routerSub!: Subscription; private destroy$ = new Subject(); constructor(private homeService:HomeService, + private router: Router, + private navCtrl: NavController, private cdr:ChangeDetectorRef) { } ngOnInit() { this.getData(); this.getUsername(); + this.routerSub = this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + if(event.url == "/home/special-column" || event.url == "/home") { + this.checkOrderOcc() + } + } + }); } - ionChange(e:any){ + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + if (this.routerSub) { + this.routerSub.unsubscribe(); + } } - select(v:string) { - if(v==this.selectBtn){ + + checkOrderOcc(){ + const unlock = this.homeService.get(this.className) + if(unlock) { + this.loadData() + } + const uid = this.homeService.getBuyId() + if(uid>0) { + const articleInfo = this.homeService.get(uid+'') + if(articleInfo){ + if( articleInfo.eventId == uid){ + this.data.forEach((res)=>{ + if(res.eventId == articleInfo.eventId) { + res.stocks = articleInfo.stocks + res.content = articleInfo.content + res.unlock = true + this.cdr.detectChanges() + localStorage.removeItem("articleInfo") + this.navCtrl.navigateForward('/home/article-detail', { + state: {article: res} + }); + return + } + }) + } + } + } + + + + } + ionChange(e:any){ + // this.searchParams['page'] = 0 + // this.data = [] + // this.currentSelect = e.detail.value + // this.getData() + if(e.detail.value==this.selectBtn){ return } + this.data = [] + this.hasMore = false; + this.selectBtn = e.detail.value + this.searchParams['page'] = 0 + this.getData() + } + select(v:string) { + + } + + loadData(){ this.hasMore = false; - this.selectBtn = v this.searchParams['page'] = 0 this.data = [] this.destroy$.next() @@ -64,7 +124,6 @@ export class ArticleContentComponent implements OnInit { default: return this.getNewData() } - } getUsername(){ @@ -89,21 +148,24 @@ export class ArticleContentComponent implements OnInit { }) } getUnLockData() { - this.getUsername() - if(this.username!= ""){ - this.homeService.unlockList(this.searchParams).pipe(takeUntil(this.destroy$)).subscribe(res=>{ - if(res.items) { - if(res.items.length > 0) { - this.data = this.data.concat(res.items) - this.searchParams['page'] = this.searchParams['page']+1; - this.hasMore = res.items.length === this.searchParams['size']; - this.cdr.detectChanges(); - } else { - this.hasMore = false; + getUser().subscribe((res)=>{ + this.username = res.username + if(this.username!= ""){ + this.homeService.unlockList(this.searchParams).pipe(takeUntil(this.destroy$)).subscribe(res=>{ + if(res.items) { + if(res.items.length > 0) { + this.data = this.data.concat(res.items) + this.searchParams['page'] = this.searchParams['page']+1; + this.hasMore = res.items.length === this.searchParams['size']; + this.cdr.detectChanges(); + } else { + this.hasMore = false; + } } - } - }) - } + }) + } + }) + } getFreeData() { diff --git a/ui/src/app/home/component/article-item/article-item.component.ts b/ui/src/app/home/component/article-item/article-item.component.ts index 0dee74c..edc865c 100644 --- a/ui/src/app/home/component/article-item/article-item.component.ts +++ b/ui/src/app/home/component/article-item/article-item.component.ts @@ -5,7 +5,6 @@ import {getGiftCount, getUser, useGiftCount} from "../../../mine/mine.service"; import {HomeService} from "../../home.service"; import {Router} from "@angular/router"; -import {debounceTime, Subject, takeUntil} from "rxjs"; @Component({ selector: 'app-article-item', diff --git a/ui/src/app/home/component/coupon-list/coupon-list.component.html b/ui/src/app/home/component/coupon-list/coupon-list.component.html index ad223f3..e5c7cd1 100644 --- a/ui/src/app/home/component/coupon-list/coupon-list.component.html +++ b/ui/src/app/home/component/coupon-list/coupon-list.component.html @@ -1,8 +1,8 @@

优惠券

- {{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}} -
+ {{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}} +
暂无可用优惠券
diff --git a/ui/src/app/home/component/coupon-list/coupon-list.component.ts b/ui/src/app/home/component/coupon-list/coupon-list.component.ts index 32a5d41..b460214 100644 --- a/ui/src/app/home/component/coupon-list/coupon-list.component.ts +++ b/ui/src/app/home/component/coupon-list/coupon-list.component.ts @@ -1,6 +1,6 @@ import {Component, OnInit, Output, EventEmitter, AfterViewInit, Input} from '@angular/core'; import { HomeService } from '../../home.service'; -import { Coupon } from 'src/app/shared/model/coupon'; +import {Coupon, CouponStatus} from 'src/app/shared/model/coupon'; import { Observable } from 'rxjs'; @Component({ @@ -13,6 +13,7 @@ export class CouponListComponent implements OnInit,AfterViewInit { @Input() coupons: Coupon[] = []; @Input() amount: number = 0; couponAmount:number = 0 + couponStatus = CouponStatus selectedCouponId: string | null = null; @Output() couponSelected = new EventEmitter(); @@ -26,7 +27,7 @@ export class CouponListComponent implements OnInit,AfterViewInit { onCouponSelect(event: any) { const selectedCoupon = this.coupons.find(c => c.id === event.detail.value); - if (selectedCoupon && selectedCoupon.minAmount < this.amount) { + if (selectedCoupon && selectedCoupon.minAmount <= this.amount) { this.couponAmount = selectedCoupon.value; this.couponSelected.emit(selectedCoupon); } else { @@ -40,22 +41,34 @@ export class CouponListComponent implements OnInit,AfterViewInit { return this.selectedCouponId === coupon.id; } isCouponAvailable(coupon: Coupon): boolean { + let f = true // 检查优惠券是否可用 - if (coupon.status !== 'ACTIVE') { - return false; + if (coupon.status === this.couponStatus.ACTIVE) { + console.log("false 1") + f = false + } + // 检查订单金额是否满足优惠券使用条件 + + console.log("check order amount ====>") + console.log(coupon.minAmount) + console.log(this.amount) + console.log("end") if (this.amount < coupon.minAmount) { - return false; + console.log("false 2") + f = false + // return false; } // 检查优惠券是否在有效期内 - const now = new Date(); - if (now < new Date(coupon.startTime) || now > new Date(coupon.endTime)) { - return false; - } - - return true; + // const now = new Date(); + // if (now < new Date(coupon.startTime) || now > new Date(coupon.endTime)) { + // + // return false; + // } + return f + // return true; } } diff --git a/ui/src/app/home/confirm-order/confirm-order.page.html b/ui/src/app/home/confirm-order/confirm-order.page.html index d3abd96..1dcdb1a 100644 --- a/ui/src/app/home/confirm-order/confirm-order.page.html +++ b/ui/src/app/home/confirm-order/confirm-order.page.html @@ -12,7 +12,7 @@
-
¥{{order.amount}}
+
¥{{(order.amount/100).toFixed(2)}}
{{order.description}}
diff --git a/ui/src/app/home/confirm-order/confirm-order.page.ts b/ui/src/app/home/confirm-order/confirm-order.page.ts index e1a8eef..549bda5 100644 --- a/ui/src/app/home/confirm-order/confirm-order.page.ts +++ b/ui/src/app/home/confirm-order/confirm-order.page.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { Router, NavigationExtras } from '@angular/router'; -import { NavController } from '@ionic/angular'; -import {Order, OrderType} from 'src/app/shared/model/order'; -import { PaymentService } from 'src/app/services/payment.service'; -import { finalize } from 'rxjs/operators'; -import {HomeService} from "../home.service"; +import { NavController, ToastController } from '@ionic/angular'; +import {Order, OrderType, WechatPayParams} from 'src/app/shared/model/order'; +import { HomeService } from "../home.service"; + +declare const wx: any; @Component({ selector: 'app-confirm-order', @@ -16,12 +16,13 @@ export class ConfirmOrderPage implements OnInit { order!: Order; orderType = OrderType; loading = false; - selectedPaymentMethod = 'wechat'; + selectedPaymentMethod = 'wechat_pay'; constructor( private router: Router, private navCtrl: NavController, - private homeService: HomeService + private homeService: HomeService, + private toastController: ToastController, ) { const navigation = this.router.getCurrentNavigation(); const order = navigation?.extras?.state?.['order']; @@ -29,11 +30,12 @@ export class ConfirmOrderPage implements OnInit { this.navCtrl.back(); return; } - - this.order = order; + this.order = order as Order; + this.order.amount = Number((this.order.amount * 100).toFixed(0)) } ngOnInit() { + } selectPaymentMethod(method: string) { @@ -47,33 +49,17 @@ export class ConfirmOrderPage implements OnInit { this.loading = true; - if (this.selectedPaymentMethod === 'wechat') { + if (this.selectedPaymentMethod === 'wechat_pay') { + // 创建支付订单并唤起微信支付 this.homeService.createOrder(this.order).subscribe({ next: (payment) => { - // 唤起微信支付 - // this.paymentService.callWechatPay(payment.orderNo).subscribe({ - // next: () => { - // // 跳转到支付结果页面 - // const navigationExtras: NavigationExtras = { - // state: { - // payment - // } - // }; - // - // this.router.navigate(['/payment-result'], navigationExtras); - // }, - // error: (error) => { - // console.error('微信支付失败:', error); - // // TODO: 显示错误提示 - // } - // }); + // 直接调用微信支付 + this.invokePay(payment); }, error: (error) => { - console.error('创建支付订单失败:', error); - // TODO: 显示错误提示 - }, - complete: () => { + alert("发起支付失败") + this.navCtrl.back() this.loading = false; } }); @@ -105,6 +91,56 @@ export class ConfirmOrderPage implements OnInit { } } + private invokePay(params: WechatPayParams) { + const onBridgeReady = () => { + // 发起支付 + (window as any).WeixinJSBridge.invoke( + 'getBrandWCPayRequest', + { + appId: params.appId, + timeStamp: params.timeStamp, + nonceStr: params.nonceStr, + package: params.package, + signType: params.signType, + paySign: params.paySign, + }, + (res: any) => { + if (res.err_msg === 'get_brand_wcpay_request:ok') { + alert('支付成功!'); + // 可以跳转回你的订单页等 + this.showToast("支付成功","success") + this.navCtrl.back() + } else { + if(res.err_msg === 'get_brand_wcpay_request:cancel') { + this.homeService.cancelOrder(this.order).subscribe((res)=>{ + alert('支付取消!'); + this.navCtrl.back() + }) + } else { + alert('支付失败:' + res.err_msg); + this.homeService.cancelOrder(this.order).subscribe((res)=>{ + this.navCtrl.back() + }) + } + } + } + ); + }; + // 判断 WeixinJSBridge 是否就绪 + if (typeof (window as any).WeixinJSBridge === 'undefined') { + if (document.addEventListener) { + document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); + } else if ((document as any).attachEvent) { + (document as any).attachEvent('WeixinJSBridgeReady', onBridgeReady); + (document as any).attachEvent('onWeixinJSBridgeReady', onBridgeReady); + } + } else { + onBridgeReady(); + } + } + + + // 获取订单类型文本 getOrderTypeText(): string { return this.order.type === OrderType.OrderTypeArticle ? '文章' : '专栏'; @@ -117,4 +153,14 @@ export class ConfirmOrderPage implements OnInit { } return `${this.order.duration}个月`; } + + private showToast(message: string, color: 'success' | 'danger' = 'success') { + this.toastController.create({ + message, + duration: 2000, + color, + position: 'top', + cssClass: 'ion-text-center' + }).then(toast => toast.present()); + } } diff --git a/ui/src/app/home/home.page.ts b/ui/src/app/home/home.page.ts index 65e6299..fa6a6da 100644 --- a/ui/src/app/home/home.page.ts +++ b/ui/src/app/home/home.page.ts @@ -1,5 +1,6 @@ import { Component, OnInit} from '@angular/core'; import { getUser} from "../mine/mine.service"; +import {Router} from "@angular/router"; @Component({ selector: 'app-home', @@ -11,22 +12,10 @@ export class HomePage implements OnInit{ constructor( - ) { - getUser().subscribe((res)=>{ - - }) + ) { } - ngOnInit() { - // this.routerSub = this.router.events.subscribe(event => { - // if (event instanceof NavigationEnd) { - // if(event.url == "/home") { - // getGiftCount().subscribe((res)=>{ - // - // }) - // } - // } - // }); + } diff --git a/ui/src/app/home/home.service.ts b/ui/src/app/home/home.service.ts index 1521a28..5126f4c 100644 --- a/ui/src/app/home/home.service.ts +++ b/ui/src/app/home/home.service.ts @@ -6,14 +6,37 @@ import {extractData, Page} from "../shared/model/page"; import {HttpUtils} from "../shared/until/http.utils"; import {Price} from "../shared/model/price"; import {Column} from "../shared/model/column"; -import {Order} from '../shared/model/order'; +import {Order, WechatPayParams} from '../shared/model/order'; import { Coupon } from '../shared/model/coupon'; @Injectable({ providedIn: 'root' }) export class HomeService { - + private buyDoneId:number = 0 + private tempInfo:Map = new Map() constructor(private http:HttpClient) { } + set(key:string,value:any) { + this.tempInfo.set(key,value) + } + + get(key:string):any { + const temp = this.tempInfo.get(key) + this.tempInfo.delete(key) + return temp + } + getBuyId():number { + const temp = this.buyDoneId + this.buyDoneId = 0 + return temp + } + setBuyId(id:number) { + this.buyDoneId = id + } + + clear() { + this.tempInfo.clear() + } + list(searchParams:{[key:string]:string}):Observable>{ return this.http.get('/api/article/all',{params:HttpUtils.getSearchParams(searchParams)}).pipe( map(response => extractData
(response)), @@ -48,8 +71,11 @@ export class HomeService { getColumnByName(title:string):Observable{ return this.http.get(`/api/column/get`,{params:{title}}) } - createOrder(order:Order):Observable { - return this.http.post('/api/order/create',order) + createOrder(order:Order):Observable { + return this.http.post('/api/order/create',order) + } + cancelOrder(order:Order):Observable{ + return this.http.post('/api/order/cancel',order) } getCouponList():Observable> { return this.http.get>('/api/coupon/get',{}) diff --git a/ui/src/app/home/special-column/special-column.page.ts b/ui/src/app/home/special-column/special-column.page.ts index 9cded48..4242143 100644 --- a/ui/src/app/home/special-column/special-column.page.ts +++ b/ui/src/app/home/special-column/special-column.page.ts @@ -1,11 +1,12 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import {Component, OnInit, OnDestroy, ChangeDetectorRef} from '@angular/core'; import { ModalController, NavController, AlertController } from "@ionic/angular"; -import { Router } from "@angular/router"; +import {NavigationEnd, Router} from "@angular/router"; import { HomeService } from '../home.service'; import { Column } from '../../shared/model/column'; import { getUser } from "../../mine/mine.service"; -import { Subject } from 'rxjs'; +import {Subject, Subscription} from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import {Article} from "../../shared/model/article"; @Component({ selector: 'app-special-column', @@ -17,12 +18,14 @@ export class SpecialColumnPage implements OnInit, OnDestroy { name: string = "" column: Column | null = null; private destroy$ = new Subject(); + private routerSub!: Subscription; constructor( private navCtrl: NavController, private router: Router, private homeService: HomeService, private modalCtrl: ModalController, + private cdr:ChangeDetectorRef, private alertCtrl: AlertController ) { const navigation = this.router.getCurrentNavigation(); @@ -34,13 +37,33 @@ export class SpecialColumnPage implements OnInit, OnDestroy { } ngOnInit() { + this.routerSub = this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + if(event.url == "/home/special-column") { + this.checkOrderOcc() + } + } + }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); + if (this.routerSub) { + this.routerSub.unsubscribe(); + } } + checkOrderOcc(){ + console.log("check column order ") + const unlock = this.homeService.get(this.column?.id+'') + if(unlock){ + console.log("unlock done") + this.column!.unlock = true; + this.cdr.detectChanges() + this.homeService.set(this.column?.title+'',true) + } + } getColumnData() { this.homeService.getColumnByName(this.name) .pipe(takeUntil(this.destroy$)) diff --git a/ui/src/app/mine/login/login.page.ts b/ui/src/app/mine/login/login.page.ts index ee366d6..276d152 100644 --- a/ui/src/app/mine/login/login.page.ts +++ b/ui/src/app/mine/login/login.page.ts @@ -61,6 +61,7 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit { this.createForm(); } ngAfterViewInit() { + } ngOnDestroy() { @@ -138,7 +139,10 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit { errorMessage = error.error.error; } } - + if(errorMessage == "openid为空") { + window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx13bd75fbedf283e5&redirect_uri=http%3A%2F%2Ffamyun.com%2Fcallback&response_type=code&scope=snsapi_base&state=123#wechat_redirect"; + return + } this.showToast(errorMessage); } }); @@ -176,6 +180,9 @@ export class LoginPage implements OnInit, OnDestroy,AfterViewInit { errorMessage = error.error.error; } } + if(errorMessage == "openid为空") { + window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx13bd75fbedf283e5&redirect_uri=http%3A%2F%2Ffamyun.com%2Fcallback&response_type=code&scope=snsapi_base&state=123#wechat_redirect"; + } this.showToast(errorMessage); } }); diff --git a/ui/src/app/mine/mine.page.html b/ui/src/app/mine/mine.page.html index 6b66e70..573558b 100644 --- a/ui/src/app/mine/mine.page.html +++ b/ui/src/app/mine/mine.page.html @@ -39,6 +39,12 @@
+ + + + + + diff --git a/ui/src/app/mine/mine.page.ts b/ui/src/app/mine/mine.page.ts index a0682be..22eb041 100644 --- a/ui/src/app/mine/mine.page.ts +++ b/ui/src/app/mine/mine.page.ts @@ -59,6 +59,10 @@ export class MinePage implements OnInit, OnDestroy { } checkLoginStatus() { + const t = localStorage.getItem("token") + if(!t){ + this.navCtrl.navigateForward("/mine/login") + } getUser().subscribe(res=>{ if(res.username == "") { this.isLoggedIn = false; @@ -99,9 +103,10 @@ export class MinePage implements OnInit, OnDestroy { togglePassword() { this.showPassword = !this.showPassword; } + login(){ + } logout() { - localStorage.removeItem('token'); this.isLoggedIn = false; this.userInfo = null; this.coupons = []; @@ -109,7 +114,7 @@ export class MinePage implements OnInit, OnDestroy { localStorage.removeItem('token'); localStorage.removeItem("giftCount") this.couponCount = 0; - this.router.navigate(['/mine/login']); + this.navCtrl.navigateForward("/mine/login") } private showToast(message: string, color: 'success' | 'danger' = 'success') { @@ -124,7 +129,13 @@ export class MinePage implements OnInit, OnDestroy { getCouponList() { this.homeService.getCouponList().subscribe(coupons => { - this.coupons = coupons; + if(coupons){ + coupons.forEach(res=>{ + res.minAmount /= 100 + res.value /= 100 + }) + this.coupons = coupons; + } }); } } diff --git a/ui/src/app/mine/mine.service.ts b/ui/src/app/mine/mine.service.ts index fd40b8b..6498bb2 100644 --- a/ui/src/app/mine/mine.service.ts +++ b/ui/src/app/mine/mine.service.ts @@ -73,6 +73,12 @@ export class MineService { return this.http.get('/api/user/guest') } + getWechatOpenId():Observable { + console.log('request') + return this.http.get('https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx13bd75fbedf283e5&redirect_uri=http%3A%2F%2Fwww.famyun.com%2Fmine%2Flogin&response_type=code&scope=snsapi_base&state=123#wechat_redirect'); + } + + } export const getLoggedIn = () => { @@ -88,6 +94,7 @@ export const getUser = (): Observable => { return of({ username: userObj.username, guest_id: userObj.guest_id, + openid:userObj.openid } as UserDetail); } else { return EMPTY diff --git a/ui/src/app/services/payment.service.ts b/ui/src/app/services/payment.service.ts deleted file mode 100644 index f7f94d4..0000000 --- a/ui/src/app/services/payment.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { environment } from 'src/environments/environment'; -import { Observable, from } from 'rxjs'; -import { map, tap, mergeMap } from 'rxjs/operators'; -import {Payment, WechatPayParams} from "../shared/model/order"; - - - -declare const WeixinJSBridge: any; - -@Injectable({ - providedIn: 'root' -}) -export class PaymentService { - - constructor(private http: HttpClient) {} - - createPayment(request: PaymentRequest): Observable { - return this.http.post('', request); - } - - getPayment(orderNo: string): Observable { - return this.http.get(`${orderNo}`); - } - - getPaymentStatus(orderNo: string): Observable<{ status: string }> { - return this.http.get<{ status: string }>(`/${orderNo}/status`); - } - - getWechatPayParams(orderNo: string): Observable { - return this.http.get(`/${orderNo}/wechat-pay`); - } - - // 唤起微信支付 - callWechatPay(orderNo: string): Observable { - return this.getWechatPayParams(orderNo).pipe( - map(params => { - if (typeof WeixinJSBridge === 'undefined') { - throw new Error('请在微信浏览器中打开'); - } - - return new Promise((resolve, reject) => { - WeixinJSBridge.invoke('getBrandWCPayRequest', params, (res: any) => { - if (res.err_msg === 'get_brand_wcpay_request:ok') { - console.log('支付成功'); - resolve(); - } else { - console.error('支付失败:', res.err_msg); - reject(new Error(res.err_msg)); - } - }); - }); - }), - mergeMap(promise => from(promise)), - tap({ - error: (error) => console.error('微信支付失败:', error) - }) - ); - } -} diff --git a/ui/src/app/shared/model/coupon.ts b/ui/src/app/shared/model/coupon.ts index 121a988..786004f 100644 --- a/ui/src/app/shared/model/coupon.ts +++ b/ui/src/app/shared/model/coupon.ts @@ -19,7 +19,6 @@ export enum CouponType { } export enum CouponStatus { - ACTIVE = 'ACTIVE', - INACTIVE = 'INACTIVE', - EXPIRED = 'EXPIRED', + ACTIVE = 1, + EXPIRED = 2 } diff --git a/ui/src/app/shared/model/order.ts b/ui/src/app/shared/model/order.ts index 61df980..bd20ed8 100644 --- a/ui/src/app/shared/model/order.ts +++ b/ui/src/app/shared/model/order.ts @@ -11,6 +11,7 @@ export interface Order { amount: number; duration: number; status: number; + coupon:number; description: string; createdAt: string; updatedAt: string; @@ -19,7 +20,7 @@ export interface Order { export interface PaymentRequest { orderNo: string; amount: number; - paymentType: 'wechat' | 'alipay'; + paymentType: 'wechat_pay' | 'alipay'; } export interface Payment { @@ -40,3 +41,5 @@ export interface WechatPayParams { signType: string; paySign: string; } + + diff --git a/ui/src/app/shared/model/user.ts b/ui/src/app/shared/model/user.ts index 2635266..3990e56 100644 --- a/ui/src/app/shared/model/user.ts +++ b/ui/src/app/shared/model/user.ts @@ -1,6 +1,7 @@ export interface UserDetail { - username:string, - guest_id:string + username:string; + guest_id:string; + openid:string; } export interface UserInfo { diff --git a/ui/src/index.html b/ui/src/index.html index 69974e9..2cd271e 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -3,7 +3,7 @@ - CLS + 路诚智讯 @@ -17,6 +17,18 @@ + + + + +