feat(1.0.3):微信支付对接成功,接口上线

developing
张帅 6 days ago
parent fc961c7817
commit a925ad776d

@ -0,0 +1 @@
dbfX2r5hEeBPV3qp

@ -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

BIN
cls

Binary file not shown.

Binary file not shown.

@ -25,6 +25,7 @@ func runServer(*cli.Context) error {
config.NewSmsService,
config.NewInternalClient,
config.NewJWTAuthMiddleware,
config.NewPayService,
), // 基础服务
fx.Options(captchafx.Module),
fx.Invoke(

@ -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")

@ -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)
}

@ -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

@ -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=

@ -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
}

@ -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. 存储验证码

@ -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
}

@ -4,7 +4,7 @@ import "testing"
func TestPhoneEncode(t *testing.T) {
ser := NewPhoneEncryptionService()
v, _ := ser.Encrypt("17782351006")
v, _ := ser.Encrypt("15585160327")
t.Log(v)
}

@ -22,6 +22,7 @@ type CreateOrderRequest struct {
Amount int64 `json:"amount"` // 订单金额
Duration int `json:"duration"` // 购买时长
Description string `json:"description"` // 商品描述
CouponId uint64 `json:"coupon"` //优惠券
}
// OrderResponse 订单响应

@ -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)
}

@ -128,3 +128,6 @@ func (s *PaymentService) DeleteOrder(id uint64) error {
}
// CreatePayment 创建支付订单
func (s *PaymentService) CreatePayment() error {
return nil
}

@ -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

@ -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"`
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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获取订单聚合根

@ -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 指定表名

@ -22,4 +22,7 @@ type OrderRepository interface {
// Delete 删除订单
Delete(id uint64) error
// GetPendingOrder 获取待支付订单
GetPendingOrder(targetId uint64, t OrderType, uid uint64) (*Order, error)
}

@ -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 指定表名

@ -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
}

@ -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)
}

@ -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'" ` // 密码加密盐值

@ -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",
// }
//}

@ -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()),

@ -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)
}

@ -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), &notify); err != nil {
return nil, fmt.Errorf("解析通知数据失败: %v", err)
}
// 验证签名
if !s.client.verifySign(&notify) {
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
}

@ -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
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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
}

@ -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

@ -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)
}
}

@ -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)

@ -11,6 +11,6 @@ import (
var PurchaseModule = fx.Module("PurchaseModule",
fx.Provide(
interfaces.AsHandler(purchase.NewPurchaseHandler),
service.NewService,
service.NewPurchaseService,
repo.NewPurchaseRepositoryORM,
))

@ -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)

@ -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({

@ -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';
}
}

@ -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,
},
],

@ -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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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);
// }
// }
})
);
}

@ -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<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
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);
})
);
}
}

@ -26,7 +26,7 @@
<div class="content">
<div class="left">
<span class="type-text">单篇解锁</span>
<span class="discount-tag">{{(price.discount || 0.3) * 10}}折</span>
<span *ngIf="price.discount!=1" class="discount-tag">{{(price.discount || 0.3) * 10}}折</span>
</div>
<div class="right">
<span class="original-price">¥{{price.amount/100 || 1888}}</span>
@ -55,7 +55,7 @@
<div class="bottom-fixed-wrapper">
<!-- 提交按钮 -->
<ion-button expand="block" class="submit-btn" [disabled]="getDiscountPriceAndCoupon(price.amount, price.discount) <= 0" (click)="submitOrder()">
确认支付 ¥{{getDiscountPriceAndCoupon(price.amount, price.discount)}}
确认支付 ¥{{(getDiscountPriceAndCoupon(price.amount, price.discount) - this.couponAmount).toFixed(2)}}
</ion-button>
<app-lc-fixed-bar></app-lc-fixed-bar>

@ -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)}}
})
}
}

@ -15,7 +15,7 @@
<!-- 发布时间 -->
<div class="article-meta">
<span class="date">{{ article?.releaseDate | date:'yyyy-MM-dd HH:mm' }}</span>
<span class="date">{{ article?.releaseDate }} {{ article?.releaseTime }}</span>
<span class="weekday">{{ weekday }}</span>
</div>
@ -33,7 +33,7 @@
<ng-container *ngIf="article && article.stocks">
<ng-container *ngFor="let stock of article.stocks.split(','); let last = last">
{{stock.trim()}}
<span *ngIf="stockChanges[stock.trim()]"
<span *ngIf="stockChanges[stock.trim()]"
[class]="'change ' + getStockChangeClass(stock.trim())">
{{formatChange(stockChanges[stock.trim()])}}
</span>

@ -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) {
// 调用接口时使用纯数字代码

@ -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)}}
})
}

@ -1,11 +1,11 @@
<ion-segment [(ngModel)]="currentSelect" (ionChange)="ionChange($event)">
<ion-segment-button value="new" (click)="select('new')">
<ion-segment-button value="new" >
<ion-label>最新</ion-label>
</ion-segment-button>
<ion-segment-button value="unlock" (click)="select('unlock')">
<ion-segment-button value="unlock">
<ion-label >已解锁</ion-label>
</ion-segment-button>
<ion-segment-button value="free" (click)="select('free')">
<ion-segment-button value="free" >
<ion-label >免费试读</ion-label>
</ion-segment-button>
</ion-segment>
@ -25,7 +25,7 @@
<ion-list *ngIf="username && data.length != 0">
<app-article-item [article]="item" [username]="username" *ngFor="let item of data"></app-article-item>
</ion-list>
<div class="empty-state" *ngIf="data.length==0">
<div class="empty-state" *ngIf="username && data.length==0">
<ion-icon name="map-outline"></ion-icon>
<p>暂无已解锁文章</p>
</div>

@ -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<void>();
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() {

@ -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',

@ -1,8 +1,8 @@
<div class="coupon-container">
<div class="coupon-header">
<h2>优惠券</h2>
<span class="count" *ngIf="coupons.length" >{{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}}</span>
<div class="no-coupon" *ngIf="!coupons.length">
<span class="count" *ngIf="coupons" >{{ couponAmount!= 0 ?'-':'' }}¥ {{couponAmount}}</span>
<div class="no-coupon" *ngIf="!coupons">
暂无可用优惠券
</div>
</div>

@ -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<Coupon|null>();
@ -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;
}
}

@ -12,7 +12,7 @@
<ion-content class="ion-padding">
<!-- 支付金额 -->
<div class="amount-section">
<div class="amount">¥{{order.amount}}</div>
<div class="amount">¥{{(order.amount/100).toFixed(2)}}</div>
<div class="desc">{{order.description}}</div>
</div>

@ -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());
}
}

@ -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)=>{
//
// })
// }
// }
// });
}

@ -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<string,any> = new Map<string, any>()
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<Page<Article>>{
return this.http.get('/api/article/all',{params:HttpUtils.getSearchParams(searchParams)}).pipe(
map(response => extractData<Article>(response)),
@ -48,8 +71,11 @@ export class HomeService {
getColumnByName(title:string):Observable<Column>{
return this.http.get<Column>(`/api/column/get`,{params:{title}})
}
createOrder(order:Order):Observable<any> {
return this.http.post<any>('/api/order/create',order)
createOrder(order:Order):Observable<WechatPayParams> {
return this.http.post<WechatPayParams>('/api/order/create',order)
}
cancelOrder(order:Order):Observable<void>{
return this.http.post<void>('/api/order/cancel',order)
}
getCouponList():Observable<Array<Coupon>> {
return this.http.get<Array<Coupon>>('/api/coupon/get',{})

@ -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<void>();
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$))

@ -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);
}
});

@ -39,6 +39,12 @@
</div>
</ion-list>
</div>
<!-- <div *ngIf="!isLoggedIn">-->
<!-- <ion-button expand="block" color="danger" (click)="login()">-->
<!-- 去登录-->
<!-- </ion-button>-->
<!-- </div>-->
</ion-content>
<ion-footer *ngIf="isLoggedIn">

@ -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;
}
});
}
}

@ -73,6 +73,12 @@ export class MineService {
return this.http.get<string>('/api/user/guest')
}
getWechatOpenId():Observable<string> {
console.log('request')
return this.http.get<string>('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<UserDetail> => {
return of({
username: userObj.username,
guest_id: userObj.guest_id,
openid:userObj.openid
} as UserDetail);
} else {
return EMPTY

@ -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<Payment> {
return this.http.post<Payment>('', request);
}
getPayment(orderNo: string): Observable<Payment> {
return this.http.get<Payment>(`${orderNo}`);
}
getPaymentStatus(orderNo: string): Observable<{ status: string }> {
return this.http.get<{ status: string }>(`/${orderNo}/status`);
}
getWechatPayParams(orderNo: string): Observable<WechatPayParams> {
return this.http.get<WechatPayParams>(`/${orderNo}/wechat-pay`);
}
// 唤起微信支付
callWechatPay(orderNo: string): Observable<void> {
return this.getWechatPayParams(orderNo).pipe(
map(params => {
if (typeof WeixinJSBridge === 'undefined') {
throw new Error('请在微信浏览器中打开');
}
return new Promise<void>((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)
})
);
}
}

@ -19,7 +19,6 @@ export enum CouponType {
}
export enum CouponStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
EXPIRED = 'EXPIRED',
ACTIVE = 1,
EXPIRED = 2
}

@ -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;
}

@ -1,6 +1,7 @@
export interface UserDetail {
username:string,
guest_id:string
username:string;
guest_id:string;
openid:string;
}
export interface UserInfo {

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>CLS</title>
<title>路诚智讯</title>
<base href="/" />
@ -17,6 +17,18 @@
<!-- add to homescreen for ios -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- 引入微信JSSDK -->
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 备用地址 -->
<script>
// 如果主地址加载失败,尝试加载备用地址
if (typeof wx === 'undefined') {
var script = document.createElement('script');
script.src = 'http://res2.wx.qq.com/open/js/jweixin-1.6.0.js';
document.head.appendChild(script);
}
</script>
</head>
<body>

Loading…
Cancel
Save