upload_db
This commit is contained in:
parent
872434b09c
commit
17752f0fd7
17
.env
Normal file
17
.env
Normal 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
17
.env.example
Normal 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
|
||||
71
README.md
71
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
|
||||
|
||||
@ -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"
|
||||
|
||||
12
go.mod
12
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
89
internal/storage/db.go
Normal file
89
internal/storage/db.go
Normal 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
|
||||
}
|
||||
218
internal/storage/mysql_customer_storage.go
Normal file
218
internal/storage/mysql_customer_storage.go
Normal 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)
|
||||
}
|
||||
249
internal/storage/mysql_followup_storage.go
Normal file
249
internal/storage/mysql_followup_storage.go
Normal 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, ¬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)
|
||||
}
|
||||
212
internal/storage/mysql_trial_period_storage.go
Normal file
212
internal/storage/mysql_trial_period_storage.go
Normal 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
41
start.sh
Executable 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
|
||||
Loading…
x
Reference in New Issue
Block a user