diff --git a/.env b/.env new file mode 100644 index 0000000..a2ce0c7 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# CRM系统数据库配置示例 +# 复制此文件为 .env 并修改配置 + +# 数据库配置 +DB_HOST=mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com +DB_PORT=3306 +DB_USER=root_dev +DB_PASSWORD=Kdse89sd +DB_NAME=crm_db + +# 存储模式: mysql 或 json +# mysql - 使用MySQL数据库存储(默认) +# json - 使用JSON文件存储(向后兼容) +STORAGE_MODE=mysql + +# 服务端口 +PORT=55335 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a2ce0c7 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# CRM系统数据库配置示例 +# 复制此文件为 .env 并修改配置 + +# 数据库配置 +DB_HOST=mysql1.rdsmbk3ednsgnnt.rds.bj.baidubce.com +DB_PORT=3306 +DB_USER=root_dev +DB_PASSWORD=Kdse89sd +DB_NAME=crm_db + +# 存储模式: mysql 或 json +# mysql - 使用MySQL数据库存储(默认) +# json - 使用JSON文件存储(向后兼容) +STORAGE_MODE=mysql + +# 服务端口 +PORT=55335 diff --git a/README.md b/README.md index 7636c40..bd00b2b 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,84 @@ A web-based CRM system built with Go backend and HTML/CSS/JavaScript frontend. - Backend: Go - Frontend: HTML, CSS, JavaScript - Charts: Chart.js (via CDN) -- Data Storage: Local JSON file +- Data Storage: MySQL数据库(默认)或本地JSON文件 + +## 数据存储 + +系统支持两种存储模式: + +### 1. MySQL数据库(推荐) + +1. 创建数据库并执行迁移脚本: + ```bash + mysql -u root -p < ./data/migration.sql + ``` + +2. 配置环境变量: + ```bash + export DB_HOST=localhost + export DB_PORT=3306 + export DB_USER=root + export DB_PASSWORD=your_password + export DB_NAME=crm_db + export STORAGE_MODE=mysql + ``` + +### 2. JSON文件存储(向后兼容) + +设置环境变量使用JSON存储: +```bash +export STORAGE_MODE=json +``` + +数据将存储在 `./data/` 目录下的JSON文件中。 ## How to Run -1. Navigate to the project directory: +### 方法一:使用启动脚本 + +1. 复制并修改配置文件: ```bash - cd /Users/d-robotics/crm/crm-go + cp .env.example .env + # 编辑 .env 文件设置数据库密码等 ``` -2. Run the server: +2. 运行启动脚本: ```bash - ./bin/server + ./start.sh + ``` + +### 方法二:手动启动 + +1. 设置环境变量: + ```bash + export DB_PASSWORD=your_password + export STORAGE_MODE=mysql + ``` + +2. 构建并运行: + ```bash + go build -o crm-server ./cmd/server/ + ./crm-server ``` 3. Open your browser and go to: ``` - http://localhost:8080 + http://localhost:8081 ``` +## 环境变量说明 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `STORAGE_MODE` | 存储模式: `mysql` 或 `json` | `mysql` | +| `DB_HOST` | MySQL主机地址 | `localhost` | +| `DB_PORT` | MySQL端口 | `3306` | +| `DB_USER` | MySQL用户名 | `root` | +| `DB_PASSWORD` | MySQL密码 | 空 | +| `DB_NAME` | 数据库名称 | `crm_db` | +| `PORT` | 服务端口 | `8081` | + ## Usage ### Customer Management diff --git a/cmd/server/main.go b/cmd/server/main.go index ef42e5c..4d7ecd3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,10 +14,36 @@ import ( ) func main() { - // Initialize storage - customerStorage := storage.NewCustomerStorage("./data/customers.json") - followUpStorage := storage.NewFollowUpStorage("./data/followups.json") - trialPeriodStorage := storage.NewTrialPeriodStorage("./data/trial_periods.json") + // 获取存储模式,默认使用MySQL + storageMode := os.Getenv("STORAGE_MODE") + if storageMode == "" { + storageMode = "mysql" // 默认使用MySQL + } + + var customerStorage storage.CustomerStorage + var followUpStorage storage.FollowUpStorage + var trialPeriodStorage storage.TrialPeriodStorage + + if storageMode == "mysql" { + // 初始化MySQL数据库连接 + dbConfig := storage.GetDBConfigFromEnv() + if err := storage.InitDB(dbConfig); err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer storage.CloseDB() + + // 使用MySQL存储 + customerStorage = storage.NewMySQLCustomerStorage() + followUpStorage = storage.NewMySQLFollowUpStorage() + trialPeriodStorage = storage.NewMySQLTrialPeriodStorage() + log.Println("✅ Using MySQL storage") + } else { + // 使用JSON文件存储(向后兼容) + customerStorage = storage.NewCustomerStorage("./data/customers.json") + followUpStorage = storage.NewFollowUpStorage("./data/followups.json") + trialPeriodStorage = storage.NewTrialPeriodStorage("./data/trial_periods.json") + log.Println("✅ Using JSON file storage") + } // Get Feishu webhook URL from environment variable feishuWebhook := "https://open.feishu.cn/open-apis/bot/v2/hook/d75c14ad-d782-489e-8a99-81b511ee4abd" diff --git a/go.mod b/go.mod index 9ff2c8c..6e66d72 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,17 @@ module crm-go -go 1.21 +go 1.21.0 -require github.com/xuri/excelize/v2 v2.8.0 +toolchain go1.24.3 require ( - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/xuri/excelize/v2 v2.8.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 6689e29..be27643 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= diff --git a/internal/storage/db.go b/internal/storage/db.go new file mode 100644 index 0000000..ad4b16a --- /dev/null +++ b/internal/storage/db.go @@ -0,0 +1,89 @@ +package storage + +import ( + "database/sql" + "fmt" + "log" + "os" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +// DBConfig 数据库配置 +type DBConfig struct { + Host string + Port int + User string + Password string + Database string +} + +var db *sql.DB + +// InitDB 初始化数据库连接 +func InitDB(config DBConfig) error { + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + config.User, config.Password, config.Host, config.Port, config.Database) + + var err error + db, err = sql.Open("mysql", dsn) + if err != nil { + return fmt.Errorf("failed to open database: %v", err) + } + + // 配置连接池 + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + + // 测试连接 + if err := db.Ping(); err != nil { + return fmt.Errorf("failed to ping database: %v", err) + } + + log.Println("✅ Database connection established") + return nil +} + +// GetDB 获取数据库连接 +func GetDB() *sql.DB { + return db +} + +// CloseDB 关闭数据库连接 +func CloseDB() error { + if db != nil { + return db.Close() + } + return nil +} + +// GetDBConfigFromEnv 从环境变量获取数据库配置 +func GetDBConfigFromEnv() DBConfig { + config := DBConfig{ + Host: "localhost", + Port: 3306, + User: "root", + Password: "", + Database: "crm_db", + } + + if host := os.Getenv("DB_HOST"); host != "" { + config.Host = host + } + if user := os.Getenv("DB_USER"); user != "" { + config.User = user + } + if pwd := os.Getenv("DB_PASSWORD"); pwd != "" { + config.Password = pwd + } + if dbName := os.Getenv("DB_NAME"); dbName != "" { + config.Database = dbName + } + if port := os.Getenv("DB_PORT"); port != "" { + fmt.Sscanf(port, "%d", &config.Port) + } + + return config +} diff --git a/internal/storage/mysql_customer_storage.go b/internal/storage/mysql_customer_storage.go new file mode 100644 index 0000000..77ef93b --- /dev/null +++ b/internal/storage/mysql_customer_storage.go @@ -0,0 +1,218 @@ +package storage + +import ( + "crm-go/models" + "crypto/rand" + "database/sql" + "encoding/hex" + "fmt" + "strings" + "time" +) + +type mysqlCustomerStorage struct { + db *sql.DB +} + +// NewMySQLCustomerStorage 创建MySQL客户存储 +func NewMySQLCustomerStorage() CustomerStorage { + return &mysqlCustomerStorage{ + db: GetDB(), + } +} + +func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) { + query := ` + SELECT id, created_at, customer_name, intended_product, version, + description, solution, type, module, status_progress, reporter + FROM customers + ORDER BY created_at DESC + ` + + rows, err := cs.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var customers []models.Customer + for rows.Next() { + var c models.Customer + var intendedProduct, version, description, solution, typ, module, statusProgress, reporter sql.NullString + + err := rows.Scan( + &c.ID, &c.CreatedAt, &c.CustomerName, + &intendedProduct, &version, &description, + &solution, &typ, &module, &statusProgress, &reporter, + ) + if err != nil { + return nil, err + } + + c.IntendedProduct = intendedProduct.String + c.Version = version.String + c.Description = description.String + c.Solution = solution.String + c.Type = typ.String + c.Module = module.String + c.StatusProgress = statusProgress.String + c.Reporter = reporter.String + + customers = append(customers, c) + } + + return customers, rows.Err() +} + +func (cs *mysqlCustomerStorage) GetCustomerByID(id string) (*models.Customer, error) { + query := ` + SELECT id, created_at, customer_name, intended_product, version, + description, solution, type, module, status_progress, reporter + FROM customers + WHERE id = ? + ` + + var c models.Customer + var intendedProduct, version, description, solution, typ, module, statusProgress, reporter sql.NullString + + err := cs.db.QueryRow(query, id).Scan( + &c.ID, &c.CreatedAt, &c.CustomerName, + &intendedProduct, &version, &description, + &solution, &typ, &module, &statusProgress, &reporter, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + c.IntendedProduct = intendedProduct.String + c.Version = version.String + c.Description = description.String + c.Solution = solution.String + c.Type = typ.String + c.Module = module.String + c.StatusProgress = statusProgress.String + c.Reporter = reporter.String + + return &c, nil +} + +func (cs *mysqlCustomerStorage) CreateCustomer(customer models.Customer) error { + if customer.ID == "" { + customer.ID = generateMySQLUUID() + } + if customer.CreatedAt.IsZero() { + customer.CreatedAt = time.Now() + } + + query := ` + INSERT INTO customers (id, created_at, customer_name, intended_product, version, + description, solution, type, module, status_progress, reporter) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + + _, err := cs.db.Exec(query, + customer.ID, customer.CreatedAt, customer.CustomerName, + customer.IntendedProduct, customer.Version, customer.Description, + customer.Solution, customer.Type, customer.Module, + customer.StatusProgress, customer.Reporter, + ) + + return err +} + +func (cs *mysqlCustomerStorage) UpdateCustomer(id string, updates models.UpdateCustomerRequest) error { + // 首先获取现有客户 + existing, err := cs.GetCustomerByID(id) + if err != nil || existing == nil { + return err + } + + // 应用更新 + if updates.CustomerName != nil { + existing.CustomerName = *updates.CustomerName + } + if updates.IntendedProduct != nil { + existing.IntendedProduct = *updates.IntendedProduct + } + if updates.Version != nil { + existing.Version = *updates.Version + } + if updates.Description != nil { + existing.Description = *updates.Description + } + if updates.Solution != nil { + existing.Solution = *updates.Solution + } + if updates.Type != nil { + existing.Type = *updates.Type + } + if updates.Module != nil { + existing.Module = *updates.Module + } + if updates.StatusProgress != nil { + existing.StatusProgress = *updates.StatusProgress + } + if updates.Reporter != nil { + existing.Reporter = *updates.Reporter + } + + query := ` + UPDATE customers + SET customer_name = ?, intended_product = ?, version = ?, + description = ?, solution = ?, type = ?, + module = ?, status_progress = ?, reporter = ? + WHERE id = ? + ` + + _, err = cs.db.Exec(query, + existing.CustomerName, existing.IntendedProduct, existing.Version, + existing.Description, existing.Solution, existing.Type, + existing.Module, existing.StatusProgress, existing.Reporter, + id, + ) + + return err +} + +func (cs *mysqlCustomerStorage) DeleteCustomer(id string) error { + query := `DELETE FROM customers WHERE id = ?` + _, err := cs.db.Exec(query, id) + if err != nil { + if strings.Contains(err.Error(), "command denied") { + return fmt.Errorf("数据库权限不足:无法执行删除操作,请联系管理员") + } + return err + } + return nil +} + +func (cs *mysqlCustomerStorage) SaveCustomers(customers []models.Customer) error { + // MySQL版本不需要使用此方法,保留接口兼容 + return nil +} + +func (cs *mysqlCustomerStorage) LoadCustomers() ([]models.Customer, error) { + return cs.GetAllCustomers() +} + +func (cs *mysqlCustomerStorage) CustomerExists(customer models.Customer) (bool, error) { + query := `SELECT COUNT(*) FROM customers WHERE description = ?` + var count int + err := cs.db.QueryRow(query, customer.Description).Scan(&count) + if err != nil { + return false, err + } + return count > 0, nil +} + +func generateMySQLUUID() string { + bytes := make([]byte, 16) + rand.Read(bytes) + bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant + return hex.EncodeToString(bytes) +} diff --git a/internal/storage/mysql_followup_storage.go b/internal/storage/mysql_followup_storage.go new file mode 100644 index 0000000..4004094 --- /dev/null +++ b/internal/storage/mysql_followup_storage.go @@ -0,0 +1,249 @@ +package storage + +import ( + "crm-go/models" + "crypto/rand" + "database/sql" + "encoding/hex" + "fmt" + "strings" + "time" +) + +type mysqlFollowUpStorage struct { + db *sql.DB +} + +// NewMySQLFollowUpStorage 创建MySQL跟进存储 +func NewMySQLFollowUpStorage() FollowUpStorage { + return &mysqlFollowUpStorage{ + db: GetDB(), + } +} + +func (fs *mysqlFollowUpStorage) GetAllFollowUps() ([]models.FollowUp, error) { + query := ` + SELECT id, created_at, customer_name, deal_status, customer_level, + industry, follow_up_time, notification_sent + FROM followups + ORDER BY follow_up_time DESC + ` + + rows, err := fs.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var followUps []models.FollowUp + for rows.Next() { + var f models.FollowUp + var dealStatus, customerLevel, industry sql.NullString + var notificationSent int + + err := rows.Scan( + &f.ID, &f.CreatedAt, &f.CustomerName, + &dealStatus, &customerLevel, &industry, + &f.FollowUpTime, ¬ificationSent, + ) + if err != nil { + return nil, err + } + + f.DealStatus = dealStatus.String + f.CustomerLevel = customerLevel.String + f.Industry = industry.String + f.NotificationSent = notificationSent == 1 + + followUps = append(followUps, f) + } + + return followUps, rows.Err() +} + +func (fs *mysqlFollowUpStorage) GetFollowUpByID(id string) (*models.FollowUp, error) { + query := ` + SELECT id, created_at, customer_name, deal_status, customer_level, + industry, follow_up_time, notification_sent + FROM followups + WHERE id = ? + ` + + var f models.FollowUp + var dealStatus, customerLevel, industry sql.NullString + var notificationSent int + + err := fs.db.QueryRow(query, id).Scan( + &f.ID, &f.CreatedAt, &f.CustomerName, + &dealStatus, &customerLevel, &industry, + &f.FollowUpTime, ¬ificationSent, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + f.DealStatus = dealStatus.String + f.CustomerLevel = customerLevel.String + f.Industry = industry.String + f.NotificationSent = notificationSent == 1 + + return &f, nil +} + +func (fs *mysqlFollowUpStorage) CreateFollowUp(followUp models.FollowUp) error { + if followUp.ID == "" { + followUp.ID = generateFollowUpMySQLUUID() + } + if followUp.CreatedAt.IsZero() { + followUp.CreatedAt = time.Now() + } + + query := ` + INSERT INTO followups (id, created_at, customer_name, deal_status, customer_level, + industry, follow_up_time, notification_sent) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ` + + notificationSent := 0 + if followUp.NotificationSent { + notificationSent = 1 + } + + _, err := fs.db.Exec(query, + followUp.ID, followUp.CreatedAt, followUp.CustomerName, + followUp.DealStatus, followUp.CustomerLevel, followUp.Industry, + followUp.FollowUpTime, notificationSent, + ) + + return err +} + +func (fs *mysqlFollowUpStorage) UpdateFollowUp(id string, updates models.UpdateFollowUpRequest) error { + // 首先获取现有记录 + existing, err := fs.GetFollowUpByID(id) + if err != nil || existing == nil { + return err + } + + // 应用更新 + if updates.CustomerName != nil { + existing.CustomerName = *updates.CustomerName + } + if updates.DealStatus != nil { + existing.DealStatus = *updates.DealStatus + } + if updates.CustomerLevel != nil { + existing.CustomerLevel = *updates.CustomerLevel + } + if updates.Industry != nil { + existing.Industry = *updates.Industry + } + if updates.FollowUpTime != nil { + t, err := time.Parse(time.RFC3339, *updates.FollowUpTime) + if err == nil { + existing.FollowUpTime = t + } + } + if updates.NotificationSent != nil { + existing.NotificationSent = *updates.NotificationSent + } + + query := ` + UPDATE followups + SET customer_name = ?, deal_status = ?, customer_level = ?, + industry = ?, follow_up_time = ?, notification_sent = ? + WHERE id = ? + ` + + notificationSent := 0 + if existing.NotificationSent { + notificationSent = 1 + } + + _, err = fs.db.Exec(query, + existing.CustomerName, existing.DealStatus, existing.CustomerLevel, + existing.Industry, existing.FollowUpTime, notificationSent, + id, + ) + + return err +} + +func (fs *mysqlFollowUpStorage) DeleteFollowUp(id string) error { + query := `DELETE FROM followups WHERE id = ?` + _, err := fs.db.Exec(query, id) + if err != nil { + if strings.Contains(err.Error(), "command denied") { + return fmt.Errorf("数据库权限不足:无法执行删除操作,请联系管理员") + } + return err + } + return nil +} + +func (fs *mysqlFollowUpStorage) SaveFollowUps(followUps []models.FollowUp) error { + // MySQL版本不需要使用此方法,保留接口兼容 + return nil +} + +func (fs *mysqlFollowUpStorage) LoadFollowUps() ([]models.FollowUp, error) { + return fs.GetAllFollowUps() +} + +func (fs *mysqlFollowUpStorage) GetPendingNotifications() ([]models.FollowUp, error) { + query := ` + SELECT id, created_at, customer_name, deal_status, customer_level, + industry, follow_up_time, notification_sent + FROM followups + WHERE notification_sent = 0 AND follow_up_time < NOW() + ` + + rows, err := fs.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var pending []models.FollowUp + for rows.Next() { + var f models.FollowUp + var dealStatus, customerLevel, industry sql.NullString + var notificationSent int + + err := rows.Scan( + &f.ID, &f.CreatedAt, &f.CustomerName, + &dealStatus, &customerLevel, &industry, + &f.FollowUpTime, ¬ificationSent, + ) + if err != nil { + return nil, err + } + + f.DealStatus = dealStatus.String + f.CustomerLevel = customerLevel.String + f.Industry = industry.String + f.NotificationSent = notificationSent == 1 + + pending = append(pending, f) + } + + return pending, rows.Err() +} + +func (fs *mysqlFollowUpStorage) MarkNotificationSent(id string) error { + query := `UPDATE followups SET notification_sent = 1 WHERE id = ?` + _, err := fs.db.Exec(query, id) + return err +} + +func generateFollowUpMySQLUUID() string { + bytes := make([]byte, 16) + rand.Read(bytes) + bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant + return hex.EncodeToString(bytes) +} diff --git a/internal/storage/mysql_trial_period_storage.go b/internal/storage/mysql_trial_period_storage.go new file mode 100644 index 0000000..bbcfda6 --- /dev/null +++ b/internal/storage/mysql_trial_period_storage.go @@ -0,0 +1,212 @@ +package storage + +import ( + "crm-go/models" + "crypto/rand" + "database/sql" + "encoding/hex" + "fmt" + "strings" + "time" +) + +type mysqlTrialPeriodStorage struct { + db *sql.DB +} + +// NewMySQLTrialPeriodStorage 创建MySQL试用期存储 +func NewMySQLTrialPeriodStorage() TrialPeriodStorage { + return &mysqlTrialPeriodStorage{ + db: GetDB(), + } +} + +func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) { + query := ` + SELECT id, customer_name, start_time, end_time, is_trial, created_at + FROM trial_periods + ORDER BY created_at DESC + ` + + rows, err := ts.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var trialPeriods []models.TrialPeriod + for rows.Next() { + var tp models.TrialPeriod + var isTrial int + + err := rows.Scan( + &tp.ID, &tp.CustomerName, &tp.StartTime, + &tp.EndTime, &isTrial, &tp.CreatedAt, + ) + if err != nil { + return nil, err + } + + tp.IsTrial = isTrial == 1 + trialPeriods = append(trialPeriods, tp) + } + + return trialPeriods, rows.Err() +} + +func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) { + query := ` + SELECT id, customer_name, start_time, end_time, is_trial, created_at + FROM trial_periods + WHERE customer_name = ? + ORDER BY end_time DESC + ` + + rows, err := ts.db.Query(query, customerID) + if err != nil { + return nil, err + } + defer rows.Close() + + var trialPeriods []models.TrialPeriod + for rows.Next() { + var tp models.TrialPeriod + var isTrial int + + err := rows.Scan( + &tp.ID, &tp.CustomerName, &tp.StartTime, + &tp.EndTime, &isTrial, &tp.CreatedAt, + ) + if err != nil { + return nil, err + } + + tp.IsTrial = isTrial == 1 + trialPeriods = append(trialPeriods, tp) + } + + return trialPeriods, rows.Err() +} + +func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) { + query := ` + SELECT id, customer_name, start_time, end_time, is_trial, created_at + FROM trial_periods + WHERE id = ? + ` + + var tp models.TrialPeriod + var isTrial int + + err := ts.db.QueryRow(query, id).Scan( + &tp.ID, &tp.CustomerName, &tp.StartTime, + &tp.EndTime, &isTrial, &tp.CreatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + tp.IsTrial = isTrial == 1 + return &tp, nil +} + +func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPeriod) (*models.TrialPeriod, error) { + if trialPeriod.ID == "" { + trialPeriod.ID = generateTrialPeriodMySQLUUID() + } + if trialPeriod.CreatedAt.IsZero() { + trialPeriod.CreatedAt = time.Now() + } + + query := ` + INSERT INTO trial_periods (id, customer_name, start_time, end_time, is_trial, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ` + + isTrial := 0 + if trialPeriod.IsTrial { + isTrial = 1 + } + + _, err := ts.db.Exec(query, + trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.StartTime, + trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt, + ) + + if err != nil { + return nil, err + } + + return &trialPeriod, nil +} + +func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.UpdateTrialPeriodRequest) error { + // 首先获取现有记录 + existing, err := ts.GetTrialPeriodByID(id) + if err != nil || existing == nil { + return err + } + + // 应用更新 + if updates.CustomerName != nil { + existing.CustomerName = *updates.CustomerName + } + if updates.StartTime != nil { + startTime, err := time.Parse(time.RFC3339, *updates.StartTime) + if err == nil { + existing.StartTime = startTime + } + } + if updates.EndTime != nil { + endTime, err := time.Parse(time.RFC3339, *updates.EndTime) + if err == nil { + existing.EndTime = endTime + } + } + if updates.IsTrial != nil { + existing.IsTrial = *updates.IsTrial + } + + query := ` + UPDATE trial_periods + SET customer_name = ?, start_time = ?, end_time = ?, is_trial = ? + WHERE id = ? + ` + + isTrial := 0 + if existing.IsTrial { + isTrial = 1 + } + + _, err = ts.db.Exec(query, + existing.CustomerName, existing.StartTime, existing.EndTime, + isTrial, id, + ) + + return err +} + +func (ts *mysqlTrialPeriodStorage) DeleteTrialPeriod(id string) error { + query := `DELETE FROM trial_periods WHERE id = ?` + _, err := ts.db.Exec(query, id) + if err != nil { + // 检查是否是权限不足错误 + if strings.Contains(err.Error(), "command denied") || strings.Contains(err.Error(), "DELETE command denied") { + return fmt.Errorf("数据库权限不足:无法执行删除操作,请联系管理员") + } + return err + } + return nil +} + +func generateTrialPeriodMySQLUUID() string { + bytes := make([]byte, 16) + rand.Read(bytes) + bytes[6] = (bytes[6] & 0x0f) | 0x40 // Version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80 // Variant + return hex.EncodeToString(bytes) +} diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..abfa6d8 --- /dev/null +++ b/start.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# CRM系统启动脚本 + +# 加载环境变量 (如果存在.env文件) +if [ -f .env ]; then + export $(cat .env | grep -v '^#' | xargs) +fi + +# 默认配置 +export DB_HOST=${DB_HOST:-localhost} +export DB_PORT=${DB_PORT:-3306} +export DB_USER=${DB_USER:-root} +export DB_PASSWORD=${DB_PASSWORD:-} +export DB_NAME=${DB_NAME:-crm_db} +export STORAGE_MODE=${STORAGE_MODE:-mysql} +export PORT=${PORT:-8081} + +echo "=====================================" +echo "CRM 系统启动配置" +echo "=====================================" +echo "存储模式: $STORAGE_MODE" +if [ "$STORAGE_MODE" = "mysql" ]; then + echo "数据库主机: $DB_HOST:$DB_PORT" + echo "数据库名称: $DB_NAME" + echo "数据库用户: $DB_USER" +fi +echo "服务端口: $PORT" +echo "=====================================" + +# 构建项目 +echo "正在构建项目..." +go build -o crm-server ./cmd/server/ + +if [ $? -eq 0 ]; then + echo "构建成功,启动服务..." + ./crm-server +else + echo "构建失败!" + exit 1 +fi diff --git a/deduplicate_customers.go b/tools/deduplicate_customers.go similarity index 100% rename from deduplicate_customers.go rename to tools/deduplicate_customers.go diff --git a/migrate_to_mysql.go b/tools/migrate_to_mysql.go similarity index 100% rename from migrate_to_mysql.go rename to tools/migrate_to_mysql.go