upload_db

This commit is contained in:
hangyu.tao 2026-01-26 11:46:06 +08:00
parent 872434b09c
commit 17752f0fd7
13 changed files with 951 additions and 13 deletions

17
.env Normal file
View File

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

17
.env.example Normal file
View File

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

View File

@ -19,25 +19,84 @@ A web-based CRM system built with Go backend and HTML/CSS/JavaScript frontend.
- Backend: Go - Backend: Go
- Frontend: HTML, CSS, JavaScript - Frontend: HTML, CSS, JavaScript
- Charts: Chart.js (via CDN) - 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 ## How to Run
1. Navigate to the project directory: ### 方法一:使用启动脚本
1. 复制并修改配置文件:
```bash ```bash
cd /Users/d-robotics/crm/crm-go cp .env.example .env
# 编辑 .env 文件设置数据库密码等
``` ```
2. Run the server: 2. 运行启动脚本:
```bash ```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: 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 ## Usage
### Customer Management ### Customer Management

View File

@ -14,10 +14,36 @@ import (
) )
func main() { func main() {
// Initialize storage // 获取存储模式默认使用MySQL
customerStorage := storage.NewCustomerStorage("./data/customers.json") storageMode := os.Getenv("STORAGE_MODE")
followUpStorage := storage.NewFollowUpStorage("./data/followups.json") if storageMode == "" {
trialPeriodStorage := storage.NewTrialPeriodStorage("./data/trial_periods.json") 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 // Get Feishu webhook URL from environment variable
feishuWebhook := "https://open.feishu.cn/open-apis/bot/v2/hook/d75c14ad-d782-489e-8a99-81b511ee4abd" feishuWebhook := "https://open.feishu.cn/open-apis/bot/v2/hook/d75c14ad-d782-489e-8a99-81b511ee4abd"

12
go.mod
View File

@ -1,11 +1,17 @@
module crm-go module crm-go
go 1.21 go 1.21.0
require github.com/xuri/excelize/v2 v2.8.0 toolchain go1.24.3
require ( 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect github.com/richardlehane/msoleps v1.0.3 // indirect

4
go.sum
View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=

89
internal/storage/db.go Normal file
View File

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

View File

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

View File

@ -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, &notificationSent,
)
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, &notificationSent,
)
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, &notificationSent,
)
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)
}

View File

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

41
start.sh Executable file
View File

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