Initial commit
This commit is contained in:
18
comm.go
Normal file
18
comm.go
Normal file
@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
// 定义公共变量名
|
||||
const (
|
||||
// 从jwt中解析出来的字段,放到 context 中
|
||||
KEY_DEVICEID = "DeviceID"
|
||||
KEY_GID = "GID"
|
||||
|
||||
KEY_HEADER_TIMEZONE = "timezone"
|
||||
KEY_HEADER_SECONDSFROMGMT = "secondsfromgmt"
|
||||
|
||||
// 定义每日免费次数
|
||||
DAILY_FREE_COUNT = 3
|
||||
|
||||
// 定义购买的应用商店
|
||||
APPSTORE = "appstore"
|
||||
PLAYSTORE = "playstore"
|
||||
)
|
||||
155
config.go
Normal file
155
config.go
Normal file
@ -0,0 +1,155 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type AzureOpenAIConfig struct {
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
Keys []string `mapstructure:"keys"`
|
||||
GPT4Model string `mapstructure:"gpt4_model"`
|
||||
GPT35Model string `mapstructure:"gpt35_model"`
|
||||
}
|
||||
|
||||
type OpenAIConfig struct {
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
Organization string `mapstructure:"organization"`
|
||||
}
|
||||
|
||||
type AIGrammarBaseConfig struct {
|
||||
// 在 Go 中,只有首字母大写的字段才能被外部包(如 viper)访问。
|
||||
JwtSecret string `mapstructure:"jwt_secret"`
|
||||
BindAddr string `mapstructure:"bind_addr"`
|
||||
}
|
||||
|
||||
type DataBaseConfig struct {
|
||||
// 在 Go 中,只有首字母大写的字段才能被外部包(如 viper)访问。
|
||||
MysqlConn string `mapstructure:"mysql_conn"`
|
||||
RedisConn string `mapstructure:"redis_conn"`
|
||||
MysqlUser string `mapstructure:"mysql_user"`
|
||||
MysqlPass string `mapstructure:"mysql_pass"`
|
||||
}
|
||||
|
||||
type LoggerConfig struct {
|
||||
// 在 Go 中,只有首字母大写的字段才能被外部包(如 viper)访问。
|
||||
EchoLogFile string `mapstructure:"echo_log_file"`
|
||||
LogFile string `mapstructure:"log_file"`
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
Level string `mapstructure:"level"`
|
||||
}
|
||||
|
||||
type ConfigManager struct {
|
||||
BaseConfig AIGrammarBaseConfig
|
||||
AzureOpenAI AzureOpenAIConfig
|
||||
OpenAI OpenAIConfig
|
||||
DBConfig DataBaseConfig
|
||||
LogConfig LoggerConfig
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
var instance *ConfigManager
|
||||
var initError error
|
||||
|
||||
func GetConfigManager() (*ConfigManager, error) {
|
||||
once.Do(func() {
|
||||
instance = &ConfigManager{}
|
||||
initError = instance.initConfig()
|
||||
})
|
||||
return instance, initError
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) initConfig() error {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("toml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 这里如果有字段是带逗号的字符串,处理会有问题,需要改正。
|
||||
if err := cm.loadConfigSection("base", &cm.BaseConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cm.loadConfigSection("azure_openai", &cm.AzureOpenAI); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cm.validateAzureOpenAI(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cm.loadConfigSection("openai", &cm.OpenAI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cm.loadConfigSection("database", &cm.DBConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cm.loadConfigSection("log", &cm.LogConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) loadConfigSection(key string, config interface{}) error {
|
||||
return viper.UnmarshalKey(key, config, viper.DecodeHook(
|
||||
func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) {
|
||||
if f == reflect.String && t == reflect.Slice {
|
||||
return strings.Split(data.(string), ","), nil
|
||||
}
|
||||
return data, nil
|
||||
}))
|
||||
}
|
||||
|
||||
// validateAzureOpenAI checks that the AzureOpenAI configuration has all required fields properly set.
|
||||
func (cm *ConfigManager) validateAzureOpenAI() error {
|
||||
if strings.TrimSpace(cm.AzureOpenAI.Endpoint) == "" {
|
||||
return errors.New("AzureOpenAI endpoint cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(cm.AzureOpenAI.GPT4Model) == "" {
|
||||
return errors.New("AzureOpenAI GPT4Model cannot be empty")
|
||||
}
|
||||
if strings.TrimSpace(cm.AzureOpenAI.GPT35Model) == "" {
|
||||
return errors.New("AzureOpenAI GPT35Model cannot be empty")
|
||||
}
|
||||
if len(cm.AzureOpenAI.Keys) == 0 || strings.TrimSpace(cm.AzureOpenAI.Keys[0]) == "" {
|
||||
return errors.New("AzureOpenAI Keys must contain at least one valid key")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cm.BaseConfig.JwtSecret) == "" {
|
||||
return errors.New("jwt secret cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) GetAzureConfig() *AzureOpenAIConfig {
|
||||
return &cm.AzureOpenAI
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) GetOpenAIConfig() *OpenAIConfig {
|
||||
return &cm.OpenAI
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) GetBaseConfig() *AIGrammarBaseConfig {
|
||||
return &cm.BaseConfig
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) GetDatabaseConfig() *DataBaseConfig {
|
||||
return &cm.DBConfig
|
||||
}
|
||||
|
||||
func (cm *ConfigManager) GetLogConfig() *LoggerConfig {
|
||||
return &cm.LogConfig
|
||||
}
|
||||
27
config.toml
Normal file
27
config.toml
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
[base]
|
||||
jwt_secret = "mCTf-JhNRnhaaGJy_x"
|
||||
bind_addr = ":80"
|
||||
|
||||
[log]
|
||||
echo_log_file = "logs/echo.log"
|
||||
log_file = "logs/app.log"
|
||||
max_size = 500
|
||||
max_backups = 3
|
||||
max_age = 28
|
||||
compress = true
|
||||
level = "debug"
|
||||
|
||||
|
||||
[azure_openai]
|
||||
endpoint = "https://grammar.openai.azure.com/"
|
||||
keys = "8b68c235b737488ab9a99983a14f8cca,0274ccde58aa47b189f0d13349885ad3"
|
||||
gpt4_model = "gpt4"
|
||||
gpt35_model = "gpt35"
|
||||
|
||||
|
||||
[database]
|
||||
mysql_conn = "172.18.0.3:3306"
|
||||
mysql_user = "root"
|
||||
mysql_pass = "mysqlpw"
|
||||
redis_conn = "172.18.0.2:6379"
|
||||
110
db.go
Normal file
110
db.go
Normal file
@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type DBManager struct {
|
||||
MySQL *sql.DB
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
// dbInstance holds the single instance of DBManager
|
||||
var dbInstance *DBManager
|
||||
var oncedb sync.Once
|
||||
|
||||
// GetDBManager provides a global access point to the DBManager instance
|
||||
func GetDBManager() (*DBManager, error) {
|
||||
oncedb.Do(func() {
|
||||
config, _ := GetConfigManager()
|
||||
dbInstance = &DBManager{}
|
||||
initError = initDBManager(dbInstance, config)
|
||||
})
|
||||
return dbInstance, initError
|
||||
}
|
||||
|
||||
// initDBManager initializes the DBManager instance with database connections
|
||||
func initDBManager(dbManager *DBManager, config *ConfigManager) error {
|
||||
// 初始化 MySQL 连接
|
||||
//dsn := "username:password@tcp(localhost:3306)/aigrammar?parseTime=true"
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/aigrammar?parseTime=true",
|
||||
config.GetDatabaseConfig().MysqlUser,
|
||||
config.GetDatabaseConfig().MysqlPass,
|
||||
config.GetDatabaseConfig().MysqlConn,
|
||||
)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening MySQL database: %v", err)
|
||||
return errors.New("Error opening MySQL database:")
|
||||
}
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(10)
|
||||
db.SetConnMaxLifetime(time.Minute * 5)
|
||||
dbManager.MySQL = db
|
||||
|
||||
// 初始化 Redis 连接
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: config.GetDatabaseConfig().RedisConn,
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
PoolSize: 10, // 连接池大小
|
||||
})
|
||||
|
||||
dbManager.Redis = rdb
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DBManager) CloseDB() {
|
||||
db.MySQL.Close()
|
||||
db.Redis.Close()
|
||||
}
|
||||
|
||||
/*
|
||||
func NewDBManager(config *ConfigManager) (*DBManager, error) {
|
||||
dbManager := &DBManager{}
|
||||
|
||||
// 初始化 MySQL 连接
|
||||
//dsn := "username:password@tcp(localhost:3306)/aigrammar?parseTime=true"
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/aigrammar?parseTime=true",
|
||||
config.GetDatabaseConfig().MysqlUser,
|
||||
config.GetDatabaseConfig().MysqlPass,
|
||||
config.GetDatabaseConfig().MysqlConn,
|
||||
)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening MySQL database: %v", err)
|
||||
return nil, errors.New("Error opening MySQL database: %v", err)
|
||||
}
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(10)
|
||||
db.SetConnMaxLifetime(time.Minute * 5)
|
||||
dbManager.MySQL = db
|
||||
|
||||
// 初始化 Redis 连接
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: config.GetDatabaseConfig().RedisConn,
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
PoolSize: 10, // 连接池大小
|
||||
})
|
||||
|
||||
dbManager.Redis = rdb
|
||||
|
||||
return dbManager, nil
|
||||
}
|
||||
|
||||
func (db *DBManager) CloseDB() {
|
||||
db.MySQL.Close()
|
||||
db.Redis.Close()
|
||||
}
|
||||
|
||||
*/
|
||||
17
errcode.go
Normal file
17
errcode.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
// 定义统一的错误码
|
||||
const (
|
||||
// 从jwt中解析出来的字段,放到 context 中
|
||||
ERR_BEGIN = 100000
|
||||
ERR_COMM_AUTH = 100001
|
||||
ERR_COMM_PARAM = 100002
|
||||
ERR_COMM_IVALID_USER = 100003
|
||||
ERR_COMM_SVR_TIMEOUT = 100004
|
||||
ERR_COMM_SVR_WRONG = 100005
|
||||
|
||||
ERR_BENIFIT_FREE_LIMIT = 101000
|
||||
ERR_DIRTY_CONTENT = 101001
|
||||
|
||||
ERR_GRAMMAR_OK = 102000
|
||||
)
|
||||
97
format.go
Normal file
97
format.go
Normal file
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Ret int `json:"ret"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func unifiedResponseHandler(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
if err != nil {
|
||||
// Let the custom HTTP error handler handle the error
|
||||
fmt.Fprintf(os.Stderr, "print error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle successful responses
|
||||
ret, _ := c.Get("ret").(int)
|
||||
msg, _ := c.Get("msg").(string)
|
||||
response := c.Get("response")
|
||||
return c.JSON(http.StatusOK, Response{
|
||||
Ret: ret,
|
||||
Message: msg,
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Custom HTTP error handler
|
||||
func customHTTPErrorHandler(err error, c echo.Context) {
|
||||
var (
|
||||
code = http.StatusInternalServerError
|
||||
message = "Internal Server Error"
|
||||
)
|
||||
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
code = he.Code
|
||||
message = he.Message.(string)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Ret: code,
|
||||
Message: message,
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to set the response
|
||||
func setResponse(c echo.Context, data interface{}) {
|
||||
c.Set("ret", 0)
|
||||
c.Set("msg", "success")
|
||||
c.Set("response", data)
|
||||
}
|
||||
|
||||
// Helper function to set the response
|
||||
func setErrResponse(c echo.Context, ret int, msg string) {
|
||||
c.Set("ret", ret)
|
||||
c.Set("msg", msg)
|
||||
c.Set("response", nil)
|
||||
}
|
||||
|
||||
/*
|
||||
func unifiedResponseHandler(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
|
||||
// Retrieve the response
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
return c.JSON(http.StatusOK, Response{
|
||||
Ret: he.Code,
|
||||
Message: he.Message.(string),
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle successful responses
|
||||
response := c.Get("response")
|
||||
return c.JSON(http.StatusOK, Response{
|
||||
Ret: 0,
|
||||
Message: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
53
go.mod
Normal file
53
go.mod
Normal file
@ -0,0 +1,53 @@
|
||||
module aigrammar
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
|
||||
github.com/awa/go-iap v1.33.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/izniburak/appstore-notifications-go v1.2.0 // indirect
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.12.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.5.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
113
go.sum
Normal file
113
go.sum
Normal file
@ -0,0 +1,113 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.1 h1:I/QS4sYByil1QAEkqGDJFpgsjIq9p2GzevLm2j2qhlw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.1/go.mod h1:pzGC8ZUnOtOCnyXHTBkj0+BjgFUsnWcqyI3FjvpnQU8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 h1:c4k2FIYIh4xtwqrQwV0Ct1v5+ehlNXj5NI/MWVsiTkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2/go.mod h1:5FDJtLEO/GxwNgUxbwrY3LP0pEoThTQJtk2oysdXHxM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||
github.com/awa/go-iap v1.33.1 h1:MCKQN5FfS7rvJnVOkFh9+xhWJhZI5/OBe8FV6aTewO8=
|
||||
github.com/awa/go-iap v1.33.1/go.mod h1:roSGnO9xHwxg8BKKnDY2gsjO9XskLZVay6+0+RY59Lg=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/izniburak/appstore-notifications-go v1.2.0 h1:nudilqBpyu2B3rr8/1R9p5d/ViXgCW05RnY+zAp6YAA=
|
||||
github.com/izniburak/appstore-notifications-go v1.2.0/go.mod h1:ezo9HwwqhnVrZRthC74aHa8yNS85fiHnOA6bb69ySIg=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
|
||||
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
||||
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
178
iap.go
Normal file
178
iap.go
Normal file
@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/awa/go-iap/appstore/api"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 处理appstore的回调
|
||||
func IapCallbackHandler(c echo.Context) error {
|
||||
// 获取请求体
|
||||
body, err := io.ReadAll(c.Request().Body) // {"signedPayload":"..."}
|
||||
if err != nil {
|
||||
logger.Error("Failed to read request body", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "read body error.")
|
||||
//return c.JSON(http.StatusInternalServerError, "Failed to read request body")
|
||||
}
|
||||
|
||||
// App Store Server Notification Request JSON String
|
||||
var request AppStoreServerRequest
|
||||
err2 := json.Unmarshal([]byte(body), &request) // bind byte to header structure
|
||||
if err2 != nil {
|
||||
logger.Error("AppStoreServerRequest Unmarshal error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Failed to read request body")
|
||||
}
|
||||
|
||||
// Apple Root CA - G3 Root certificate
|
||||
// for details: https://www.apple.com/certificateauthority/
|
||||
// you need download it and covert it to a valid pem file in order to verify X5c certificates
|
||||
// `openssl x509 -in AppleRootCA-G3.cer -out cert.pem`
|
||||
appStoreServerNotification, err := IAP_Notify_New(request.SignedPayload, IAP_ROOT_CERT)
|
||||
if err != nil {
|
||||
logger.Error("IAP_Notify_New error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to decode body")
|
||||
}
|
||||
|
||||
// 打印字段,为了显示方便,把加密串替换掉
|
||||
appStoreServerNotification.Payload.Data.SignedRenewalInfo = "..."
|
||||
appStoreServerNotification.Payload.Data.SignedTransactionInfo = "..."
|
||||
logger.Debug("appStoreServerNotification", zap.Any("appStoreServerNotification", appStoreServerNotification))
|
||||
|
||||
// 打印日志
|
||||
//buff, _ := json.Marshal(&appStoreServerNotification)
|
||||
//fmt.Println(string(buff))
|
||||
UpdateOrderByNotify(appStoreServerNotification)
|
||||
|
||||
setResponse(c, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理从客户端过来的订单验证请求
|
||||
func IapVerify(c echo.Context) error {
|
||||
var request struct {
|
||||
TransID string `json:"transid" form:"transid"`
|
||||
AppAccountToken string `json:"appaccounttoken" form:"appaccounttoken"`
|
||||
Env string `json:"env" form:"env"`
|
||||
ProductID string `json:"productid" form:"productid"`
|
||||
ReceiptData string `json:"receiptdata" form:"receiptdata"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("read param error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
// 验证 receiptdata 是否为有效的 JSON 字符串
|
||||
var jsonObj interface{}
|
||||
if err := json.Unmarshal([]byte(request.ReceiptData), &jsonObj); err != nil {
|
||||
logger.Debug("receiptdata from request", zap.Any("receiptdata", request.ReceiptData)) // 打印日志
|
||||
} else {
|
||||
logger.Debug("receiptdata from request", zap.Any("receiptdata", jsonObj)) // 打印日志
|
||||
}
|
||||
|
||||
var isSandBox = true
|
||||
// 忽略大小写进行比较
|
||||
if strings.EqualFold(request.Env, "Production") {
|
||||
isSandBox = false
|
||||
}
|
||||
|
||||
cfg := &api.StoreConfig{
|
||||
KeyContent: []byte(IAP_ACCOUNT_KEY), // Loads a .p8 certificate
|
||||
KeyID: IAP_KEY_ID, // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
|
||||
BundleID: IAP_BUNDLEID, // Your app’s bundle ID
|
||||
Issuer: IAP_ISSUER, // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")
|
||||
Sandbox: isSandBox, // default is Production
|
||||
}
|
||||
client := api.NewStoreClient(cfg)
|
||||
ctx := context.Background()
|
||||
response, err := client.GetTransactionInfo(ctx, request.TransID)
|
||||
if err != nil {
|
||||
logger.Error("GetTransactionInfo error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "GetTransactionInfo error")
|
||||
}
|
||||
|
||||
transantion, err := client.ParseSignedTransaction(response.SignedTransactionInfo)
|
||||
if err != nil {
|
||||
logger.Error("ParseSignedTransaction error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "ParseSignedTransaction error")
|
||||
}
|
||||
|
||||
logger.Debug("transantion", zap.Any("transantion", transantion)) // 打印日志
|
||||
//buff, _ := json.Marshal(&transantion)
|
||||
//fmt.Println(string(buff))
|
||||
|
||||
if transantion.TransactionID != request.TransID {
|
||||
logger.Error("transactionId not match.", zap.Any("transantion.TransactionID", transantion.TransactionID), zap.Any("request.TransID", request.TransID))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "transactionId not match")
|
||||
}
|
||||
|
||||
// 写入DB
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
errDB := UpdateOrderByVerify(GID, request.AppAccountToken, transantion.OriginalTransactionId, transantion)
|
||||
if errDB != nil {
|
||||
logger.Error("UpdateOrderByVerify error.", zap.Error(errDB))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "UpdateOrderByVerify error")
|
||||
}
|
||||
|
||||
setResponse(c, map[string]string{"ret": "ok"})
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询订单历史信息,通常是内部服务发起
|
||||
func IapHistory(c echo.Context) error {
|
||||
var request struct {
|
||||
OriginTransID string `json:"origintransid" form:"origintransid"`
|
||||
Lang string `json:"lang" form:"lang"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("read param error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
cfg := &api.StoreConfig{
|
||||
KeyContent: []byte(IAP_ACCOUNT_KEY), // Loads a .p8 certificate
|
||||
KeyID: IAP_KEY_ID, // Your private key ID from App Store Connect (Ex: 2X9R4HXF34)
|
||||
BundleID: IAP_BUNDLEID, // Your app’s bundle ID
|
||||
Issuer: IAP_ISSUER, // Your issuer ID from the Keys page in App Store Connect (Ex: "57246542-96fe-1a63-e053-0824d011072a")
|
||||
Sandbox: true, // default is Production
|
||||
}
|
||||
client := api.NewStoreClient(cfg)
|
||||
|
||||
query := &url.Values{}
|
||||
query.Set("productType", "AUTO_RENEWABLE")
|
||||
//query.Set("productType", "NON_CONSUMABLE")
|
||||
ctx := context.Background()
|
||||
responses, err := client.GetTransactionHistory(ctx, request.OriginTransID, query)
|
||||
if err != nil {
|
||||
logger.Error("GetTransactionHistory error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "GetTransactionHistory error")
|
||||
}
|
||||
|
||||
// 由于接口字段中有HasMore,所以 responses 是个数组,每个 responses 中的 transantions 也是数组
|
||||
var allTransactions []*api.JWSTransaction
|
||||
for _, response := range responses {
|
||||
transantions, err := client.ParseSignedTransactions(response.SignedTransactions)
|
||||
if err != nil {
|
||||
logger.Error("ParseSignedTransactions error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "ParseSignedTransactions error")
|
||||
}
|
||||
allTransactions = append(allTransactions, transantions...)
|
||||
|
||||
logger.Debug("transantions", zap.Any("transantions", transantions)) // 打印
|
||||
//buff, _ := json.Marshal(&transantions)
|
||||
//fmt.Println(string(buff))
|
||||
}
|
||||
|
||||
setResponse(c, allTransactions)
|
||||
//setResponse(c, map[string]string{"ret": "ok"})
|
||||
return nil
|
||||
}
|
||||
36
iap_def.go
Normal file
36
iap_def.go
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
// 定义公共变量名
|
||||
const (
|
||||
IAP_KEY_ID = "4QU88N92RF"
|
||||
IAP_BUNDLEID = "com.easyprompts.aigrammar"
|
||||
IAP_ISSUER = "3f08cd58-bf08-44b3-bb4b-d86ddc9cafe8"
|
||||
|
||||
IAP_ROOT_CERT = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS
|
||||
QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u
|
||||
IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN
|
||||
MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS
|
||||
b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y
|
||||
aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49
|
||||
AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf
|
||||
TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517
|
||||
IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr
|
||||
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA
|
||||
MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4
|
||||
at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM
|
||||
6BgD56KyKA==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
// For generate key file and download it, please refer to https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api
|
||||
IAP_ACCOUNT_KEY = `
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgVB1nZ5afhMVcik5i
|
||||
aOI3CZgn2LpuRt41Sk4X6CZQftSgCgYIKoZIzj0DAQehRANCAARzgJhXtQ946Spu
|
||||
soEazCxU/4qMJRSAmFJgdhTnmVrD3SoFv1sjRRtYxY0VX1rsym2wVA2bJVDBONju
|
||||
wDVRzD84
|
||||
-----END PRIVATE KEY-----
|
||||
`
|
||||
)
|
||||
182
iap_notify_v2.go
Normal file
182
iap_notify_v2.go
Normal file
@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
* 来源于: https://github.com/izniburak/appstore-notifications-go
|
||||
* 源码中 parseJwtSignedPayload 对出错直接用了 panic,不能直接引用
|
||||
* 修改代码以更健壮。
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
func IAP_Notify_New(payload string, appleRootCert string) (*AppStoreServerNotification, error) {
|
||||
asn := &AppStoreServerNotification{}
|
||||
asn.IsValid = false
|
||||
asn.IsTest = false
|
||||
asn.appleRootCert = appleRootCert
|
||||
|
||||
if err := asn.parseJwtSignedPayload(payload); err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return asn, nil
|
||||
}
|
||||
|
||||
func (asn *AppStoreServerNotification) extractHeaderByIndex(payload string, index int) ([]byte, error) {
|
||||
// get header from token
|
||||
payloadArr := strings.Split(payload, ".")
|
||||
|
||||
// convert header to byte
|
||||
headerByte, err := base64.RawStdEncoding.DecodeString(payloadArr[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// bind byte to header structure
|
||||
var header NotificationHeader
|
||||
err = json.Unmarshal(headerByte, &header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// decode x.509 certificate headers to byte
|
||||
certByte, err := base64.StdEncoding.DecodeString(header.X5c[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certByte, nil
|
||||
}
|
||||
|
||||
func (asn *AppStoreServerNotification) verifyCertificate(certByte []byte, intermediateCert []byte) error {
|
||||
// create certificate pool
|
||||
roots := x509.NewCertPool()
|
||||
|
||||
// parse and append apple root certificate to the pool
|
||||
ok := roots.AppendCertsFromPEM([]byte(asn.appleRootCert))
|
||||
if !ok {
|
||||
return errors.New("root certificate couldn't be parsed")
|
||||
}
|
||||
|
||||
// parse and append intermediate x5c certificate
|
||||
interCert, err := x509.ParseCertificate(intermediateCert)
|
||||
if err != nil {
|
||||
return errors.New("intermediate certificate couldn't be parsed")
|
||||
}
|
||||
intermediate := x509.NewCertPool()
|
||||
intermediate.AddCert(interCert)
|
||||
|
||||
// parse x5c certificate
|
||||
cert, err := x509.ParseCertificate(certByte)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// verify X5c certificate using app store certificate resides in opts
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediate,
|
||||
}
|
||||
if _, err := cert.Verify(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (asn *AppStoreServerNotification) extractPublicKeyFromPayload(payload string) (*ecdsa.PublicKey, error) {
|
||||
// get certificate from X5c[0] header
|
||||
certStr, err := asn.extractHeaderByIndex(payload, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse certificate
|
||||
cert, err := x509.ParseCertificate(certStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get public key
|
||||
switch pk := cert.PublicKey.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
return pk, nil
|
||||
default:
|
||||
return nil, errors.New("appstore public key must be of type ecdsa.PublicKey")
|
||||
}
|
||||
}
|
||||
|
||||
func (asn *AppStoreServerNotification) parseJwtSignedPayload(payload string) error {
|
||||
// get root certificate from x5c header
|
||||
rootCertStr, err := asn.extractHeaderByIndex(payload, 2)
|
||||
if err != nil {
|
||||
return errors.New("extractHeaderByIndex error, in rootCertStr")
|
||||
}
|
||||
|
||||
// get intermediate certificate from x5c header
|
||||
intermediateCertStr, err := asn.extractHeaderByIndex(payload, 1)
|
||||
if err != nil {
|
||||
return errors.New("extractHeaderByIndex error, in intermediateCertStr")
|
||||
}
|
||||
|
||||
// verify certificates
|
||||
if err = asn.verifyCertificate(rootCertStr, intermediateCertStr); err != nil {
|
||||
fmt.Printf("verifyCertificate eror: %v\n", err)
|
||||
return errors.New("verifyCertificate eror")
|
||||
}
|
||||
|
||||
// payload data
|
||||
notificationPayload := &NotificationPayload{}
|
||||
_, err = jwt.ParseWithClaims(payload, notificationPayload, func(token *jwt.Token) (interface{}, error) {
|
||||
return asn.extractPublicKeyFromPayload(payload)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("ParseWithClaims NotificationPayload error")
|
||||
}
|
||||
asn.Payload = notificationPayload
|
||||
asn.IsTest = asn.Payload.NotificationType == "TEST"
|
||||
|
||||
if asn.IsTest {
|
||||
asn.IsValid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// transaction info
|
||||
transactionInfo := &TransactionInfo{}
|
||||
payload = asn.Payload.Data.SignedTransactionInfo
|
||||
_, err = jwt.ParseWithClaims(payload, transactionInfo, func(token *jwt.Token) (interface{}, error) {
|
||||
return asn.extractPublicKeyFromPayload(payload)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("ParseWithClaims SignedTransactionInfo error")
|
||||
}
|
||||
asn.TransactionInfo = transactionInfo
|
||||
|
||||
// renewal info
|
||||
renewalInfo := &RenewalInfo{}
|
||||
payload = asn.Payload.Data.SignedRenewalInfo
|
||||
_, err = jwt.ParseWithClaims(payload, renewalInfo, func(token *jwt.Token) (interface{}, error) {
|
||||
return asn.extractPublicKeyFromPayload(payload)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("ParseWithClaims SignedRenewalInfo error")
|
||||
}
|
||||
asn.RenewalInfo = renewalInfo
|
||||
|
||||
// valid request
|
||||
asn.IsValid = true
|
||||
|
||||
return nil
|
||||
}
|
||||
118
iap_notify_v2_types.go
Normal file
118
iap_notify_v2_types.go
Normal file
@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
* 来源于: https://github.com/izniburak/appstore-notifications-go
|
||||
* 源码中 parseJwtSignedPayload 对出错直接用了 panic,不能直接引用
|
||||
* 修改代码以更健壮。
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
import "github.com/golang-jwt/jwt"
|
||||
|
||||
type AppStoreServerNotification struct {
|
||||
appleRootCert string
|
||||
Payload *NotificationPayload
|
||||
TransactionInfo *TransactionInfo
|
||||
RenewalInfo *RenewalInfo
|
||||
IsValid bool
|
||||
IsTest bool
|
||||
}
|
||||
|
||||
type AppStoreServerRequest struct {
|
||||
SignedPayload string `json:"signedPayload"`
|
||||
}
|
||||
|
||||
type NotificationHeader struct {
|
||||
Alg string `json:"alg"`
|
||||
X5c []string `json:"x5c"`
|
||||
}
|
||||
|
||||
type NotificationPayload struct {
|
||||
jwt.StandardClaims
|
||||
NotificationType string `json:"notificationType"`
|
||||
Subtype string `json:"subtype"`
|
||||
NotificationUUID string `json:"notificationUUID"`
|
||||
Version string `json:"version"`
|
||||
SignedDate int `json:"signedDate"`
|
||||
Summary NotificationSummary `json:"summary,omitempty"`
|
||||
Data NotificationData `json:"data,omitempty"`
|
||||
ExternalPurchaseToken ExternalPurchaseToken `json:"externalPurchaseToken,omitempty"`
|
||||
}
|
||||
|
||||
type ExternalPurchaseToken struct {
|
||||
ExternalPurchaseId string `json:"externalPurchaseId"`
|
||||
TokenCreationDate int `json:"tokenCreationDate"`
|
||||
AppAppleId string `json:"appAppleId"`
|
||||
BundleId string `json:"bundleId"`
|
||||
}
|
||||
|
||||
type NotificationSummary struct {
|
||||
RequestIdentifier string `json:"requestIdentifier"`
|
||||
AppAppleId string `json:"appAppleId"`
|
||||
BundleId string `json:"bundleId"`
|
||||
ProductId string `json:"productId"`
|
||||
Environment string `json:"environment"`
|
||||
StoreFrontCountryCodes []string `json:"storefrontCountryCodes"`
|
||||
FailedCount int64 `json:"failedCount"`
|
||||
SucceededCount int64 `json:"succeededCount"`
|
||||
}
|
||||
|
||||
type NotificationData struct {
|
||||
AppAppleId int `json:"appAppleId"`
|
||||
BundleId string `json:"bundleId"`
|
||||
BundleVersion string `json:"bundleVersion"`
|
||||
Environment string `json:"environment"`
|
||||
SignedRenewalInfo string `json:"signedRenewalInfo"`
|
||||
SignedTransactionInfo string `json:"signedTransactionInfo"`
|
||||
Status int32 `json:"status"`
|
||||
ConsumptionRequestReason string `json:"consumptionRequestReason,omitempty"`
|
||||
}
|
||||
|
||||
type TransactionInfo struct {
|
||||
jwt.StandardClaims
|
||||
AppAccountToken string `json:"appAccountToken"`
|
||||
BundleId string `json:"bundleId"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Environment string `json:"environment"`
|
||||
ExpiresDate int `json:"expiresDate"`
|
||||
InAppOwnershipType string `json:"inAppOwnershipType"`
|
||||
IsUpgraded bool `json:"isUpgraded"`
|
||||
OfferDiscountType string `json:"offerDiscountType"`
|
||||
OfferIdentifier string `json:"offerIdentifier"`
|
||||
OfferType int32 `json:"offerType"`
|
||||
OriginalPurchaseDate int `json:"originalPurchaseDate"`
|
||||
OriginalTransactionId string `json:"originalTransactionId"`
|
||||
Price int64 `json:"price,omitempty"`
|
||||
ProductId string `json:"productId"`
|
||||
PurchaseDate int `json:"purchaseDate"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
RevocationDate int `json:"revocationDate"`
|
||||
RevocationReason int32 `json:"revocationReason"`
|
||||
SignedDate int `json:"signedDate"`
|
||||
StoreFront string `json:"storefront"`
|
||||
StoreFrontId string `json:"storefrontId"`
|
||||
SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"`
|
||||
TransactionId string `json:"transactionId"`
|
||||
TransactionReason string `json:"transactionReason"`
|
||||
Type string `json:"type"`
|
||||
WebOrderLineItemId string `json:"webOrderLineItemId"`
|
||||
}
|
||||
|
||||
type RenewalInfo struct {
|
||||
jwt.StandardClaims
|
||||
AutoRenewProductId string `json:"autoRenewProductId"`
|
||||
AutoRenewStatus int32 `json:"autoRenewStatus"`
|
||||
Environment string `json:"environment"`
|
||||
ExpirationIntent int32 `json:"expirationIntent"`
|
||||
GracePeriodExpiresDate int `json:"gracePeriodExpiresDate"`
|
||||
IsInBillingRetryPeriod bool `json:"isInBillingRetryPeriod"`
|
||||
OfferIdentifier string `json:"offerIdentifier"`
|
||||
OfferType int32 `json:"offerType"`
|
||||
OriginalTransactionId string `json:"originalTransactionId"`
|
||||
PriceIncreaseStatus int32 `json:"priceIncreaseStatus"`
|
||||
ProductId string `json:"productId"`
|
||||
RecentSubscriptionStartDate int `json:"recentSubscriptionStartDate"`
|
||||
RenewalDate int `json:"renewalDate"`
|
||||
SignedDate int `json:"signedDate"`
|
||||
}
|
||||
53
logger.go
Normal file
53
logger.go
Normal file
@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/natefinch/lumberjack"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// Logger 是全局日志器
|
||||
var logger *zap.Logger
|
||||
|
||||
func initLogger(filename string, maxsize int, maxbackups int, maxage int, compress bool, level string) {
|
||||
// 日志文件切割配置
|
||||
logWriter := zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: filename, // 日志文件位置
|
||||
MaxSize: maxsize, // 每个日志文件保存的最大尺寸 单位:M
|
||||
MaxBackups: maxbackups, // 日志文件最多保存多少个备份
|
||||
MaxAge: maxage, // 文件最多保存多少天
|
||||
Compress: compress, // 是否压缩
|
||||
})
|
||||
|
||||
// 编码器配置
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
FunctionKey: "func",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码器
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder, // ISO8601 UTC 时间格式
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder, // 短路径编码器
|
||||
}
|
||||
|
||||
// 设置日志级别
|
||||
// UnmarshalText unmarshals the text to an AtomicLevel. It uses the same text representations as the static
|
||||
// zapcore.Levels ("debug", "info", "warn", "error", "dpanic", "panic", and "fatal").
|
||||
atomicLevel := zap.NewAtomicLevel()
|
||||
atomicLevel.UnmarshalText([]byte(level))
|
||||
//atomicLevel.SetLevel(zap.InfoLevel)
|
||||
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig), // 编码器配置
|
||||
logWriter, // 日志写入器
|
||||
atomicLevel, // 日志级别
|
||||
)
|
||||
|
||||
// 初始化日志器
|
||||
logger = zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
|
||||
}
|
||||
228
logs/app.log
Normal file
228
logs/app.log
Normal file
File diff suppressed because one or more lines are too long
67
logs/echo.log
Normal file
67
logs/echo.log
Normal file
@ -0,0 +1,67 @@
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-01T19:05:10+08:00","uri":"/pub/iap/callback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-01T19:24:23+08:00","uri":"/pub/iap/callback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-01T19:24:38+08:00","uri":"/internal/iap/history","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T16:48:49+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T16:54:57+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":500,"time":"2024-07-02T17:20:21+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T17:24:08+08:00","uri":"/pub/iap/callback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T17:29:08+08:00","uri":"/pub/iap/callback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T17:30:30+08:00","uri":"/pub/iap/callback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-02T17:37:40+08:00","uri":"/grammar/feedback","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":500,"time":"2024-07-02T18:21:48+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":500,"time":"2024-07-02T18:22:29+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-03T11:52:24+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-03T11:53:23+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-03T18:29:58+08:00","uri":"/user/get","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":500,"time":"2024-07-04T08:46:11+08:00","uri":"/user/get","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":500,"time":"2024-07-04T09:03:49+08:00","uri":"/user/get","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T09:05:34+08:00","uri":"/user/get","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T09:12:22+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T09:36:49+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T11:10:46+08:00","uri":"/iap/verify","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:15:31+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:21:29+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:21:33+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:24:02+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:24:02+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:26:46+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:26:48+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:29:29+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:29:31+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:32:41+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:32:43+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:37:03+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:37:04+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:39:05+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:39:05+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:42:24+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:42:25+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:45:15+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:45:16+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:48:36+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:48:37+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:51:16+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:51:18+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:53:33+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:54:18+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:54:19+08:00","uri":"/iap/verify","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T17:57:38+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T19:07:40+08:00","uri":"/grammar/translate","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T19:08:08+08:00","uri":"/grammar/grammar","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-04T19:10:37+08:00","uri":"/grammar/words","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T08:37:44+08:00","uri":"/user/get","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T08:50:41+08:00","uri":"/grammar/grammar","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T08:56:16+08:00","uri":"/grammar/grammar","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T08:58:28+08:00","uri":"/grammar/words","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T08:59:33+08:00","uri":"/grammar/translate","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:03:06+08:00","uri":"/internal/user/rights","user_agent":""}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:05:35+08:00","uri":"/internal/user/rights?ID=10004","user_agent":""}
|
||||
{"level":"fatal","msg":"Failed to start server{error 26 0 listen tcp :80: bind: address already in use}","time":"2024-07-05T09:32:29+08:00"}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:33:56+08:00","uri":"/internal/user/rights/reset?ID=10004","user_agent":""}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:34:05+08:00","uri":"/internal/user/rights?ID=10004","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:34:14+08:00","uri":"/grammar/translate","user_agent":"AIGrammar/1.0 (com.easyprompts.aigrammar; build:1; iOS 16.7.8) Alamofire/5.9.1"}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-07-05T09:45:11+08:00","uri":"/internal/user/rights?ID=10004","user_agent":""}
|
||||
{"level":"info","method":"GET","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-08-01T17:49:18+08:00","uri":"/internal/user/rights?ID=10004","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-08-01T17:49:38+08:00","uri":"/grammar/words","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-08-01T18:56:25+08:00","uri":"/grammar/words","user_agent":""}
|
||||
{"level":"info","method":"POST","msg":"request log","remote_ip":"192.168.65.1","status":200,"time":"2024-08-01T18:56:35+08:00","uri":"/grammar/feedback","user_agent":""}
|
||||
167
main.go
Normal file
167
main.go
Normal file
@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/golang-jwt/jwt"
|
||||
echojwt "github.com/labstack/echo-jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/natefinch/lumberjack"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 私有变量,只能在main包内访问
|
||||
var jwtSigningKey []byte
|
||||
|
||||
func main() {
|
||||
|
||||
configManager, err := GetConfigManager()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
|
||||
os.Exit(1) // Exit the program with an error code
|
||||
}
|
||||
|
||||
logconfig := configManager.GetLogConfig()
|
||||
initLogger(logconfig.LogFile, logconfig.MaxSize, logconfig.MaxBackups, logconfig.MaxAge, logconfig.Compress, logconfig.Level) // 初始化全局日志
|
||||
defer logger.Sync() // 刷到磁盘
|
||||
|
||||
// 初始化数据库,并创建实例
|
||||
dbManager, errdb := GetDBManager()
|
||||
if errdb != nil {
|
||||
logger.Fatal("DBManager init error", zap.Error(errdb)) // 记录错误信息
|
||||
os.Exit(1) // Exit the program with an error code
|
||||
} else {
|
||||
logger.Info("DBManager init successfully. Mysql ", zap.String("mysql_conn", configManager.GetDatabaseConfig().MysqlConn))
|
||||
}
|
||||
defer dbManager.CloseDB()
|
||||
|
||||
// JWT密钥,写到配置文件中
|
||||
baseConfig := configManager.GetBaseConfig()
|
||||
jwtSigningKey = []byte(baseConfig.JwtSecret) // 设置JWT密钥
|
||||
|
||||
e := echo.New()
|
||||
|
||||
// 处理日志,格式可定义,日志输出到文件,且文件自动分割
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&logrus.JSONFormatter{})
|
||||
|
||||
// Configure Lumberjack for log rotation
|
||||
logOutput := &lumberjack.Logger{
|
||||
Filename: logconfig.EchoLogFile,
|
||||
MaxSize: logconfig.MaxSize, // megabytes
|
||||
MaxBackups: logconfig.MaxBackups,
|
||||
MaxAge: logconfig.MaxAge, // days
|
||||
Compress: logconfig.Compress, // disabled by default
|
||||
}
|
||||
|
||||
// Set output of logger to both stdout and Lumberjack
|
||||
multiWriter := io.MultiWriter(os.Stdout, logOutput)
|
||||
logger.SetOutput(multiWriter)
|
||||
|
||||
// Custom log format middleware
|
||||
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
LogStatus: true,
|
||||
LogURI: true,
|
||||
LogMethod: true,
|
||||
LogRemoteIP: true,
|
||||
LogUserAgent: true,
|
||||
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"status": v.Status,
|
||||
"uri": v.URI,
|
||||
"method": v.Method,
|
||||
"remote_ip": c.RealIP(),
|
||||
"user_agent": v.UserAgent,
|
||||
}).Info("request log")
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
|
||||
// 全局中间件
|
||||
//e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(unifiedResponseHandler) // Register the unified response handler
|
||||
e.HTTPErrorHandler = customHTTPErrorHandler
|
||||
|
||||
// 定义一组无需jwt鉴权的处理功能
|
||||
s := e.Group("/pub")
|
||||
s.GET("/ping", PingHander)
|
||||
s.POST("/iap/callback", IapCallbackHandler)
|
||||
|
||||
// 处理应用商店的回调
|
||||
p := e.Group("/")
|
||||
// 使用 echo-jwt 替换 deprecated JWT middleware
|
||||
p.Use(echojwt.WithConfig(echojwt.Config{
|
||||
SigningKey: baseConfig.JwtSecret,
|
||||
ContextKey: "user",
|
||||
ParseTokenFunc: parseToken,
|
||||
}))
|
||||
p.POST("grammar/translate", TranslateHandler)
|
||||
p.POST("grammar/grammar", GrammarHandler)
|
||||
p.POST("grammar/words", WordsHandler)
|
||||
p.POST("grammar/feedback", TranslateFeedBackHandler)
|
||||
|
||||
p.POST("iap/verify", IapVerify)
|
||||
p.POST("user/get", queryUserHandler)
|
||||
|
||||
i := e.Group("/internal")
|
||||
i.POST("/iap/history", IapHistory)
|
||||
i.GET("/user/rights", UserRightsHandler)
|
||||
i.GET("/user/rights/reset", ResetUserRightsHandler)
|
||||
|
||||
// 启动服务器
|
||||
logger.Fatal("Failed to start server", zap.Error(e.Start(baseConfig.BindAddr)))
|
||||
//e.Logger.Fatal(e.Start(baseConfig.BindAddr))
|
||||
}
|
||||
|
||||
// 自定义的JWT Claims结构
|
||||
type jwtCustomClaims struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
GID int `json:"gid"`
|
||||
Exp1 int64 `json:"exp1"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
func parseToken(c echo.Context, auth string) (interface{}, error) {
|
||||
logger.Debug("into func")
|
||||
|
||||
token, err := jwt.ParseWithClaims(auth, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSigningKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("ParaseToken Error.", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*jwtCustomClaims); ok && token.Valid {
|
||||
logger.Info("claims: ", zap.Any("claims", claims))
|
||||
// 判断token有效期
|
||||
if time.Now().Unix() > claims.Exp1 {
|
||||
return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token expired")
|
||||
}
|
||||
|
||||
// 设置参数
|
||||
c.Set(KEY_DEVICEID, claims.DeviceID)
|
||||
c.Set(KEY_GID, claims.GID)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// 处理翻译请求
|
||||
func PingHander(c echo.Context) error {
|
||||
setResponse(c, map[string]string{"pang": "ok"})
|
||||
return nil
|
||||
}
|
||||
25
prompts.go
Normal file
25
prompts.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
// 定义几个不可修改的长字符串常量
|
||||
const (
|
||||
// ExamplePrompt1 包含逗号和句号
|
||||
TranslatePromptTemplate = `You are a professional translator. You need to translate the text the user provided into %s `
|
||||
|
||||
// ExamplePrompt2 包含换行符和特殊格式
|
||||
GrammarPromptTemplate = `You are a language expert. I will send you a piece of English text, which may contain incorrect grammar or misspelled words.
|
||||
You need to mark the errors, give the reasons for the errors, and provide suggestions for revisions.
|
||||
Please output the result in the form of an array, in the following format:
|
||||
[{"plain":"%s", "type":"%s", "reason":"%s", correction":["%s", "%s"]}, {"plain":"%s", "type":"%s", "reason":"%s", "correction":["%s", "%s"]}, ]
|
||||
The value corresponding to "plain" is a fragment of the input English text,
|
||||
the value corresponding to "type" is one of "ok", "grammar", and "spell", which respectively correspond to correct, grammatical error, and spelling error;
|
||||
the value corresponding to "reason" is a description of the modification opinion, which should be kept short within 10 words;
|
||||
the value corresponding to "correction" is an array, which is filled with our suggestions for the above errors and modifications. There can be multiple suggestions, so we use an array to represent them. Please do not exceed 5.
|
||||
If there are no errors in the text, please just type the word OK. Please do not add any explanations. `
|
||||
//If you understand my requirements, please answer OK without explanation.`
|
||||
|
||||
// ExamplePrompt3 是另一个复杂的例子
|
||||
WordsPromptTemplate = `You are an English learning expert. I will give you some words. Please explain the meaning of the word in English, then give common phrases about it, and finally output its synonyms.
|
||||
Please note that each item does not need to be more than 5.
|
||||
The result we need is in json format, and the style is: {"word":"$word", "explain":["$exp1", "exp2", ...], "phrase": ["$p1", "$p2", "p3", ...], "sync": ["$s1", "s2", ...]}
|
||||
Please just output the result according to the format, no explanation is needed.`
|
||||
)
|
||||
93
shell/sql
Normal file
93
shell/sql
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
CREATE TABLE aigrammar.`user` (
|
||||
ID INT UNSIGNED DEFAULT 10000 auto_increment NOT NULL,
|
||||
UserID varchar(100) NULL COMMENT 'UserID',
|
||||
UserName varchar(100) NULL COMMENT 'username',
|
||||
DeviceID varchar(256) NULL COMMENT 'DeviceID',
|
||||
RegChannel varchar(100) NULL COMMENT 'Email, Apple, Google',
|
||||
OpenID varchar(100) NULL COMMENT 'ID from other channels',
|
||||
RegTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
UpdateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
CONSTRAINT user_pk PRIMARY KEY (ID)
|
||||
)
|
||||
ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
AUTO_INCREMENT 10000
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
|
||||
ALTER TABLE aigrammar.`user` MODIFY COLUMN UserID varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'UserID';
|
||||
ALTER TABLE aigrammar.`user` MODIFY COLUMN UserName varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'username';
|
||||
ALTER TABLE aigrammar.`user` MODIFY COLUMN RegChannel varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'Email, Apple, Google';
|
||||
ALTER TABLE aigrammar.`user` MODIFY COLUMN OpenID varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'ID from other channels';
|
||||
ALTER TABLE aigrammar.`user` MODIFY COLUMN DeviceID varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'DeviceID';
|
||||
|
||||
|
||||
CREATE TABLE aigrammar.vip (
|
||||
ID INT UNSIGNED NOT NULL,
|
||||
IsVIP INT DEFAULT 0 NULL COMMENT '1-VIP; 0-not vip',
|
||||
AppStore varchar(100) DEFAULT 'apple' NULL COMMENT 'apple;google',
|
||||
ProductID varchar(100) NULL,
|
||||
ProductType varchar(100) NULL COMMENT 'yearly;monthly;weekly;',
|
||||
Environment varchar(100) NULL COMMENT 'prod;sandbox',
|
||||
PurchaseDate TIMESTAMP NULL,
|
||||
Price INT NULL,
|
||||
Currency varchar(100) NULL,
|
||||
Storefront varchar(100) NULL COMMENT 'USA',
|
||||
ExpDate TIMESTAMP NULL,
|
||||
AutoRenew INT NULL COMMENT '1-yes;0-no',
|
||||
OriginalTransactionID varchar(100) NULL COMMENT 'applestore originalTransactionId',
|
||||
CONSTRAINT vip_pk PRIMARY KEY (ID)
|
||||
)
|
||||
ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
|
||||
CREATE TABLE aigrammar.order_log (
|
||||
LogID INT UNSIGNED auto_increment NOT NULL,
|
||||
AppStore varchar(100) NULL COMMENT 'apple;google',
|
||||
NotificationType varchar(100) NULL,
|
||||
Subtype varchar(100) NULL,
|
||||
Environment varchar(100) NULL COMMENT 'product;sandbox',
|
||||
AppAccountToken varchar(100) NULL,
|
||||
CreateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
TransactionInfo TEXT NULL,
|
||||
RenewalInfo TEXT NULL,
|
||||
Payload TEXT NULL,
|
||||
CONSTRAINT oder_log_pk PRIMARY KEY (LogID)
|
||||
)
|
||||
ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
|
||||
CREATE TABLE aigrammar.product (
|
||||
ProductID varchar(100) NOT NULL,
|
||||
AppStore varchar(100) DEFAULT 'apple' NOT NULL,
|
||||
Duration INT DEFAULT 0 NULL COMMENT '订阅天数',
|
||||
ProductName varchar(100) NULL COMMENT '自定义商品名称',
|
||||
Price INT NULL COMMENT '定价,分',
|
||||
Currency varchar(100) NULL COMMENT '币种',
|
||||
CONSTRAINT product_pk PRIMARY KEY (ProductID,AppStore)
|
||||
)
|
||||
ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
|
||||
|
||||
CREATE TABLE aigrammar.feedback_log (
|
||||
LogID INT UNSIGNED auto_increment NOT NULL,
|
||||
AppStore varchar(100) NULL,
|
||||
Product varchar(100) NULL,
|
||||
`Input` TEXT NULL,
|
||||
`Output` TEXT NULL,
|
||||
`Result` varchar(100) NULL,
|
||||
UserID INT UNSIGNED NULL,
|
||||
CreateTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
CONSTRAINT feedback_log_pk PRIMARY KEY (LogID)
|
||||
)
|
||||
ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_0900_ai_ci;
|
||||
271
translate.go
Normal file
271
translate.go
Normal file
@ -0,0 +1,271 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 处理翻译请求
|
||||
func TranslateHandler(c echo.Context) error {
|
||||
// 验证参数
|
||||
var request struct {
|
||||
Input string `json:"input" form:"input"`
|
||||
Lang string `json:"lang" form:"lang"`
|
||||
}
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("bind request error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if request.Lang == "" {
|
||||
request.Lang = "Chinese" // 默认语言为中文
|
||||
}
|
||||
prompt := fmt.Sprintf(TranslatePromptTemplate, request.Lang)
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
|
||||
// 检查是否有权限
|
||||
if can, _ := queryUserBenefits(c); !can {
|
||||
logger.Error("user beyond limit.", zap.Int("ID", GID), zap.String("input", request.Input))
|
||||
setErrResponse(c, ERR_BENIFIT_FREE_LIMIT, "no benifits left.")
|
||||
return nil
|
||||
}
|
||||
|
||||
translation, err, errcode := gTranslate(request.Input, prompt)
|
||||
if err != nil {
|
||||
logger.Error("query error.", zap.Int("ID", GID), zap.Error(err))
|
||||
setErrResponse(c, errcode, "server timeout. please try again.")
|
||||
return nil
|
||||
}
|
||||
logger.Info("translation", zap.Int("ID", GID), zap.String("input", request.Input), zap.String("output", translation))
|
||||
|
||||
// 返回结果
|
||||
setResponse(c, map[string]string{"translation": translation})
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理语法改错请求
|
||||
func GrammarHandler(c echo.Context) error {
|
||||
var request struct {
|
||||
Input string `json:"input" form:"input"`
|
||||
Lang string `json:"lang" form:"lang"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("bind request error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
// 检查是否有权限
|
||||
if can, _ := queryUserBenefits(c); !can {
|
||||
logger.Error("user beyond limit.", zap.Int("ID", GID), zap.String("input", request.Input))
|
||||
setErrResponse(c, ERR_BENIFIT_FREE_LIMIT, "no benifits left.")
|
||||
return nil
|
||||
}
|
||||
|
||||
translation, err, errcode := gTranslate(request.Input, GrammarPromptTemplate)
|
||||
if err != nil {
|
||||
logger.Error("query error.", zap.Int("ID", GID), zap.Error(err))
|
||||
setErrResponse(c, errcode, "server timeout. please try again.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 判断 translation 是否为 "OK"
|
||||
if strings.EqualFold(translation, "OK") {
|
||||
logger.Info("grammar ok", zap.Int("ID", GID), zap.String("input", request.Input), zap.Any("output", translation))
|
||||
setErrResponse(c, ERR_GRAMMAR_OK, "grammar ok")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 验证 translation 是否为有效的 JSON 字符串
|
||||
var jsonObj interface{}
|
||||
if err := json.Unmarshal([]byte(translation), &jsonObj); err != nil {
|
||||
// 记录日志
|
||||
logger.Error("not json format", zap.Int("ID", GID), zap.String("input", request.Input), zap.String("output", translation))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Translation is not a valid JSON string: "+err.Error())
|
||||
}
|
||||
|
||||
// 由于 translation 已经是 JSON 字符串,直接以原样返回
|
||||
logger.Info("grammar", zap.Int("ID", GID), zap.String("input", request.Input), zap.Any("output", jsonObj))
|
||||
setResponse(c, jsonObj)
|
||||
return nil
|
||||
//return c.JSONBlob(http.StatusOK, []byte(translation))
|
||||
}
|
||||
|
||||
// 处理单词解释请求
|
||||
func WordsHandler(c echo.Context) error {
|
||||
var request struct {
|
||||
Input string `json:"input" form:"input"`
|
||||
Lang string `json:"lang" form:"lang"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("bind request error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
// 检查是否有权限
|
||||
if can, _ := queryUserBenefits(c); !can {
|
||||
logger.Error("user beyond limit.", zap.Int("ID", GID), zap.String("input", request.Input))
|
||||
setErrResponse(c, ERR_BENIFIT_FREE_LIMIT, "no benifits left.")
|
||||
return nil
|
||||
}
|
||||
|
||||
translation, err, errcode := gTranslate(request.Input, WordsPromptTemplate)
|
||||
if err != nil {
|
||||
logger.Error("query error.", zap.Int("ID", GID), zap.Error(err))
|
||||
setErrResponse(c, errcode, "server timeout. please try again.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 验证 translation 是否为有效的 JSON 字符串
|
||||
var jsonObj interface{}
|
||||
if err := json.Unmarshal([]byte(translation), &jsonObj); err != nil {
|
||||
// 记录日志
|
||||
logger.Error("not json format", zap.Int("ID", GID), zap.String("input", request.Input), zap.String("output", translation))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Translation is not a valid JSON string: "+err.Error())
|
||||
}
|
||||
logger.Info("words", zap.Int("ID", GID), zap.String("input", request.Input), zap.Any("output", jsonObj))
|
||||
|
||||
// 由于 translation 已经是 JSON 字符串,直接以原样返回
|
||||
setResponse(c, jsonObj)
|
||||
return nil
|
||||
|
||||
//return c.JSONBlob(http.StatusOK, []byte(translation))
|
||||
}
|
||||
|
||||
// 处理用户的反馈
|
||||
func TranslateFeedBackHandler(c echo.Context) error {
|
||||
var request struct {
|
||||
Product string `json:"product" form:"product"`
|
||||
Input string `json:"input" form:"input"`
|
||||
Output string `json:"output" form:"output"`
|
||||
Result string `json:"res" form:"res"`
|
||||
}
|
||||
|
||||
if err := c.Bind(&request); err != nil {
|
||||
logger.Error("bind request error.", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
|
||||
logger.Info("feedback", zap.Int("ID", GID), zap.Any("feedback", request))
|
||||
// TODO: 写入到 feedback_log 表中
|
||||
|
||||
setResponse(c, nil)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// gTranslate 调用Azure OpenAI的翻译接口
|
||||
func gTranslate(input string, prompt string) (string, error, int) {
|
||||
// get azure openai config
|
||||
configManager, err := GetConfigManager()
|
||||
if err != nil {
|
||||
logger.Error("GetConfigManager error.", zap.Error(err))
|
||||
return "", errors.New("Get Config error."), ERR_COMM_SVR_WRONG
|
||||
}
|
||||
azureConfig := configManager.GetAzureConfig()
|
||||
|
||||
azureOpenAIKey := azureConfig.Keys[0]
|
||||
modelDeploymentID := azureConfig.GPT4Model
|
||||
azureOpenAIEndpoint := azureConfig.Endpoint
|
||||
|
||||
// API密钥认证
|
||||
cred := azcore.NewKeyCredential(azureOpenAIKey)
|
||||
|
||||
// 创建客户端
|
||||
client, err := azopenai.NewClientWithKeyCredential(azureOpenAIEndpoint, cred, nil)
|
||||
if err != nil {
|
||||
logger.Error("NewClientWithKeyCredential error", zap.Error(err))
|
||||
return "", err, ERR_COMM_SVR_WRONG
|
||||
}
|
||||
|
||||
// This is a conversation in progress.
|
||||
// NOTE: all messages, regardless of role, count against token usage for this API.
|
||||
messages := []azopenai.ChatRequestMessageClassification{
|
||||
// You set the tone and rules of the conversation with a prompt as the system role.
|
||||
&azopenai.ChatRequestSystemMessage{Content: to.Ptr(prompt)},
|
||||
|
||||
// The user asks a question
|
||||
//&azopenai.ChatRequestUserMessage{Content: azopenai.NewChatRequestUserMessageContent("Can you help me?")},
|
||||
|
||||
// The reply would come back from the ChatGPT. You'd add it to the conversation so we can maintain context.
|
||||
//&azopenai.ChatRequestAssistantMessage{Content: to.Ptr("the user's text is shown below.")},
|
||||
|
||||
// The user answers the question based on the latest reply.
|
||||
&azopenai.ChatRequestUserMessage{Content: azopenai.NewChatRequestUserMessageContent(input)},
|
||||
|
||||
// from here you'd keep iterating, sending responses back from ChatGPT
|
||||
}
|
||||
|
||||
gotReply := false
|
||||
var resultTextBuilder strings.Builder
|
||||
|
||||
resp, err := client.GetChatCompletions(context.TODO(), azopenai.ChatCompletionsOptions{
|
||||
// This is a conversation in progress.
|
||||
// NOTE: all messages count against token usage for this API.
|
||||
Messages: messages,
|
||||
DeploymentName: &modelDeploymentID,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("GetChatCompletions error", zap.String("input", input), zap.Error(err))
|
||||
return "", err, ERR_COMM_SVR_WRONG
|
||||
}
|
||||
|
||||
for _, choice := range resp.Choices {
|
||||
gotReply = true
|
||||
|
||||
// 被过滤了,需要做个判断
|
||||
if choice.ContentFilterResults != nil {
|
||||
var filter_err = errors.New("no content")
|
||||
|
||||
if choice.ContentFilterResults.Error != nil {
|
||||
filter_err = choice.ContentFilterResults.Error
|
||||
//fmt.Fprintf(os.Stderr, " Error:%v\n", choice.ContentFilterResults.Error)
|
||||
}
|
||||
|
||||
if *choice.ContentFilterResults.Sexual.Filtered || *choice.ContentFilterResults.Violence.Filtered {
|
||||
filter_err = errors.New("Sexual or Violence input")
|
||||
return "", filter_err, ERR_DIRTY_CONTENT
|
||||
}
|
||||
logger.Warn("filterd", zap.Any("Hate", *choice.ContentFilterResults.Hate.Severity), zap.Any("Hate-filtered", *choice.ContentFilterResults.Hate.Filtered),
|
||||
zap.Any("SelfHarm", *choice.ContentFilterResults.SelfHarm.Severity), zap.Any("SelfHarm-filtered", *choice.ContentFilterResults.Hate.Filtered),
|
||||
zap.Any("Sexual", *choice.ContentFilterResults.Sexual.Severity), zap.Any("Sexual-filtered", *choice.ContentFilterResults.Sexual.Filtered),
|
||||
zap.Any("Violence", *choice.ContentFilterResults.Violence.Severity), zap.Any("Violence-filtered", *choice.ContentFilterResults.Violence.Filtered),
|
||||
zap.Any("Error", filter_err), zap.String("input", input))
|
||||
|
||||
//fmt.Fprintf(os.Stderr, " Hate: sev: %v, filtered: %v\n", *choice.ContentFilterResults.Hate.Severity, *choice.ContentFilterResults.Hate.Filtered)
|
||||
//fmt.Fprintf(os.Stderr, " SelfHarm: sev: %v, filtered: %v\n", *choice.ContentFilterResults.SelfHarm.Severity, *choice.ContentFilterResults.SelfHarm.Filtered)
|
||||
//fmt.Fprintf(os.Stderr, " Sexual: sev: %v, filtered: %v\n", *choice.ContentFilterResults.Sexual.Severity, *choice.ContentFilterResults.Sexual.Filtered)
|
||||
//fmt.Fprintf(os.Stderr, " Violence: sev: %v, filtered: %v\n", *choice.ContentFilterResults.Violence.Severity, *choice.ContentFilterResults.Violence.Filtered)
|
||||
}
|
||||
|
||||
if choice.Message != nil && choice.Message.Content != nil {
|
||||
//fmt.Fprintf(os.Stderr, "Content[%d]: %s\n", *choice.Index, *choice.Message.Content)
|
||||
resultTextBuilder.WriteString(*choice.Message.Content)
|
||||
}
|
||||
|
||||
if choice.FinishReason != nil {
|
||||
// this choice's conversation is complete.
|
||||
logger.Debug("Finish Reason", zap.Any("index", *choice.Index), zap.Any("reason", *choice.FinishReason))
|
||||
//fmt.Fprintf(os.Stderr, "Finish reason[%d]: %s\n", *choice.Index, *choice.FinishReason)
|
||||
}
|
||||
}
|
||||
|
||||
if !gotReply {
|
||||
return "", errors.New("Got chat completions reply"), ERR_COMM_SVR_WRONG
|
||||
}
|
||||
return resultTextBuilder.String(), nil, 0
|
||||
}
|
||||
263
user.go
Normal file
263
user.go
Normal file
@ -0,0 +1,263 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/awa/go-iap/appstore/api"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserResponse struct {
|
||||
ID int `json:"id"`
|
||||
UserID string `json:"userid"`
|
||||
UserName string `json:"username"`
|
||||
VIP int `json:"vip"`
|
||||
}
|
||||
|
||||
// TODO: 以后引入 GORM mysql driver ,简化数据库操作。
|
||||
|
||||
func queryUserHandler(c echo.Context) error {
|
||||
// 从 context 中获取变量
|
||||
deviceID := c.Get(KEY_DEVICEID).(string)
|
||||
GID, _ := c.Get(KEY_GID).(int)
|
||||
|
||||
db, _ := GetDBManager()
|
||||
var response UserResponse
|
||||
|
||||
// 查询 user 表
|
||||
err := db.MySQL.QueryRow("SELECT ID, UserID, UserName FROM user WHERE DeviceID = ?", deviceID).Scan(&response.ID, &response.UserID, &response.UserName)
|
||||
if err == sql.ErrNoRows {
|
||||
// 用户不存在,创建新用户
|
||||
// TODO: 这里要不要自动分配userid,这个userid在内部基本不会用到
|
||||
res, err := db.MySQL.Exec("INSERT INTO user (DeviceID) VALUES (?)", deviceID)
|
||||
if err != nil {
|
||||
logger.Error("insert db error", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user")
|
||||
}
|
||||
lastID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
logger.Error("insert db error", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to retrieve last insert ID")
|
||||
}
|
||||
response.ID = int(lastID)
|
||||
response.UserName = ""
|
||||
logger.Debug("insert user", zap.Int("ID", response.ID), zap.String("DeviceID", deviceID))
|
||||
|
||||
} else if err != nil {
|
||||
logger.Error("query db error", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
// 看上传的 userID 跟数据库的是否一致 userID 转int ..
|
||||
if response.ID != GID {
|
||||
logger.Warn("userid not match", zap.Int("ID", response.ID), zap.Int("userGID", GID))
|
||||
//log.Printf("userid not match: %v != %v", numid, response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 vip 表
|
||||
err = db.MySQL.QueryRow("SELECT IsVIP FROM vip WHERE ID = ?", response.ID).Scan(&response.VIP)
|
||||
if err == sql.ErrNoRows {
|
||||
response.VIP = 0 // 默认非VIP
|
||||
} else if err != nil {
|
||||
logger.Error("query db error", zap.Error(err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
setResponse(c, response)
|
||||
return nil
|
||||
//return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// 查询用户的所有redis key,内部接口
|
||||
func UserRightsHandler(c echo.Context) error {
|
||||
// 获取 c 的 GET方法的参数
|
||||
ID, _ := strconv.Atoi(c.QueryParam("ID"))
|
||||
|
||||
db, _ := GetDBManager()
|
||||
ub := NewUserBenefits(db.Redis)
|
||||
|
||||
// 查询redis
|
||||
userData, err := ub.QueryUserBenefits(ID)
|
||||
if err != nil {
|
||||
logger.Error("QueryUserBenefits", zap.Error(err), zap.Int("ID", ID))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
logger.Debug("QueryUserBenefits", zap.Any("userData", userData), zap.Int("ID", ID))
|
||||
}
|
||||
|
||||
setResponse(c, userData)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查询用户的所有redis key,内部接口
|
||||
func ResetUserRightsHandler(c echo.Context) error {
|
||||
// 获取 c 的 GET方法的参数
|
||||
ID, _ := strconv.Atoi(c.QueryParam("ID"))
|
||||
datastr := c.QueryParam("datestr")
|
||||
|
||||
db, _ := GetDBManager()
|
||||
ub := NewUserBenefits(db.Redis)
|
||||
|
||||
// 查询redis
|
||||
err := ub.ResetUserBenefits(ID, datastr)
|
||||
if err != nil {
|
||||
logger.Error("ResetUserRightsHandler", zap.Error(err), zap.Int("ID", ID))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
logger.Debug("ResetUserRightsHandler", zap.Int("ID", ID))
|
||||
}
|
||||
|
||||
setResponse(c, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 编写查询用户是否VIP的函数,输入是GID,输出是vip,并且给出是否有error ,不需要使用 echo context
|
||||
func queryUserVIP(ID int) (int, error) {
|
||||
db, _ := GetDBManager()
|
||||
var vip int
|
||||
err := db.MySQL.QueryRow("SELECT IsVIP FROM vip WHERE ID = ?", ID).Scan(&vip)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil // 默认非VIP
|
||||
} else if err != nil {
|
||||
logger.Error("query db error", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
return vip, nil
|
||||
}
|
||||
|
||||
// 编写查询用户是否有权限使用某功能,输入是用户ID,如果用户是VIP,则有权限;否则查询 QueryUserBenefits 看是否超过免费限制,输出 是否有权限,以及是否出错。
|
||||
func queryUserBenefits(c echo.Context) (bool, error) {
|
||||
// 获取参数
|
||||
ID, _ := c.Get(KEY_GID).(int)
|
||||
timeZone := c.Request().Header.Get(KEY_HEADER_TIMEZONE)
|
||||
secondsFromGMT, _ := strconv.Atoi(c.Request().Header.Get(KEY_HEADER_SECONDSFROMGMT))
|
||||
|
||||
db, _ := GetDBManager()
|
||||
var vip int
|
||||
err := db.MySQL.QueryRow("SELECT IsVIP FROM vip WHERE ID = ?", ID).Scan(&vip)
|
||||
if err == sql.ErrNoRows {
|
||||
// 非VIP,查询redis的免费次数
|
||||
db, _ := GetDBManager()
|
||||
ub := NewUserBenefits(db.Redis)
|
||||
status, err := ub.CheckAndDecrement(ID, timeZone, secondsFromGMT)
|
||||
if err != nil {
|
||||
logger.Error("CheckAndDecrement", zap.Error(err), zap.Int("ID", ID), zap.String("timeZone", timeZone), zap.Int("secondsFromGMT", secondsFromGMT))
|
||||
return false, err
|
||||
} else {
|
||||
logger.Debug("CheckAndDecrement", zap.Int("ID", ID), zap.String("timeZone", timeZone), zap.Int("secondsFromGMT", secondsFromGMT), zap.Int("status", status))
|
||||
return status == 0, nil
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Error("query db error", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
if vip == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 从苹果校验订单后,插入vip表中
|
||||
func UpdateOrderByVerify(ID int, AppAcountToken string, OriginTransID string, transantion *api.JWSTransaction) error {
|
||||
// 写入vip表,如果ID对应记录不存在,则插入,否则更新
|
||||
db, _ := GetDBManager()
|
||||
|
||||
var ProductType, Currency string
|
||||
var Price, Duration int
|
||||
// 先从 product 表中,根据 transantion.ProductID 获取到对应的 Duration,ProductName, Price, Currency
|
||||
err := db.MySQL.QueryRow("SELECT ProductType, Price, Currency, Duration from product where ProductID = ?", transantion.ProductID).Scan(&ProductType, &Price, &Currency, &Duration)
|
||||
if err == sql.ErrNoRows {
|
||||
logger.Error("query productID empty.", zap.Error(err), zap.Int("ID", ID), zap.String("AppAcountToken", AppAcountToken), zap.String("OriginTransID", OriginTransID))
|
||||
return err
|
||||
} else if err != nil {
|
||||
logger.Error("query productID error", zap.Error(err), zap.Int("ID", ID), zap.String("AppAcountToken", AppAcountToken), zap.String("OriginTransID", OriginTransID))
|
||||
return err
|
||||
}
|
||||
|
||||
// 取当前的时间戳
|
||||
//purchase_time := time.Now().Unix()
|
||||
//exp_time := purchase_time + int64(Duration)*3600*24
|
||||
|
||||
currentTime := time.Now()
|
||||
nextDay := time.Now().AddDate(0, 0, Duration)
|
||||
|
||||
// TODO: transaction.TransactionReason 有新购和续费,需要区分;同一个购买或者续费事件,可能有通知多次,需要排重
|
||||
var tmpID int
|
||||
errDup := db.MySQL.QueryRow("SELECT ID from vip where TransactionID = ? and OriginalTransactionID = ? and IsVip = 1 and ExpDate > ?", transantion.TransactionID, transantion.OriginalTransactionId, currentTime).Scan(&tmpID)
|
||||
if errDup != sql.ErrNoRows {
|
||||
// 表示重复了,可以直接返回
|
||||
logger.Info("duplicate request", zap.Int("ID", ID), zap.String("AppAcountToken", AppAcountToken), zap.String("OriginTransID", OriginTransID), zap.String("TransactionID", transantion.TransactionID))
|
||||
return nil
|
||||
} else if errDup != nil {
|
||||
logger.Error("query error", zap.Error(errDup), zap.Int("ID", ID), zap.String("AppAcountToken", AppAcountToken), zap.String("OriginTransID", OriginTransID))
|
||||
// 这里不返回,继续尝试更新。
|
||||
}
|
||||
|
||||
// 更新到DB
|
||||
sql := `INSERT INTO vip (ID, IsVip, AppStore, ProductID, ProductType, Environment, Price, Currency, Storefront, PurchaseDate, ExpDate, AutoRenew, OriginalTransactionID, TransactionID, AppAccountToken, TransactionReason)
|
||||
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
IsVip = 1, AppStore = ?, ProductID = ?, ProductType = ?, Environment = ?, Price = ?, Currency = ?, Storefront = ?, PurchaseDate = ?, ExpDate = ?, AutoRenew = ?, OriginalTransactionID = ? , TransactionID = ?, AppAccountToken = ?, TransactionReason = ? `
|
||||
|
||||
_, err2 := db.MySQL.Exec(sql,
|
||||
ID, APPSTORE, transantion.ProductID, ProductType, transantion.Environment, Price, Currency, transantion.Storefront, currentTime, nextDay, 1, OriginTransID, transantion.TransactionID, transantion.AppAccountToken, transantion.TransactionReason,
|
||||
APPSTORE, transantion.ProductID, ProductType, transantion.Environment, Price, Currency, transantion.Storefront, currentTime, nextDay, 1, OriginTransID, transantion.TransactionID, transantion.AppAccountToken, transantion.TransactionReason)
|
||||
|
||||
if err2 != nil {
|
||||
logger.Error("UpdateOrderByVerify", zap.Error(err), zap.Int("ID", ID), zap.String("AppAcountToken", AppAcountToken), zap.String("OriginTransID", OriginTransID))
|
||||
return err2
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 接收到appstore的回调,写入数据。因为不知道对应的用户账号,所以只能记录。
|
||||
func UpdateOrderByNotify(Notification *AppStoreServerNotification) error {
|
||||
db, _ := GetDBManager()
|
||||
|
||||
// 先写order_log表
|
||||
sql := `INSERT INTO order_log (AppStore, NotificationType, Subtype, Environment, AppAccountToken, TransactionInfo, RenewalInfo, Payload)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?) `
|
||||
|
||||
// 需要把 Notification.TransactionInfo 转成 字符串
|
||||
TransactionInfo, _ := json.Marshal(Notification.TransactionInfo)
|
||||
RenewalInfo, _ := json.Marshal(Notification.RenewalInfo)
|
||||
Payload, _ := json.Marshal(Notification.Payload)
|
||||
|
||||
_, err := db.MySQL.Exec(sql,
|
||||
APPSTORE, Notification.Payload.NotificationType, Notification.Payload.Subtype, Notification.Payload.Data.Environment, Notification.TransactionInfo.AppAccountToken, TransactionInfo, RenewalInfo, Payload)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("UpdateOrderByNotify", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 给定 appaccounttoken,在order_log中查询是否已经存在了,如果存在,则无需向苹果发起验证请求
|
||||
func CheckOrderByAppAcountToken(AppAccountToken string) (bool, error) {
|
||||
db, _ := GetDBManager()
|
||||
|
||||
// 根据AppAccountToken查询 order_log表,查看记录是否存在,如果存在,返回true,否则false
|
||||
var LogID int
|
||||
err := db.MySQL.QueryRow("SELECT LogID from order_log where AppAccountToken = ? and AppStore = ? ", AppAccountToken, APPSTORE).Scan(&LogID)
|
||||
if err == sql.ErrNoRows {
|
||||
logger.Info("query empty", zap.String("AppAccountToken", AppAccountToken))
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
logger.Error("query error.", zap.Error(err), zap.String("AppAccountToken", AppAccountToken))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// TODO: 可以在这里更新 vip 表,这样 Verify 的过程中,如果查询到记录,就不需要再去appstore校验了。
|
||||
|
||||
return true, nil
|
||||
}
|
||||
139
userBenefits.go
Normal file
139
userBenefits.go
Normal file
@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
type UserBenefits struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewUserBenefits 初始化 UserBenefits 实例
|
||||
func NewUserBenefits(redisClient *redis.Client) *UserBenefits {
|
||||
return &UserBenefits{
|
||||
client: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndDecrement 检查用户是否可以访问并递减剩余次数,0表示可用,其他值表示异常
|
||||
func (ub *UserBenefits) CheckAndDecrement(ID int, timeZone string, secondsFromGMT int) (int, error) {
|
||||
loc, err := time.LoadLocation(timeZone)
|
||||
if err != nil {
|
||||
// 使用 secondsFromGMT 计算时区
|
||||
loc = time.FixedZone("Custom", secondsFromGMT)
|
||||
}
|
||||
currentTime := time.Now().In(loc)
|
||||
dateKey := currentTime.Format("20060102")
|
||||
|
||||
key := fmt.Sprintf("%d_%s", ID, dateKey)
|
||||
|
||||
// 获取当前计数
|
||||
val, err := ub.client.Get(ctx, key).Int()
|
||||
if err == redis.Nil {
|
||||
// 如果没有设置,初始化为3
|
||||
ub.client.Set(ctx, key, DAILY_FREE_COUNT, 24*time.Hour)
|
||||
val = DAILY_FREE_COUNT
|
||||
} else if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
// 检查次数是否已用完
|
||||
if val <= 0 {
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// 递减次数
|
||||
ub.client.Decr(ctx, key)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// QueryUserBenefits 查询用户使用情况
|
||||
func (ub *UserBenefits) QueryUserBenefits(ID int) (string, error) {
|
||||
keys, err := ub.client.Keys(ctx, fmt.Sprintf("%d_*", ID)).Result()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := make(map[string]int)
|
||||
for _, key := range keys {
|
||||
val, err := ub.client.Get(ctx, key).Int()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
// 将结果编码为 JSON 格式
|
||||
jsonData, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// ResetUserBenefits 删除指定日期或所有日期的用户权益,只限于内部调用
|
||||
func (ub *UserBenefits) ResetUserBenefits(ID int, dateStr string) error {
|
||||
var pattern string
|
||||
if dateStr == "" {
|
||||
// 删除该用户的所有权益
|
||||
pattern = fmt.Sprintf("%d_*", ID)
|
||||
} else {
|
||||
// 删除特定日期的权益
|
||||
pattern = fmt.Sprintf("%d_%s", ID, dateStr)
|
||||
}
|
||||
|
||||
keys, err := ub.client.Keys(ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
fmt.Println("No keys to delete.")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = ub.client.Del(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted keys for pattern %s\n", pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
func test_smain() {
|
||||
// Redis 客户端使用连接池初始化
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "172.18.0.4:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
PoolSize: 10, // 连接池大小
|
||||
})
|
||||
|
||||
ub := NewUserBenefits(rdb)
|
||||
|
||||
// 示例使用
|
||||
status, err := ub.CheckAndDecrement(10001, "Europe/Berlin", 0)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
} else {
|
||||
fmt.Println("Status Code:", status)
|
||||
}
|
||||
|
||||
// 查询用户使用情况
|
||||
userData, err := ub.QueryUserBenefits(10001)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
} else {
|
||||
fmt.Println("User Data:", userData)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user