Initial sanitized code sync

This commit is contained in:
zqq61
2026-03-15 20:48:19 +08:00
commit 17ee51ba04
126 changed files with 22546 additions and 0 deletions

171
internal/handler/account.go Normal file
View File

@@ -0,0 +1,171 @@
package handler
import (
"fmt"
"net/http"
"gpt-plus/internal/db"
"gpt-plus/internal/service"
"github.com/gin-gonic/gin"
)
func RegisterAccountRoutes(api *gin.RouterGroup) {
api.GET("/accounts", ListAccounts)
api.GET("/accounts/:id", GetAccount)
api.POST("/accounts/check", CheckAccounts)
api.POST("/accounts/:id/check", CheckSingleAccount)
api.POST("/accounts/test-model", TestModel)
api.PUT("/accounts/:id/note", UpdateAccountNote)
api.POST("/accounts/export", ExportAccounts)
api.POST("/accounts/:id/transfer-to-cpa", TransferToCPA)
}
func ListAccounts(c *gin.Context) {
d := db.GetDB()
query := d.Model(&db.Account{}).Order("created_at DESC")
plan := c.Query("plan")
if plan != "" && plan != "all" {
query = query.Where("plan = ?", plan)
}
// "all" or empty: show everything
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
if search := c.Query("search"); search != "" {
query = query.Where("email LIKE ?", "%"+search+"%")
}
p := db.PaginationParams{Page: intQuery(c, "page", 1), Size: intQuery(c, "size", 20)}
var accounts []db.Account
result, err := db.Paginate(query, p, &accounts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for i := range accounts {
if accounts[i].Plan == "team_owner" {
d.Where("parent_id = ?", accounts[i].ID).Find(&accounts[i].SubAccounts)
}
if accounts[i].Plan == "team_member" && accounts[i].ParentID != nil {
var parent db.Account
if d.First(&parent, *accounts[i].ParentID).Error == nil {
accounts[i].Parent = &parent
}
}
}
result.Items = accounts
c.JSON(http.StatusOK, result)
}
func GetAccount(c *gin.Context) {
var account db.Account
d := db.GetDB()
if err := d.First(&account, c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "账号不存在"})
return
}
if account.Plan == "team_owner" {
d.Where("parent_id = ?", account.ID).Find(&account.SubAccounts)
}
c.JSON(http.StatusOK, account)
}
func CheckAccounts(c *gin.Context) {
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
results := service.CheckAccountStatuses(db.GetDB(), req.IDs)
c.JSON(http.StatusOK, gin.H{"message": "状态检查完成", "results": results})
}
func CheckSingleAccount(c *gin.Context) {
id := c.Param("id")
var uid uint
if _, err := fmt.Sscanf(id, "%d", &uid); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
results := service.CheckAccountStatuses(db.GetDB(), []uint{uid})
if len(results) > 0 {
c.JSON(http.StatusOK, results[0])
} else {
c.JSON(http.StatusNotFound, gin.H{"error": "账号不存在"})
}
}
func TestModel(c *gin.Context) {
var req struct {
ID uint `json:"id" binding:"required"`
Model string `json:"model"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result := service.TestModelAvailability(db.GetDB(), req.ID, req.Model)
c.JSON(http.StatusOK, result)
}
func UpdateAccountNote(c *gin.Context) {
var req struct {
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.GetDB().Model(&db.Account{}).Where("id = ?", c.Param("id")).Update("note", req.Note).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "备注已更新"})
}
func TransferToCPA(c *gin.Context) {
id := c.Param("id")
var uid uint
if _, err := fmt.Sscanf(id, "%d", &uid); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
result := service.TransferAccountToCPA(db.GetDB(), uid)
if result.OK {
c.JSON(http.StatusOK, result)
} else {
c.JSON(http.StatusInternalServerError, result)
}
}
func ExportAccounts(c *gin.Context) {
var req struct {
IDs []uint `json:"ids" binding:"required"`
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
data, filename, err := service.ExportAccounts(db.GetDB(), req.IDs, req.Note)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"")
if len(req.IDs) > 1 {
c.Data(http.StatusOK, "application/zip", data)
} else {
c.Data(http.StatusOK, "application/json", data)
}
}

61
internal/handler/auth.go Normal file
View File

@@ -0,0 +1,61 @@
package handler
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
var adminPasswordHash []byte
func InitAdminPassword() error {
password := os.Getenv("ADMIN_PASSWORD")
if password == "" {
password = "admin"
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
adminPasswordHash = hash
return nil
}
type loginRequest struct {
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码不能为空"})
return
}
if err := bcrypt.CompareHashAndPassword(adminPasswordHash, []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "密码错误"})
return
}
token, err := GenerateToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
return
}
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie("token", token, 86400, "/", "", IsSecure(), true)
c.JSON(http.StatusOK, gin.H{"message": "登录成功"})
}
func Logout(c *gin.Context) {
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie("token", "", -1, "/", "", IsSecure(), true)
c.JSON(http.StatusOK, gin.H{"message": "已退出登录"})
}
func CheckAuth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"authenticated": true})
}

324
internal/handler/card.go Normal file
View File

@@ -0,0 +1,324 @@
package handler
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"gpt-plus/internal/db"
"gpt-plus/pkg/provider/card"
"github.com/gin-gonic/gin"
)
func RegisterCardRoutes(api *gin.RouterGroup) {
// Cards
api.GET("/cards", ListCards)
api.GET("/cards/active", GetActiveCard)
api.GET("/cards/stats", GetCardStats)
api.POST("/cards", AddCard)
api.PUT("/cards/:id/activate", ActivateCard)
api.PUT("/cards/:id/status", UpdateCardStatus)
api.DELETE("/cards/:id", DeleteCard)
// Card codes
api.GET("/card-codes", ListCardCodes)
api.GET("/card-codes/stats", GetCardCodeStats)
api.POST("/card-codes/import", ImportCardCodes)
api.POST("/card-codes/redeem", RedeemCardCode)
api.DELETE("/card-codes/:id", DeleteCardCode)
}
func ListCards(c *gin.Context) {
d := db.GetDB()
query := d.Model(&db.Card{}).Order("created_at DESC")
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
p := db.PaginationParams{
Page: intQuery(c, "page", 1),
Size: intQuery(c, "size", 20),
}
var cards []db.Card
result, err := db.Paginate(query, p, &cards)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for i := range cards {
number, _ := db.Decrypt(cards[i].NumberEnc)
cards[i].NumberLast4 = number
cvc, _ := db.Decrypt(cards[i].CVCEnc)
cards[i].CVCPlain = cvc
}
result.Items = cards
c.JSON(http.StatusOK, result)
}
func GetActiveCard(c *gin.Context) {
d := db.GetDB()
var card db.Card
if err := d.Where("status = ?", "active").First(&card).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "无激活的卡片"})
return
}
number, _ := db.Decrypt(card.NumberEnc)
card.NumberLast4 = number
cvc, _ := db.Decrypt(card.CVCEnc)
card.CVCPlain = cvc
c.JSON(http.StatusOK, card)
}
func GetCardStats(c *gin.Context) {
c.JSON(http.StatusOK, db.GetCardStats(db.GetDB()))
}
type addCardRequest struct {
Number string `json:"number" binding:"required"`
CVC string `json:"cvc" binding:"required"`
ExpMonth string `json:"exp_month" binding:"required"`
ExpYear string `json:"exp_year" binding:"required"`
Name string `json:"name"`
Country string `json:"country"`
}
func AddCard(c *gin.Context) {
var req addCardRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
numberEnc, _ := db.Encrypt(req.Number)
cvcEnc, _ := db.Encrypt(req.CVC)
maxBinds := 0 // 0 = unlimited
var cfg db.SystemConfig
if db.GetDB().Where("key = ?", "card.max_binds").First(&cfg).Error == nil {
maxBinds, _ = strconv.Atoi(cfg.Value)
}
card := &db.Card{
NumberHash: db.HashSHA256(req.Number),
NumberEnc: numberEnc,
CVCEnc: cvcEnc,
ExpMonth: req.ExpMonth,
ExpYear: req.ExpYear,
Name: req.Name,
Country: req.Country,
Source: "manual",
Status: "available",
MaxBinds: maxBinds,
}
if err := db.GetDB().Create(card).Error; err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
c.JSON(http.StatusConflict, gin.H{"error": "该卡号已存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": card.ID, "message": "卡片已添加"})
}
func ActivateCard(c *gin.Context) {
id := c.Param("id")
d := db.GetDB()
// Deactivate current active card
d.Model(&db.Card{}).Where("status = ?", "active").Update("status", "available")
var card db.Card
if err := d.First(&card, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "卡片不存在"})
return
}
card.Status = "active"
d.Save(&card)
c.JSON(http.StatusOK, gin.H{"message": "已激活"})
}
func UpdateCardStatus(c *gin.Context) {
id := c.Param("id")
var req struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Status != "disabled" && req.Status != "available" {
c.JSON(http.StatusBadRequest, gin.H{"error": "状态只能为 disabled 或 available"})
return
}
if err := db.GetDB().Model(&db.Card{}).Where("id = ?", id).Update("status", req.Status).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "状态已更新"})
}
func DeleteCard(c *gin.Context) {
if err := db.GetDB().Delete(&db.Card{}, c.Param("id")).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
// Card Codes
func ListCardCodes(c *gin.Context) {
d := db.GetDB()
query := d.Model(&db.CardCode{}).Order("created_at DESC")
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
p := db.PaginationParams{Page: intQuery(c, "page", 1), Size: intQuery(c, "size", 20)}
var codes []db.CardCode
result, err := db.Paginate(query, p, &codes)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func GetCardCodeStats(c *gin.Context) {
c.JSON(http.StatusOK, db.GetCardCodeStats(db.GetDB()))
}
func ImportCardCodes(c *gin.Context) {
var req struct {
Codes []string `json:"codes" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
d := db.GetDB()
var count int
for _, code := range req.Codes {
code = strings.TrimSpace(code)
if code == "" {
continue
}
cc := &db.CardCode{Code: code, Status: "unused"}
if d.Create(cc).Error == nil {
count++
}
}
c.JSON(http.StatusOK, gin.H{"imported": count})
}
func RedeemCardCode(c *gin.Context) {
var req struct {
ID uint `json:"id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
d := db.GetDB()
var code db.CardCode
if err := d.First(&code, req.ID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "卡密不存在"})
return
}
if code.Status != "unused" {
c.JSON(http.StatusBadRequest, gin.H{"error": "该卡密状态不是 unused"})
return
}
code.Status = "redeeming"
d.Save(&code)
// Redeem in background
go func() {
var cfgBase db.SystemConfig
apiBase := ""
if db.GetDB().Where("key = ?", "card.api_base_url").First(&cfgBase).Error == nil {
apiBase = cfgBase.Value
}
if apiBase == "" {
code.Status = "failed"
code.Error = "开卡 API 地址未配置"
d.Save(&code)
return
}
apiProv, err := card.NewAPIProvider(card.APIProviderConfig{
BaseURL: apiBase,
Codes: []string{code.Code},
PoolCfg: card.PoolConfig{MultiBind: true, MaxBinds: 999},
})
if err != nil {
code.Status = "failed"
code.Error = err.Error()
d.Save(&code)
return
}
cardInfo, err := apiProv.GetCard(context.Background())
if err != nil {
code.Status = "failed"
code.Error = err.Error()
d.Save(&code)
return
}
numberEnc, _ := db.Encrypt(cardInfo.Number)
cvcEnc, _ := db.Encrypt(cardInfo.CVC)
newCard := &db.Card{
NumberHash: db.HashSHA256(cardInfo.Number),
NumberEnc: numberEnc,
CVCEnc: cvcEnc,
ExpMonth: cardInfo.ExpMonth,
ExpYear: cardInfo.ExpYear,
Name: cardInfo.Name,
Country: cardInfo.Country,
Source: "api",
CardCodeID: &code.ID,
Status: "available",
MaxBinds: 1,
}
d.Create(newCard)
now := time.Now()
code.Status = "redeemed"
code.CardID = &newCard.ID
code.RedeemedAt = &now
d.Save(&code)
}()
c.JSON(http.StatusOK, gin.H{"message": "兑换已开始"})
}
func DeleteCardCode(c *gin.Context) {
if err := db.GetDB().Delete(&db.CardCode{}, c.Param("id")).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
func intQuery(c *gin.Context, key string, def int) int {
v := c.Query(key)
if v == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil {
return def
}
return n
}

154
internal/handler/config.go Normal file
View File

@@ -0,0 +1,154 @@
package handler
import (
"net/http"
"gpt-plus/internal/db"
"github.com/gin-gonic/gin"
)
type configGroup struct {
Group string `json:"group"`
Items []db.SystemConfig `json:"items"`
}
func GetConfig(c *gin.Context) {
var configs []db.SystemConfig
if err := db.GetDB().Order("\"group\", key").Find(&configs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Mask password fields
for i := range configs {
if configs[i].Type == "password" && configs[i].Value != "" {
configs[i].Value = "••••••••"
}
}
groups := make(map[string][]db.SystemConfig)
for _, cfg := range configs {
groups[cfg.Group] = append(groups[cfg.Group], cfg)
}
c.JSON(http.StatusOK, gin.H{"groups": groups})
}
type updateConfigRequest struct {
Items []struct {
Key string `json:"key" binding:"required"`
Value string `json:"value"`
} `json:"items" binding:"required"`
}
func UpdateConfig(c *gin.Context) {
var req updateConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tx := db.GetDB().Begin()
for _, item := range req.Items {
// Skip masked password values
if item.Value == "••••••••" {
continue
}
var existing db.SystemConfig
if err := tx.Where("key = ?", item.Key).First(&existing).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusNotFound, gin.H{"error": "配置项不存在: " + item.Key})
return
}
value := item.Value
if existing.Type == "password" && value != "" {
encrypted, err := db.Encrypt(value)
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "加密失败"})
return
}
value = encrypted
}
if err := tx.Model(&existing).Update("value", value).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
tx.Commit()
c.JSON(http.StatusOK, gin.H{"message": "配置已更新"})
}
func SeedDefaultConfigs() {
defaults := []db.SystemConfig{
// Proxy group
{Key: "proxy.mode", Value: "b2proxy", Group: "proxy", Label: "代理模式", Type: "string"},
{Key: "proxy.url", Value: "", Group: "proxy", Label: "代理地址", Type: "string"},
{Key: "proxy.b2proxy.enabled", Value: "true", Group: "proxy", Label: "启用 B2Proxy", Type: "bool"},
{Key: "proxy.b2proxy.api_base", Value: "http://global.rrp.b2proxy.com:8089", Group: "proxy", Label: "B2Proxy API 地址", Type: "string"},
{Key: "proxy.b2proxy.zone", Value: "custom", Group: "proxy", Label: "B2Proxy 区域", Type: "string"},
{Key: "proxy.b2proxy.proto", Value: "socks5", Group: "proxy", Label: "B2Proxy 协议", Type: "string"},
{Key: "proxy.b2proxy.sess_time", Value: "5", Group: "proxy", Label: "会话时长(分钟)", Type: "int"},
// Email group
{Key: "email.gateway.base_url", Value: "https://regmail.zhengmi.org", Group: "email", Label: "邮箱网关地址", Type: "string"},
{Key: "email.gateway.api_key", Value: "Admin2026.", Group: "email", Label: "邮箱网关 API Key", Type: "password"},
{Key: "email.gateway.provider", Value: "mail", Group: "email", Label: "邮箱网关 Provider", Type: "string"},
// Card group
{Key: "card.max_binds", Value: "0", Group: "card", Label: "单卡最大绑定次数(0=无限)", Type: "int"},
{Key: "card.default_name", Value: "Anna Hoover", Group: "card", Label: "默认持卡人姓名", Type: "string"},
{Key: "card.default_country", Value: "US", Group: "card", Label: "默认国家", Type: "string"},
{Key: "card.default_currency", Value: "USD", Group: "card", Label: "默认货币", Type: "string"},
{Key: "card.default_address", Value: "1208 Oakdale Street", Group: "card", Label: "默认地址", Type: "string"},
{Key: "card.default_city", Value: "Jonesboro", Group: "card", Label: "默认城市", Type: "string"},
{Key: "card.default_state", Value: "AR", Group: "card", Label: "默认州/省", Type: "string"},
{Key: "card.default_postal_code", Value: "72401", Group: "card", Label: "默认邮编", Type: "string"},
{Key: "card.api_base_url", Value: "https://yyl.ncet.top", Group: "card", Label: "开卡 API 地址", Type: "string"},
{Key: "card.api_key", Value: "", Group: "card", Label: "开卡 API 密钥", Type: "password"},
// Stripe group
{Key: "stripe.build_hash", Value: "ede17ac9fd", Group: "stripe", Label: "Build Hash", Type: "string"},
{Key: "stripe.tag_version", Value: "4.5.43", Group: "stripe", Label: "Tag Version", Type: "string"},
{Key: "stripe.fingerprint_dir", Value: "./fingerprints", Group: "stripe", Label: "指纹目录", Type: "string"},
// Captcha group
{Key: "captcha.provider", Value: "hcaptchasolver", Group: "captcha", Label: "验证码提供商", Type: "string"},
{Key: "captcha.api_key", Value: "Kenzx_4ba2535aaf33bd1238345f795a085853ace94e54ade70a2a", Group: "captcha", Label: "验证码 API Key", Type: "password"},
{Key: "captcha.proxy", Value: "", Group: "captcha", Label: "验证码代理", Type: "string"},
// Account group
{Key: "account.password_length", Value: "16", Group: "account", Label: "密码长度", Type: "int"},
{Key: "account.locale", Value: "en-GB", Group: "account", Label: "区域设置", Type: "string"},
// Team group
{Key: "team.enabled", Value: "true", Group: "team", Label: "启用 Team", Type: "bool"},
{Key: "team.workspace_prefix", Value: "Team", Group: "team", Label: "工作区前缀", Type: "string"},
{Key: "team.seat_quantity", Value: "5", Group: "team", Label: "座位数", Type: "int"},
{Key: "team.coupon", Value: "team-1-month-free", Group: "team", Label: "优惠券", Type: "string"},
{Key: "team.invite_count", Value: "0", Group: "team", Label: "邀请人数", Type: "int"},
// CPA (CLI Proxy API) group
{Key: "cpa.base_url", Value: "http://127.0.0.1:8317", Group: "cpa", Label: "CPA 地址", Type: "string"},
{Key: "cpa.management_key", Value: "gptplus2026", Group: "cpa", Label: "Management Key", Type: "password"},
}
d := db.GetDB()
for _, cfg := range defaults {
var existing db.SystemConfig
res := d.Where("key = ?", cfg.Key).First(&existing)
if res.Error != nil {
d.Create(&cfg)
} else if existing.Value == "" && cfg.Value != "" {
d.Model(&existing).Updates(map[string]interface{}{
"value": cfg.Value,
"label": cfg.Label,
"type": cfg.Type,
})
}
}
}

View File

@@ -0,0 +1,18 @@
package handler
import (
"net/http"
"gpt-plus/internal/db"
"github.com/gin-gonic/gin"
)
func GetDashboard(c *gin.Context) {
stats, err := db.GetDashboardStats(db.GetDB())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}

View File

@@ -0,0 +1,40 @@
package handler
import (
"net/http"
"gpt-plus/internal/db"
"github.com/gin-gonic/gin"
)
func RegisterEmailRecordRoutes(api *gin.RouterGroup) {
api.GET("/email-records", ListEmailRecords)
api.GET("/email-records/stats", GetEmailRecordStats)
}
func ListEmailRecords(c *gin.Context) {
d := db.GetDB()
query := d.Model(&db.EmailRecord{}).Order("created_at DESC")
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
p := db.PaginationParams{
Page: intQuery(c, "page", 1),
Size: intQuery(c, "size", 20),
}
var records []db.EmailRecord
result, err := db.Paginate(query, p, &records)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func GetEmailRecordStats(c *gin.Context) {
c.JSON(http.StatusOK, db.GetEmailRecordStats(db.GetDB()))
}

View File

@@ -0,0 +1,518 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"net/http/httptest"
"os"
"testing"
"gpt-plus/internal/db"
"github.com/glebarez/sqlite"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func init() {
gin.SetMode(gin.TestMode)
}
func setupTestEnv(t *testing.T) *gorm.DB {
t.Helper()
d, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open test db: %v", err)
}
if err := d.AutoMigrate(
&db.SystemConfig{}, &db.EmailRecord{}, &db.Task{}, &db.TaskLog{},
&db.CardCode{}, &db.Card{}, &db.Account{},
); err != nil {
t.Fatalf("migrate: %v", err)
}
db.DB = d
SetJWTSecret("test-secret-key")
os.Setenv("ADMIN_PASSWORD", "testpass")
if err := InitAdminPassword(); err != nil {
t.Fatalf("init password: %v", err)
}
// Reset rate limiter between tests
loginLimiter.mu.Lock()
loginLimiter.attempts = make(map[string][]time.Time)
loginLimiter.mu.Unlock()
return d
}
func jsonReq(method, path string, body interface{}) *http.Request {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
return req
}
func doRequest(r *gin.Engine, req *http.Request) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
func loginAndGetCookie(t *testing.T, r *gin.Engine) *http.Cookie {
t.Helper()
req := jsonReq("POST", "/api/login", map[string]string{"password": "testpass"})
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("login failed: %d %s", w.Code, w.Body.String())
}
for _, c := range w.Result().Cookies() {
if c.Name == "token" {
return c
}
}
t.Fatal("no token cookie in login response")
return nil
}
func authedReq(method, path string, body interface{}, cookie *http.Cookie) *http.Request {
req := jsonReq(method, path, body)
req.AddCookie(cookie)
return req
}
// --- Auth tests ---
func TestLoginSuccess(t *testing.T) {
setupTestEnv(t)
r := SetupRouter(false)
req := jsonReq("POST", "/api/login", map[string]string{"password": "testpass"})
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
var resp map[string]string
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["message"] != "登录成功" {
t.Fatalf("message = %q", resp["message"])
}
found := false
for _, c := range w.Result().Cookies() {
if c.Name == "token" && c.HttpOnly {
found = true
}
}
if !found {
t.Fatal("no httpOnly token cookie set")
}
}
func TestLoginWrongPassword(t *testing.T) {
setupTestEnv(t)
r := SetupRouter(false)
req := jsonReq("POST", "/api/login", map[string]string{"password": "wrong"})
w := doRequest(r, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", w.Code)
}
}
func TestLoginEmptyPassword(t *testing.T) {
setupTestEnv(t)
r := SetupRouter(false)
req := jsonReq("POST", "/api/login", map[string]string{})
w := doRequest(r, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", w.Code)
}
}
func TestLogout(t *testing.T) {
setupTestEnv(t)
r := SetupRouter(false)
req := jsonReq("POST", "/api/logout", nil)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
for _, c := range w.Result().Cookies() {
if c.Name == "token" && c.MaxAge < 0 {
return
}
}
t.Fatal("token cookie not cleared")
}
func TestAuthMiddlewareRejectsUnauthenticated(t *testing.T) {
setupTestEnv(t)
r := SetupRouter(false)
req := jsonReq("GET", "/api/config", nil)
w := doRequest(r, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", w.Code)
}
}
func TestAuthMiddlewareAcceptsValidToken(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
req := authedReq("GET", "/api/auth/check", nil, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", w.Code)
}
}
// --- Config tests ---
func TestGetConfig(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
req := authedReq("GET", "/api/config", nil, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body: %s", w.Code, w.Body.String())
}
var resp struct {
Groups map[string][]db.SystemConfig `json:"groups"`
}
json.Unmarshal(w.Body.Bytes(), &resp)
if len(resp.Groups) == 0 {
t.Fatal("expected non-empty config groups")
}
if _, ok := resp.Groups["proxy"]; !ok {
t.Fatal("expected proxy group")
}
}
func TestUpdateConfig(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
body := map[string]interface{}{
"items": []map[string]string{
{"key": "proxy.url", "value": "socks5://1.2.3.4:1080"},
},
}
req := authedReq("PUT", "/api/config", body, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200, body: %s", w.Code, w.Body.String())
}
var cfg db.SystemConfig
db.GetDB().Where("key = ?", "proxy.url").First(&cfg)
if cfg.Value != "socks5://1.2.3.4:1080" {
t.Fatalf("config value = %q, want socks5://1.2.3.4:1080", cfg.Value)
}
}
// --- Card tests ---
func TestCardCRUD(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
// Add card
body := map[string]string{
"number": "4242424242424242", "cvc": "123",
"exp_month": "12", "exp_year": "2030", "country": "US",
}
req := authedReq("POST", "/api/cards", body, cookie)
w := doRequest(r, req)
if w.Code != http.StatusCreated {
t.Fatalf("add card: status = %d, body: %s", w.Code, w.Body.String())
}
// List cards
req = authedReq("GET", "/api/cards", nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("list cards: status = %d", w.Code)
}
var listResp struct {
Items []db.Card `json:"items"`
Total int64 `json:"total"`
}
json.Unmarshal(w.Body.Bytes(), &listResp)
if listResp.Total != 1 {
t.Fatalf("total = %d, want 1", listResp.Total)
}
if listResp.Items[0].NumberLast4 != "4242" {
t.Fatalf("last4 = %q, want 4242", listResp.Items[0].NumberLast4)
}
// Stats
req = authedReq("GET", "/api/cards/stats", nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("card stats: status = %d", w.Code)
}
// Delete
cardID := listResp.Items[0].ID
req = authedReq("DELETE", "/api/cards/"+itoa(cardID), nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("delete card: status = %d", w.Code)
}
}
func TestCardDuplicate(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
body := map[string]string{
"number": "5555555555554444", "cvc": "321",
"exp_month": "06", "exp_year": "2029", "country": "JP",
}
req := authedReq("POST", "/api/cards", body, cookie)
doRequest(r, req) // first
req = authedReq("POST", "/api/cards", body, cookie)
w := doRequest(r, req) // duplicate
if w.Code != http.StatusConflict {
t.Fatalf("duplicate card: status = %d, want 409", w.Code)
}
}
// --- Card Code tests ---
func TestCardCodeImport(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
body := map[string]interface{}{
"codes": []string{"CODE-001", "CODE-002", "CODE-003"},
}
req := authedReq("POST", "/api/card-codes/import", body, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("import: status = %d, body: %s", w.Code, w.Body.String())
}
var resp map[string]int
json.Unmarshal(w.Body.Bytes(), &resp)
if resp["imported"] != 3 {
t.Fatalf("imported = %d, want 3", resp["imported"])
}
// Stats
req = authedReq("GET", "/api/card-codes/stats", nil, cookie)
w = doRequest(r, req)
var stats db.CardCodeStats
json.Unmarshal(w.Body.Bytes(), &stats)
if stats.Unused != 3 {
t.Fatalf("unused = %d, want 3", stats.Unused)
}
}
// --- Dashboard test ---
func TestDashboard(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
req := authedReq("GET", "/api/dashboard", nil, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("dashboard: status = %d, body: %s", w.Code, w.Body.String())
}
}
// --- Email record tests ---
func TestEmailRecordStats(t *testing.T) {
d := setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
d.Create(&db.EmailRecord{Email: "a@test.com", Status: "used"})
d.Create(&db.EmailRecord{Email: "b@test.com", Status: "used_failed"})
req := authedReq("GET", "/api/email-records/stats", nil, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var stats db.EmailRecordStats
json.Unmarshal(w.Body.Bytes(), &stats)
if stats.Total != 2 || stats.Used != 1 || stats.UsedFailed != 1 {
t.Fatalf("stats = %+v", stats)
}
}
// --- Account tests ---
func TestAccountListAndDetail(t *testing.T) {
d := setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
d.Create(&db.Account{Email: "plus@test.com", Plan: "plus", Status: "active", AccountID: "acc-1"})
owner := db.Account{Email: "team@test.com", Plan: "team_owner", Status: "active", AccountID: "acc-2"}
d.Create(&owner)
d.Create(&db.Account{Email: "m1@test.com", Plan: "team_member", Status: "active", ParentID: &owner.ID})
// List (should only show plus + team_owner)
req := authedReq("GET", "/api/accounts", nil, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var listResp struct {
Items []db.Account `json:"items"`
Total int64 `json:"total"`
}
json.Unmarshal(w.Body.Bytes(), &listResp)
if listResp.Total != 2 {
t.Fatalf("total = %d, want 2 (team_member hidden)", listResp.Total)
}
// Detail with sub-accounts
req = authedReq("GET", "/api/accounts/"+itoa(owner.ID), nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("detail status = %d", w.Code)
}
var detail db.Account
json.Unmarshal(w.Body.Bytes(), &detail)
if len(detail.SubAccounts) != 1 {
t.Fatalf("sub_accounts = %d, want 1", len(detail.SubAccounts))
}
}
func TestAccountUpdateNote(t *testing.T) {
d := setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
d.Create(&db.Account{Email: "note@test.com", Plan: "plus", Status: "active"})
req := authedReq("PUT", "/api/accounts/1/note", map[string]string{"note": "VIP"}, cookie)
w := doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var acct db.Account
d.First(&acct, 1)
if acct.Note != "VIP" {
t.Fatalf("note = %q, want VIP", acct.Note)
}
}
func TestAccountSearch(t *testing.T) {
d := setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
d.Create(&db.Account{Email: "alice@test.com", Plan: "plus", Status: "active"})
d.Create(&db.Account{Email: "bob@test.com", Plan: "plus", Status: "active"})
req := authedReq("GET", "/api/accounts?search=alice", nil, cookie)
w := doRequest(r, req)
var resp struct {
Total int64 `json:"total"`
}
json.Unmarshal(w.Body.Bytes(), &resp)
if resp.Total != 1 {
t.Fatalf("search total = %d, want 1", resp.Total)
}
}
// --- Task tests ---
func TestTaskCreateAndList(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
body := map[string]interface{}{"type": "plus", "count": 5}
req := authedReq("POST", "/api/tasks", body, cookie)
w := doRequest(r, req)
if w.Code != http.StatusCreated {
t.Fatalf("create task: status = %d, body: %s", w.Code, w.Body.String())
}
var created db.Task
json.Unmarshal(w.Body.Bytes(), &created)
if created.ID == "" || created.Status != "pending" || created.TotalCount != 5 {
t.Fatalf("created = %+v", created)
}
// List
req = authedReq("GET", "/api/tasks", nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("list: status = %d", w.Code)
}
// Delete
req = authedReq("DELETE", "/api/tasks/"+created.ID, nil, cookie)
w = doRequest(r, req)
if w.Code != http.StatusOK {
t.Fatalf("delete: status = %d, body: %s", w.Code, w.Body.String())
}
}
func TestTaskCreateInvalidType(t *testing.T) {
setupTestEnv(t)
SeedDefaultConfigs()
r := SetupRouter(false)
cookie := loginAndGetCookie(t, r)
body := map[string]interface{}{"type": "invalid", "count": 1}
req := authedReq("POST", "/api/tasks", body, cookie)
w := doRequest(r, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", w.Code)
}
}
func itoa(n uint) string {
return fmt.Sprintf("%d", n)
}

View File

@@ -0,0 +1,84 @@
package handler
import (
"net/http"
"os"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret []byte
func SetJWTSecret(secret string) {
jwtSecret = []byte(secret)
}
func GenerateToken() (string, error) {
claims := jwt.MapClaims{
"role": "admin",
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr, err := c.Cookie("token")
if err != nil || tokenStr == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "令牌无效或已过期"})
return
}
c.Next()
}
}
// Rate limiter for login endpoint: 5 attempts per minute per IP
type rateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
}
var loginLimiter = &rateLimiter{attempts: make(map[string][]time.Time)}
func LoginRateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
loginLimiter.mu.Lock()
defer loginLimiter.mu.Unlock()
now := time.Now()
window := now.Add(-1 * time.Minute)
// Prune old entries
valid := make([]time.Time, 0)
for _, t := range loginLimiter.attempts[ip] {
if t.After(window) {
valid = append(valid, t)
}
}
loginLimiter.attempts[ip] = valid
if len(valid) >= 5 {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "登录尝试过于频繁,请稍后再试"})
return
}
loginLimiter.attempts[ip] = append(loginLimiter.attempts[ip], now)
c.Next()
}
}
func IsSecure() bool {
return os.Getenv("GIN_MODE") == "release"
}

View File

@@ -0,0 +1,42 @@
package handler
import (
"github.com/gin-gonic/gin"
)
func SetupRouter(devMode bool) *gin.Engine {
r := gin.Default()
// Public routes
pub := r.Group("/api")
pub.POST("/login", LoginRateLimitMiddleware(), Login)
pub.POST("/logout", Logout)
// Protected routes
api := r.Group("/api")
api.Use(AuthMiddleware())
{
api.GET("/auth/check", CheckAuth)
// Config
api.GET("/config", GetConfig)
api.PUT("/config", UpdateConfig)
// Dashboard
api.GET("/dashboard", GetDashboard)
// Cards + Card Codes
RegisterCardRoutes(api)
// Email Records
RegisterEmailRecordRoutes(api)
// Tasks
RegisterTaskRoutes(api)
// Accounts
RegisterAccountRoutes(api)
}
return r
}

201
internal/handler/task.go Normal file
View File

@@ -0,0 +1,201 @@
package handler
import (
"encoding/json"
"net/http"
"time"
"gpt-plus/internal/db"
"gpt-plus/internal/task"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
var taskManager *task.TaskManager
func SetTaskManager(tm *task.TaskManager) {
taskManager = tm
}
func RegisterTaskRoutes(api *gin.RouterGroup) {
api.POST("/tasks", CreateTask)
api.GET("/tasks", ListTasks)
api.GET("/tasks/:id", GetTask)
api.GET("/tasks/:id/logs", GetTaskLogs)
api.POST("/tasks/:id/start", StartTask)
api.POST("/tasks/:id/stop", StopTask)
api.POST("/tasks/:id/force-stop", ForceStopTask)
api.DELETE("/tasks/:id", DeleteTask)
}
type createTaskRequest struct {
Type string `json:"type" binding:"required"`
Count int `json:"count" binding:"required,min=1"`
}
func CreateTask(c *gin.Context) {
var req createTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Type != "plus" && req.Type != "team" && req.Type != "both" {
c.JSON(http.StatusBadRequest, gin.H{"error": "类型必须为 plus, team 或 both"})
return
}
// Snapshot current config
var configs []db.SystemConfig
db.GetDB().Find(&configs)
cfgJSON, _ := json.Marshal(configs)
t := &db.Task{
ID: uuid.New().String(),
Type: req.Type,
TotalCount: req.Count,
Status: "pending",
Config: string(cfgJSON),
}
if err := db.GetDB().Create(t).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, t)
}
func ListTasks(c *gin.Context) {
d := db.GetDB()
query := d.Model(&db.Task{}).Order("created_at DESC")
if status := c.Query("status"); status != "" {
statuses := splitComma(status)
query = query.Where("status IN ?", statuses)
}
p := db.PaginationParams{Page: intQuery(c, "page", 1), Size: intQuery(c, "size", 20)}
var tasks []db.Task
result, err := db.Paginate(query, p, &tasks)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func GetTask(c *gin.Context) {
var t db.Task
if err := db.GetDB().First(&t, "id = ?", c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusOK, t)
}
func GetTaskLogs(c *gin.Context) {
taskID := c.Param("id")
sinceID := intQuery(c, "since_id", 0)
limit := intQuery(c, "limit", 50)
if limit > 200 {
limit = 200
}
var logs []db.TaskLog
query := db.GetDB().Where("task_id = ?", taskID)
if sinceID > 0 {
query = query.Where("id > ?", sinceID)
}
query.Order("id ASC").Limit(limit).Find(&logs)
c.JSON(http.StatusOK, gin.H{"items": logs})
}
func StartTask(c *gin.Context) {
if taskManager == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "任务管理器未初始化"})
return
}
if err := taskManager.Start(c.Param("id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "任务已启动"})
}
func StopTask(c *gin.Context) {
if taskManager == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "任务管理器未初始化"})
return
}
if err := taskManager.Stop(c.Param("id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "正在停止..."})
}
func ForceStopTask(c *gin.Context) {
if taskManager == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "任务管理器未初始化"})
return
}
if err := taskManager.ForceStop(c.Param("id")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已强制取消"})
}
func DeleteTask(c *gin.Context) {
taskID := c.Param("id")
var t db.Task
if err := db.GetDB().First(&t, "id = ?", taskID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
if t.Status == "running" || t.Status == "stopping" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无法删除运行中的任务"})
return
}
db.GetDB().Where("task_id = ?", taskID).Delete(&db.TaskLog{})
db.GetDB().Delete(&t)
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
func splitComma(s string) []string {
var result []string
for _, v := range splitStr(s, ",") {
v = trimStr(v)
if v != "" {
result = append(result, v)
}
}
return result
}
func splitStr(s, sep string) []string {
result := []string{}
start := 0
for i := 0; i <= len(s)-len(sep); i++ {
if s[i:i+len(sep)] == sep {
result = append(result, s[start:i])
start = i + len(sep)
}
}
result = append(result, s[start:])
return result
}
func trimStr(s string) string {
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
s = s[1:]
}
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
s = s[:len(s)-1]
}
return s
}
// Ensure time import is used
var _ = time.Now