mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-09 10:43:44 +08:00
feat: integrated Alipay payment module
This commit is contained in:
142
api/service/payment/alipay_service.go
Normal file
142
api/service/payment/alipay_service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"fmt"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AlipayService struct {
|
||||
config *types.AlipayConfig
|
||||
client *alipay.Client
|
||||
}
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||
config := appConfig.AlipayConfig
|
||||
if !config.Enabled {
|
||||
logger.Info("Disabled alipay service")
|
||||
return nil, nil
|
||||
}
|
||||
priKey, err := readKey(config.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
xClient, err := alipay.New(config.AppId, priKey, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
|
||||
}
|
||||
|
||||
if err = xClient.LoadAppCertPublicKeyFromFile(config.PublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with loading alipay CertPublicKey: %v", err)
|
||||
}
|
||||
if err = xClient.LoadAliPayRootCertFromFile(config.RootCert); err != nil {
|
||||
return nil, fmt.Errorf("error with loading alipay RootCert: %v", err)
|
||||
}
|
||||
if err = xClient.LoadAlipayCertPublicKeyFromFile(config.AlipayPublicKey); err != nil {
|
||||
return nil, fmt.Errorf("error with loading alipay AlipayCertPublicKey: %v", err)
|
||||
}
|
||||
|
||||
return &AlipayService{config: &config, client: xClient}, nil
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlMobile(outTradeNo string, notifyURL string, returnURL string, Amount string, subject string) (string, error) {
|
||||
var p = alipay.TradeWapPay{}
|
||||
p.NotifyURL = notifyURL
|
||||
p.ReturnURL = returnURL
|
||||
p.Subject = subject
|
||||
p.OutTradeNo = outTradeNo
|
||||
p.TotalAmount = Amount
|
||||
p.ProductCode = "QUICK_WAP_WAY"
|
||||
res, err := s.client.TradeWapPay(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.String(), err
|
||||
}
|
||||
|
||||
func (s *AlipayService) PayUrlPc(outTradeNo string, notifyURL string, returnURL string, amount string, subject string) (string, error) {
|
||||
var p = alipay.TradePagePay{}
|
||||
p.NotifyURL = notifyURL
|
||||
p.ReturnURL = returnURL
|
||||
p.Subject = subject
|
||||
p.OutTradeNo = outTradeNo
|
||||
p.TotalAmount = amount
|
||||
p.ProductCode = "FAST_INSTANT_TRADE_PAY"
|
||||
res, err := s.client.TradePagePay(p)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return res.String(), err
|
||||
}
|
||||
|
||||
// TradeVerify 交易验证
|
||||
func (s *AlipayService) TradeVerify(reqForm url.Values) NotifyVo {
|
||||
err := s.client.VerifySign(reqForm)
|
||||
if err != nil {
|
||||
log.Println("异步通知验证签名发生错误", err)
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步通知验证签名发生错误",
|
||||
}
|
||||
}
|
||||
|
||||
return s.TradeQuery(reqForm.Get("out_trade_no"))
|
||||
}
|
||||
|
||||
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
|
||||
var p = alipay.TradeQuery{}
|
||||
p.OutTradeNo = outTradeNo
|
||||
rsp, err := s.client.TradeQuery(p)
|
||||
if err != nil {
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if rsp.IsSuccess() == true && rsp.TradeStatus == "TRADE_SUCCESS" {
|
||||
return NotifyVo{
|
||||
Status: 1,
|
||||
OutTradeNo: rsp.OutTradeNo,
|
||||
TradeNo: rsp.TradeNo,
|
||||
Amount: rsp.TotalAmount,
|
||||
Subject: rsp.Subject,
|
||||
Message: "OK",
|
||||
}
|
||||
} else {
|
||||
return NotifyVo{
|
||||
Status: 0,
|
||||
Message: "异步查询验证订单信息发生错误" + outTradeNo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readKey(filename string) (string, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
type NotifyVo struct {
|
||||
Status int
|
||||
OutTradeNo string
|
||||
TradeNo string
|
||||
Amount string
|
||||
Message string
|
||||
Subject string
|
||||
}
|
||||
|
||||
func (v NotifyVo) Success() bool {
|
||||
return v.Status == 1
|
||||
}
|
||||
56
api/service/snowflake.go
Normal file
56
api/service/snowflake.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Snowflake 雪花算法实现
|
||||
type Snowflake struct {
|
||||
mu sync.Mutex
|
||||
lastTimestamp int64
|
||||
workerID int
|
||||
sequence int
|
||||
}
|
||||
|
||||
func NewSnowflake() *Snowflake {
|
||||
return &Snowflake{
|
||||
lastTimestamp: -1,
|
||||
workerID: 0, // TODO: 增加 WorkID 参数
|
||||
sequence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Next 生成一个新的唯一ID
|
||||
func (s *Snowflake) Next() (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
timestamp := time.Now().UnixNano() / 1000000 // 转换为毫秒
|
||||
if timestamp < s.lastTimestamp {
|
||||
return "", fmt.Errorf("clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTimestamp-timestamp)
|
||||
}
|
||||
|
||||
if timestamp == s.lastTimestamp {
|
||||
s.sequence = (s.sequence + 1) & 4095
|
||||
if s.sequence == 0 {
|
||||
timestamp = s.waitNextMillis()
|
||||
}
|
||||
} else {
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.lastTimestamp = timestamp
|
||||
id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
|
||||
now := time.Now()
|
||||
return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
|
||||
}
|
||||
|
||||
func (s *Snowflake) waitNextMillis() int64 {
|
||||
timestamp := time.Now().UnixNano() / 1000000
|
||||
for timestamp <= s.lastTimestamp {
|
||||
timestamp = time.Now().UnixNano() / 1000000
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
123
api/service/xxl_job_service.go
Normal file
123
api/service/xxl_job_service.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"chatplus/core/types"
|
||||
logger2 "chatplus/logger"
|
||||
"chatplus/store/model"
|
||||
"chatplus/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/xxl-job/xxl-job-executor-go"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logger = logger2.GetLogger()
|
||||
|
||||
type XXLJobExecutor struct {
|
||||
executor xxl.Executor
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
|
||||
exec := xxl.NewExecutor(
|
||||
xxl.ServerAddr(config.XXLConfig.ServerAddr),
|
||||
xxl.AccessToken(config.XXLConfig.AccessToken), //请求令牌(默认为空)
|
||||
xxl.ExecutorIp(config.XXLConfig.ExecutorIp), //可自动获取
|
||||
xxl.ExecutorPort(config.XXLConfig.ExecutorPort), //默认9999(非必填)
|
||||
xxl.RegistryKey(config.XXLConfig.RegistryKey), //执行器名称
|
||||
xxl.SetLogger(&customLogger{}), //自定义日志
|
||||
)
|
||||
exec.Init()
|
||||
return &XXLJobExecutor{executor: exec, db: db}
|
||||
}
|
||||
|
||||
func (e *XXLJobExecutor) Run() error {
|
||||
e.executor.RegTask("ClearOrder", e.ClearOrder)
|
||||
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
|
||||
return e.executor.Run()
|
||||
}
|
||||
|
||||
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
timeout := time.Now().Unix() - 600
|
||||
start := utils.Stamp2str(timeout)
|
||||
res := e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||
}
|
||||
|
||||
// ResetVipCalls 清理过期的 VIP 会员
|
||||
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||
logger.Info("开始进行月底账号盘点...")
|
||||
var users []model.User
|
||||
res := e.db.Where("vip = ?", 1).Find(&users)
|
||||
if res.Error != nil {
|
||||
return "No vip users found"
|
||||
}
|
||||
|
||||
var sysConfig model.Config
|
||||
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||
if res.Error != nil {
|
||||
panic(res.Error)
|
||||
}
|
||||
|
||||
var config types.SystemConfig
|
||||
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 获取本月月初时间
|
||||
currentTime := time.Now()
|
||||
year, month, _ := currentTime.Date()
|
||||
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
|
||||
for _, u := range users {
|
||||
// 账号到期,直接清零
|
||||
if u.ExpiredTime <= currentTime.Unix() {
|
||||
logger.Info("账号过期:", u.Mobile)
|
||||
u.Calls = 0
|
||||
u.Vip = false
|
||||
} else {
|
||||
if u.Calls <= 0 {
|
||||
u.Calls = config.VipMonthCalls
|
||||
} else {
|
||||
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
|
||||
var orders []model.Order
|
||||
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
|
||||
var calls = 0
|
||||
for _, o := range orders {
|
||||
var remark types.OrderRemark
|
||||
err = utils.JsonDecode(o.Remark, &remark)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if remark.Days > 0 { // 会员续费
|
||||
continue
|
||||
}
|
||||
calls += remark.Calls
|
||||
}
|
||||
if u.Calls > calls { // 本月套餐没有用完
|
||||
u.Calls = calls + config.VipMonthCalls
|
||||
} else {
|
||||
u.Calls = u.Calls + config.VipMonthCalls
|
||||
}
|
||||
logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
|
||||
}
|
||||
}
|
||||
u.Tokens = 0
|
||||
// update user
|
||||
e.db.Updates(&u)
|
||||
}
|
||||
logger.Info("月底盘点完成!")
|
||||
return "success"
|
||||
}
|
||||
|
||||
type customLogger struct{}
|
||||
|
||||
func (l *customLogger) Info(format string, a ...interface{}) {
|
||||
logger.Debug(format, a)
|
||||
}
|
||||
|
||||
func (l *customLogger) Error(format string, a ...interface{}) {
|
||||
logger.Error(format, a)
|
||||
}
|
||||
Reference in New Issue
Block a user