feat: 添加企业线索管理功能,包括线索的创建、更新、删除和查询接口,更新相关文档和数据库结构

This commit is contained in:
wood911010 committed 2026-02-01 13:19:29 +08:00
1 parent d6890462a3
commit b8c21c24cd
11 files changed
+2500

No files matched your search

@@ -0,0 +1,210 @@
# 企业线索收集
从各个渠道收集用户线索,并进行跟进和转化。
## 表设计
```sql
-- 企业线索表
CREATE TABLE leads (
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
lead_no VARCHAR(50) NOT NULL COMMENT '线索编号,系统生成',
company_name VARCHAR(200) NOT NULL COMMENT '企业/机构名称',
contact_name VARCHAR(100) NOT NULL COMMENT '联系人',
contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话',
contact_email VARCHAR(100) COMMENT '邮箱地址',
requirement TEXT NOT NULL COMMENT '需求描述',
extra_info TEXT COMMENT '补充信息',
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending-待联系, contacted-已联系, converted-已成交, invalid-已失效',
follow_up_date DATETIME COMMENT '跟进日期',
follow_up_record TEXT COMMENT '跟进记录',
internal_note TEXT COMMENT '内部备注',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_leads_status (status),
INDEX idx_leads_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业线索表';
```
## 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | VARCHAR(36) | 是 | 主键,UUID |
| lead_no | VARCHAR(50) | 是 | 线索编号,系统生成,格式:LEAD + 时间戳 + 6位随机数 |
| company_name | VARCHAR(200) | 是 | 企业/机构名称 |
| contact_name | VARCHAR(100) | 是 | 联系人姓名 |
| contact_phone | VARCHAR(20) | 是 | 联系电话 |
| contact_email | VARCHAR(100) | 否 | 邮箱地址 |
| requirement | TEXT | 是 | 需求描述 |
| extra_info | TEXT | 否 | 补充信息 |
| status | VARCHAR(20) | 是 | 状态:pending-待联系,contacted-已联系,converted-已成交,invalid-已失效 |
| follow_up_date | DATETIME | 否 | 跟进日期 |
| follow_up_record | TEXT | 否 | 跟进记录 |
| internal_note | TEXT | 否 | 内部备注 |
| created_at | DATETIME | 是 | 创建时间 |
| updated_at | DATETIME | 是 | 更新时间 |
## 状态流转
```
pending (待联系) → contacted (已联系) → converted (已成交)
↓
invalid (已失效)
```
## 接口设计
### 1. 新增线索(公开接口)
```
POST /api/leads
```
**无需鉴权**
**请求体:**
```json
{
"company_name": "北京科技有限公司",
"contact_name": "张三",
"contact_phone": "13800138000",
"contact_email": "zhangsan@example.com",
"requirement": "需要采购50套软件授权",
"extra_info": "希望了解批量购买优惠"
}
```
**响应:**
```json
{
"code": "000000",
"data": {
"id": "uuid",
"lead_no": "LEAD20260201123456ABC123",
"company_name": "北京科技有限公司",
"contact_name": "张三",
"contact_phone": "13800138000",
"contact_email": "zhangsan@example.com",
"requirement": "需要采购50套软件授权",
"status": "pending",
"created_at": "2026-02-01T12:00:00Z"
}
}
```
### 2. 线索列表(管理端)
```
GET /api/leads
```
**需要管理员鉴权**
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | int | 否 | 页码,默认1 |
| page_size | int | 否 | 每页数量,默认10,最大100 |
| search | string | 否 | 关键词检索(企业名称/联系人/联系电话) |
| status | string | 否 | 状态筛选:pending/contacted/converted/invalid |
**响应:**
```json
{
"code": "000000",
"data": {
"leads": [
{
"id": "uuid",
"lead_no": "LEAD20260201123456ABC123",
"company_name": "北京科技有限公司",
"contact_name": "张三",
"contact_phone": "13800138000",
"status": "pending",
"created_at": "2026-02-01T12:00:00Z"
}
],
"total_count": 100,
"page": 1,
"page_size": 10
}
}
```
### 3. 线索详情(管理端)
```
GET /api/leads/{id}
```
**需要管理员鉴权**
**响应:**
```json
{
"code": "000000",
"data": {
"id": "uuid",
"lead_no": "LEAD20260201123456ABC123",
"company_name": "北京科技有限公司",
"contact_name": "张三",
"contact_phone": "13800138000",
"contact_email": "zhangsan@example.com",
"requirement": "需要采购50套软件授权",
"extra_info": "希望了解批量购买优惠",
"status": "contacted",
"follow_up_date": "2026-02-05T10:00:00Z",
"follow_up_record": "已电话沟通,客户表示下周决策",
"internal_note": "重点跟进客户",
"created_at": "2026-02-01T12:00:00Z",
"updated_at": "2026-02-05T10:00:00Z"
}
}
```
### 4. 线索跟进(管理端)
```
PUT /api/leads/{id}
```
**需要管理员鉴权**
**请求体:**
```json
{
"status": "contacted",
"follow_up_date": "2026-02-05T10:00:00Z",
"follow_up_record": "已电话沟通,客户表示下周决策",
"internal_note": "重点跟进客户"
}
```
**响应:**
```json
{
"code": "000000",
"message": "成功"
}
```
### 5. 删除线索(管理端)
```
DELETE /api/leads/{id}
```
**需要管理员鉴权**
**响应:**
```json
{
"code": "000000",
"message": "成功"
}
```
## 实现文件
### 新建文件
- `backend/internal/models/lead.go` - 线索模型
- `backend/internal/repository/lead_repository.go` - 线索仓储
- `backend/internal/service/lead_service.go` - 线索服务
- `backend/internal/api/handlers/lead_handler.go` - 线索处理器
- `backend/migrations/014_create_leads_table.sql` - 数据库迁移
### 修改文件
- `backend/internal/api/routes/router.go` - 添加路由
- `backend/internal/database/migration.go` - 添加自动迁移和初始化
+594
View File
@@ -2824,6 +2824,447 @@ const docTemplate = `{
}
}
},
"/api/leads": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "管理员获取线索列表,支持分页和筛选",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "获取线索列表",
"parameters": [
{
"minimum": 1,
"type": "integer",
"description": "页码,默认1",
"name": "page",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"description": "每页条数,默认10,最大100",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "关键词检索",
"name": "search",
"in": "query"
},
{
"type": "string",
"description": "状态筛选",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadListResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"post": {
"description": "用户提交企业线索,无需鉴权",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "创建线索",
"parameters": [
{
"description": "线索创建请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.LeadCreateRequest"
}
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/leads/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID获取线索详细信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "获取线索详情",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "更新线索信息,用于线索跟进",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "更新线索",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "线索更新请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.LeadUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "删除指定线索",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "删除线索",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/leads/{id}/status": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "快速更新线索状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "更新线索状态",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "状态: pending/contacted/converted/invalid",
"name": "status",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/packages": {
"get": {
"security": [
@@ -7838,6 +8279,159 @@ const docTemplate = `{
}
}
},
"models.LeadCreateRequest": {
"type": "object",
"required": [
"company_name",
"contact_name",
"contact_phone",
"requirement"
],
"properties": {
"company_name": {
"type": "string",
"maxLength": 200,
"minLength": 1
},
"contact_email": {
"type": "string",
"maxLength": 100
},
"contact_name": {
"type": "string",
"maxLength": 100,
"minLength": 1
},
"contact_phone": {
"type": "string",
"maxLength": 20,
"minLength": 1
},
"extra_info": {
"type": "string"
},
"requirement": {
"type": "string",
"minLength": 1
}
}
},
"models.LeadListResponse": {
"type": "object",
"properties": {
"leads": {
"type": "array",
"items": {
"$ref": "#/definitions/models.LeadResponse"
}
},
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total_count": {
"type": "integer"
}
}
},
"models.LeadResponse": {
"type": "object",
"properties": {
"company_name": {
"type": "string"
},
"contact_email": {
"type": "string"
},
"contact_name": {
"type": "string"
},
"contact_phone": {
"type": "string"
},
"created_at": {
"type": "string"
},
"extra_info": {
"type": "string"
},
"follow_up_date": {
"type": "string"
},
"follow_up_record": {
"type": "string"
},
"id": {
"type": "string"
},
"internal_note": {
"type": "string"
},
"lead_no": {
"type": "string"
},
"requirement": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"models.LeadUpdateRequest": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"maxLength": 200,
"minLength": 1
},
"contact_email": {
"type": "string",
"maxLength": 100
},
"contact_name": {
"type": "string",
"maxLength": 100,
"minLength": 1
},
"contact_phone": {
"type": "string",
"maxLength": 20,
"minLength": 1
},
"extra_info": {
"type": "string"
},
"follow_up_date": {
"type": "string"
},
"follow_up_record": {
"type": "string"
},
"internal_note": {
"type": "string"
},
"requirement": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"pending",
"contacted",
"converted",
"invalid"
]
}
}
},
"models.License": {
"type": "object",
"properties": {
+594
View File
@@ -2820,6 +2820,447 @@
}
}
},
"/api/leads": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "管理员获取线索列表,支持分页和筛选",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "获取线索列表",
"parameters": [
{
"minimum": 1,
"type": "integer",
"description": "页码,默认1",
"name": "page",
"in": "query"
},
{
"maximum": 100,
"minimum": 1,
"type": "integer",
"description": "每页条数,默认10,最大100",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "关键词检索",
"name": "search",
"in": "query"
},
{
"type": "string",
"description": "状态筛选",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadListResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"post": {
"description": "用户提交企业线索,无需鉴权",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "创建线索",
"parameters": [
{
"description": "线索创建请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.LeadCreateRequest"
}
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/leads/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "根据ID获取线索详细信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "获取线索详情",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "更新线索信息,用于线索跟进",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "更新线索",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "线索更新请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.LeadUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "删除指定线索",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "删除线索",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/leads/{id}/status": {
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "快速更新线索状态",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"企业线索"
],
"summary": "更新线索状态",
"parameters": [
{
"type": "string",
"description": "线索ID",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "状态: pending/contacted/converted/invalid",
"name": "status",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.LeadResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数无效",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"401": {
"description": "未认证",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"403": {
"description": "权限不足",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "线索不存在",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/api/packages": {
"get": {
"security": [
@@ -7834,6 +8275,159 @@
}
}
},
"models.LeadCreateRequest": {
"type": "object",
"required": [
"company_name",
"contact_name",
"contact_phone",
"requirement"
],
"properties": {
"company_name": {
"type": "string",
"maxLength": 200,
"minLength": 1
},
"contact_email": {
"type": "string",
"maxLength": 100
},
"contact_name": {
"type": "string",
"maxLength": 100,
"minLength": 1
},
"contact_phone": {
"type": "string",
"maxLength": 20,
"minLength": 1
},
"extra_info": {
"type": "string"
},
"requirement": {
"type": "string",
"minLength": 1
}
}
},
"models.LeadListResponse": {
"type": "object",
"properties": {
"leads": {
"type": "array",
"items": {
"$ref": "#/definitions/models.LeadResponse"
}
},
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total_count": {
"type": "integer"
}
}
},
"models.LeadResponse": {
"type": "object",
"properties": {
"company_name": {
"type": "string"
},
"contact_email": {
"type": "string"
},
"contact_name": {
"type": "string"
},
"contact_phone": {
"type": "string"
},
"created_at": {
"type": "string"
},
"extra_info": {
"type": "string"
},
"follow_up_date": {
"type": "string"
},
"follow_up_record": {
"type": "string"
},
"id": {
"type": "string"
},
"internal_note": {
"type": "string"
},
"lead_no": {
"type": "string"
},
"requirement": {
"type": "string"
},
"status": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"models.LeadUpdateRequest": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"maxLength": 200,
"minLength": 1
},
"contact_email": {
"type": "string",
"maxLength": 100
},
"contact_name": {
"type": "string",
"maxLength": 100,
"minLength": 1
},
"contact_phone": {
"type": "string",
"maxLength": 20,
"minLength": 1
},
"extra_info": {
"type": "string"
},
"follow_up_date": {
"type": "string"
},
"follow_up_record": {
"type": "string"
},
"internal_note": {
"type": "string"
},
"requirement": {
"type": "string",
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"pending",
"contacted",
"converted",
"invalid"
]
}
}
},
"models.License": {
"type": "object",
"properties": {
+380
View File
@@ -1569,6 +1569,114 @@ definitions:
description: 上传后的文件URL
type: string
type: object
models.LeadCreateRequest:
properties:
company_name:
maxLength: 200
minLength: 1
type: string
contact_email:
maxLength: 100
type: string
contact_name:
maxLength: 100
minLength: 1
type: string
contact_phone:
maxLength: 20
minLength: 1
type: string
extra_info:
type: string
requirement:
minLength: 1
type: string
required:
- company_name
- contact_name
- contact_phone
- requirement
type: object
models.LeadListResponse:
properties:
leads:
items:
$ref: '#/definitions/models.LeadResponse'
type: array
page:
type: integer
page_size:
type: integer
total_count:
type: integer
type: object
models.LeadResponse:
properties:
company_name:
type: string
contact_email:
type: string
contact_name:
type: string
contact_phone:
type: string
created_at:
type: string
extra_info:
type: string
follow_up_date:
type: string
follow_up_record:
type: string
id:
type: string
internal_note:
type: string
lead_no:
type: string
requirement:
type: string
status:
type: string
updated_at:
type: string
type: object
models.LeadUpdateRequest:
properties:
company_name:
maxLength: 200
minLength: 1
type: string
contact_email:
maxLength: 100
type: string
contact_name:
maxLength: 100
minLength: 1
type: string
contact_phone:
maxLength: 20
minLength: 1
type: string
extra_info:
type: string
follow_up_date:
type: string
follow_up_record:
type: string
internal_note:
type: string
requirement:
minLength: 1
type: string
status:
enum:
- pending
- contacted
- converted
- invalid
type: string
type: object
models.License:
properties:
activated_at:
@@ -3886,6 +3994,278 @@ paths:
summary: 获取指定类型的枚举值
tags:
- 枚举管理
/api/leads:
get:
consumes:
- application/json
description: 管理员获取线索列表,支持分页和筛选
parameters:
- description: 页码,默认1
in: query
minimum: 1
name: page
type: integer
- description: 每页条数,默认10,最大100
in: query
maximum: 100
minimum: 1
name: page_size
type: integer
- description: 关键词检索
in: query
name: search
type: string
- description: 状态筛选
in: query
name: status
type: string
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.LeadListResponse'
type: object
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 权限不足
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 获取线索列表
tags:
- 企业线索
post:
consumes:
- application/json
description: 用户提交企业线索,无需鉴权
parameters:
- description: 线索创建请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/models.LeadCreateRequest'
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.LeadResponse'
type: object
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: 创建线索
tags:
- 企业线索
/api/leads/{id}:
delete:
consumes:
- application/json
description: 删除指定线索
parameters:
- description: 线索ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功
schema:
$ref: '#/definitions/models.APIResponse'
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 权限不足
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 线索不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 删除线索
tags:
- 企业线索
get:
consumes:
- application/json
description: 根据ID获取线索详细信息
parameters:
- description: 线索ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.LeadResponse'
type: object
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 权限不足
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 线索不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 获取线索详情
tags:
- 企业线索
put:
consumes:
- application/json
description: 更新线索信息,用于线索跟进
parameters:
- description: 线索ID
in: path
name: id
required: true
type: string
- description: 线索更新请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/models.LeadUpdateRequest'
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.LeadResponse'
type: object
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 权限不足
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 线索不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 更新线索
tags:
- 企业线索
/api/leads/{id}/status:
put:
consumes:
- application/json
description: 快速更新线索状态
parameters:
- description: 线索ID
in: path
name: id
required: true
type: string
- description: '状态: pending/contacted/converted/invalid'
in: query
name: status
required: true
type: string
produces:
- application/json
responses:
"200":
description: 成功
schema:
allOf:
- $ref: '#/definitions/models.APIResponse'
- properties:
data:
$ref: '#/definitions/models.LeadResponse'
type: object
"400":
description: 请求参数无效
schema:
$ref: '#/definitions/models.ErrorResponse'
"401":
description: 未认证
schema:
$ref: '#/definitions/models.ErrorResponse'
"403":
description: 权限不足
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: 线索不存在
schema:
$ref: '#/definitions/models.ErrorResponse'
security:
- BearerAuth: []
summary: 更新线索状态
tags:
- 企业线索
/api/packages:
get:
consumes:
@@ -0,0 +1,278 @@
package handlers
import (
"net/http"
"license-manager/internal/api/middleware"
"license-manager/internal/models"
"license-manager/internal/service"
"license-manager/pkg/i18n"
"github.com/gin-gonic/gin"
)
type LeadHandler struct {
leadService service.LeadService
}
func NewLeadHandler(leadService service.LeadService) *LeadHandler {
return &LeadHandler{
leadService: leadService,
}
}
// CreateLead 创建线索
// @Summary 创建线索
// @Description 用户提交企业线索,无需鉴权
// @Tags 企业线索
// @Accept json
// @Produce json
// @Param request body models.LeadCreateRequest true "线索创建请求"
// @Success 200 {object} models.APIResponse{data=models.LeadResponse} "成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads [post]
func (h *LeadHandler) CreateLead(c *gin.Context) {
lang := middleware.GetLanguage(c)
var req models.LeadCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lead, err := h.leadService.CreateLead(c.Request.Context(), &req)
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: lead.ToResponse(),
Timestamp: getCurrentTimestamp(),
})
}
// GetLeads 获取线索列表
// @Summary 获取线索列表
// @Description 管理员获取线索列表,支持分页和筛选
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码,默认1" minimum(1)
// @Param page_size query int false "每页条数,默认10,最大100" minimum(1) maximum(100)
// @Param search query string false "关键词检索"
// @Param status query string false "状态筛选"
// @Success 200 {object} models.APIResponse{data=models.LeadListResponse} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads [get]
func (h *LeadHandler) GetLeads(c *gin.Context) {
lang := middleware.GetLanguage(c)
var req models.LeadListRequest
if err := c.ShouldBindQuery(&req); err != nil {
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
result, err := h.leadService.GetLeadList(c.Request.Context(), &req)
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: result,
Timestamp: getCurrentTimestamp(),
})
}
// GetLead 获取线索详情
// @Summary 获取线索详情
// @Description 根据ID获取线索详细信息
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "线索ID"
// @Success 200 {object} models.APIResponse{data=models.LeadResponse} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 404 {object} models.ErrorResponse "线索不存在"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads/{id} [get]
func (h *LeadHandler) GetLead(c *gin.Context) {
lang := middleware.GetLanguage(c)
id := c.Param("id")
lead, err := h.leadService.GetLead(c.Request.Context(), id)
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: lead.ToResponse(),
Timestamp: getCurrentTimestamp(),
})
}
// UpdateLead 更新线索
// @Summary 更新线索
// @Description 更新线索信息,用于线索跟进
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "线索ID"
// @Param request body models.LeadUpdateRequest true "线索更新请求"
// @Success 200 {object} models.APIResponse{data=models.LeadResponse} "成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 404 {object} models.ErrorResponse "线索不存在"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads/{id} [put]
func (h *LeadHandler) UpdateLead(c *gin.Context) {
lang := middleware.GetLanguage(c)
id := c.Param("id")
var req models.LeadUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message + ": " + err.Error(),
Timestamp: getCurrentTimestamp(),
})
return
}
lead, err := h.leadService.UpdateLead(c.Request.Context(), id, &req)
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: lead.ToResponse(),
Timestamp: getCurrentTimestamp(),
})
}
// DeleteLead 删除线索
// @Summary 删除线索
// @Description 删除指定线索
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "线索ID"
// @Success 200 {object} models.APIResponse{} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 404 {object} models.ErrorResponse "线索不存在"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads/{id} [delete]
func (h *LeadHandler) DeleteLead(c *gin.Context) {
lang := middleware.GetLanguage(c)
id := c.Param("id")
if err := h.leadService.DeleteLead(c.Request.Context(), id); err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: nil,
Timestamp: getCurrentTimestamp(),
})
}
// UpdateLeadStatus 更新线索状态
// @Summary 更新线索状态
// @Description 快速更新线索状态
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "线索ID"
// @Param status query string true "状态: pending/contacted/converted/invalid"
// @Success 200 {object} models.APIResponse{data=models.LeadResponse} "成功"
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 404 {object} models.ErrorResponse "线索不存在"
// @Router /api/leads/{id}/status [put]
func (h *LeadHandler) UpdateLeadStatus(c *gin.Context) {
lang := middleware.GetLanguage(c)
id := c.Param("id")
status := c.Query("status")
if status == "" {
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(status, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
// 验证状态值
validStatuses := map[string]bool{
string(models.LeadStatusPending): true,
string(models.LeadStatusContacted): true,
string(models.LeadStatusConverted): true,
string(models.LeadStatusInvalid): true,
}
if !validStatuses[status] {
statusCode, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
c.JSON(statusCode, models.ErrorResponse{
Code: errCode,
Message: message,
Timestamp: getCurrentTimestamp(),
})
return
}
req := &models.LeadUpdateRequest{
Status: status,
}
lead, err := h.leadService.UpdateLead(c.Request.Context(), id, req)
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: lead.ToResponse(),
Timestamp: getCurrentTimestamp(),
})
}
+13
View File
@@ -50,6 +50,7 @@ func SetupRouter() *gin.Engine {
cuInvoiceRepo := repository.NewCuInvoiceRepository(db)
adminInvoiceRepo := repository.NewAdminInvoiceRepository(db)
packageRepo := repository.NewPackageRepository(db)
leadRepo := repository.NewLeadRepository(db)
// 获取logger实例
log := logger.GetLogger()
@@ -118,6 +119,8 @@ func SetupRouter() *gin.Engine {
adminInvoiceService := service.NewAdminInvoiceService(adminInvoiceRepo, cuOrderRepo, userRepo, cuUserRepo, db)
adminInvoiceHandler := handlers.NewAdminInvoiceHandler(adminInvoiceService)
packageHandler := handlers.NewPackageHandler(packageService)
leadService := service.NewLeadService(leadRepo, db)
leadHandler := handlers.NewLeadHandler(leadService)
// 健康检测接口(无需认证)
router.GET("/health", systemHandler.HealthCheck)
@@ -143,6 +146,9 @@ func SetupRouter() *gin.Engine {
// 公共发票下载接口
public.GET("/public/invoices/download", adminInvoiceHandler.DownloadByToken)
// 线索收集(无需鉴权)
public.POST("/leads", leadHandler.CreateLead)
}
// 需要认证的接口
@@ -204,6 +210,13 @@ func SetupRouter() *gin.Engine {
auth.PUT("/packages/:id", packageHandler.UpdatePackage)
auth.DELETE("/packages/:id", packageHandler.DeletePackage)
auth.PUT("/packages/:id/status", packageHandler.UpdatePackageStatus)
// 线索管理(管理员)
auth.GET("/leads", leadHandler.GetLeads)
auth.GET("/leads/:id", leadHandler.GetLead)
auth.PUT("/leads/:id", leadHandler.UpdateLead)
auth.PUT("/leads/:id/status", leadHandler.UpdateLeadStatus)
auth.DELETE("/leads/:id", leadHandler.DeleteLead)
}
// 管理员接口
+1
View File
@@ -31,6 +31,7 @@ func AutoMigrate() error {
&models.AuthorizationChange{},
&models.Invoice{}, // 发票表
&models.Package{}, // 套餐表
&models.Lead{}, // 线索表
)
if err != nil {
return fmt.Errorf("failed to migrate database: %w", err)
+154
View File
@@ -0,0 +1,154 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// LeadStatus 线索状态
type LeadStatus string
const (
LeadStatusPending LeadStatus = "pending" // 待联系
LeadStatusContacted LeadStatus = "contacted" // 已联系
LeadStatusConverted LeadStatus = "converted" // 已成交
LeadStatusInvalid LeadStatus = "invalid" // 已失效
)
// Lead 企业线索
type Lead struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
LeadNo string `gorm:"type:varchar(50);not null;uniqueIndex" json:"lead_no"`
CompanyName string `gorm:"type:varchar(200);not null" json:"company_name"`
ContactName string `gorm:"type:varchar(100);not null" json:"contact_name"`
ContactPhone string `gorm:"type:varchar(20);not null" json:"contact_phone"`
ContactEmail string `gorm:"type:varchar(100)" json:"contact_email"`
Requirement string `gorm:"type:text;not null" json:"requirement"`
ExtraInfo string `gorm:"type:text" json:"extra_info"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"`
FollowUpDate *time.Time `gorm:"" json:"follow_up_date"`
FollowUpRecord string `gorm:"type:text" json:"follow_up_record"`
InternalNote string `gorm:"type:text" json:"internal_note"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
}
// TableName 指定表名
func (Lead) TableName() string {
return "leads"
}
// BeforeCreate 创建前自动设置ID和时间戳
func (l *Lead) BeforeCreate(tx *gorm.DB) error {
if l.ID == "" {
l.ID = uuid.New().String()
}
now := time.Now()
if l.CreatedAt.IsZero() {
l.CreatedAt = now
}
if l.UpdatedAt.IsZero() {
l.UpdatedAt = now
}
// 生成线索编号
if l.LeadNo == "" {
l.LeadNo = generateLeadNo()
}
return nil
}
// generateLeadNo 生成线索编号
func generateLeadNo() string {
return "LEAD" + time.Now().Format("20060102150405") + randomString(6)
}
// randomString 生成随机字符串
func randomString(length int) string {
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result := make([]byte, length)
now := time.Now().UnixNano()
for i := range result {
result[i] = charset[(now+int64(i)*17)%int64(len(charset))]
}
return string(result)
}
// ToResponse 转换为响应结构
func (l *Lead) ToResponse() *LeadResponse {
return &LeadResponse{
ID: l.ID,
LeadNo: l.LeadNo,
CompanyName: l.CompanyName,
ContactName: l.ContactName,
ContactPhone: l.ContactPhone,
ContactEmail: l.ContactEmail,
Requirement: l.Requirement,
ExtraInfo: l.ExtraInfo,
Status: l.Status,
FollowUpDate: l.FollowUpDate,
FollowUpRecord: l.FollowUpRecord,
InternalNote: l.InternalNote,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
}
}
// LeadResponse 线索响应结构
type LeadResponse struct {
ID string `json:"id"`
LeadNo string `json:"lead_no"`
CompanyName string `json:"company_name"`
ContactName string `json:"contact_name"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
Requirement string `json:"requirement"`
ExtraInfo string `json:"extra_info"`
Status string `json:"status"`
FollowUpDate *time.Time `json:"follow_up_date"`
FollowUpRecord string `json:"follow_up_record"`
InternalNote string `json:"internal_note"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// LeadCreateRequest 创建线索请求
type LeadCreateRequest struct {
CompanyName string `json:"company_name" binding:"required,min=1,max=200"`
ContactName string `json:"contact_name" binding:"required,min=1,max=100"`
ContactPhone string `json:"contact_phone" binding:"required,min=1,max=20"`
ContactEmail string `json:"contact_email" binding:"omitempty,email,max=100"`
Requirement string `json:"requirement" binding:"required,min=1"`
ExtraInfo string `json:"extra_info"`
}
// LeadUpdateRequest 更新线索请求
type LeadUpdateRequest struct {
CompanyName string `json:"company_name" binding:"omitempty,min=1,max=200"`
ContactName string `json:"contact_name" binding:"omitempty,min=1,max=100"`
ContactPhone string `json:"contact_phone" binding:"omitempty,min=1,max=20"`
ContactEmail string `json:"contact_email" binding:"omitempty,email,max=100"`
Requirement string `json:"requirement" binding:"omitempty,min=1"`
ExtraInfo string `json:"extra_info"`
Status string `json:"status" binding:"omitempty,oneof=pending contacted converted invalid"`
FollowUpDate *time.Time `json:"follow_up_date"`
FollowUpRecord string `json:"follow_up_record"`
InternalNote string `json:"internal_note"`
}
// LeadListRequest 线索列表请求
type LeadListRequest struct {
Page int `form:"page" binding:"omitempty,min=1"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"`
Search string `form:"search"` // 关键词检索
Status string `form:"status"` // 状态筛选
}
// LeadListResponse 线索列表响应
type LeadListResponse struct {
Leads []*LeadResponse `json:"leads"`
TotalCount int64 `json:"total_count"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
@@ -0,0 +1,97 @@
package repository
import (
"context"
"license-manager/internal/models"
"gorm.io/gorm"
)
// LeadRepository 线索仓储接口
type LeadRepository interface {
Create(lead *models.Lead) error
Update(lead *models.Lead) error
Delete(id string) error
GetByID(id string) (*models.Lead, error)
GetList(ctx context.Context, req *models.LeadListRequest) ([]*models.Lead, int64, error)
}
type leadRepository struct {
db *gorm.DB
}
// NewLeadRepository 创建线索仓储
func NewLeadRepository(db *gorm.DB) LeadRepository {
return &leadRepository{db: db}
}
func (r *leadRepository) Create(lead *models.Lead) error {
return r.db.Create(lead).Error
}
func (r *leadRepository) Update(lead *models.Lead) error {
return r.db.Save(lead).Error
}
func (r *leadRepository) Delete(id string) error {
return r.db.Unscoped().Where("id = ?", id).Delete(&models.Lead{}).Error
}
func (r *leadRepository) GetByID(id string) (*models.Lead, error) {
var lead models.Lead
err := r.db.Where("id = ?", id).First(&lead).Error
if err != nil {
return nil, err
}
return &lead, nil
}
func (r *leadRepository) GetList(ctx context.Context, req *models.LeadListRequest) ([]*models.Lead, int64, error) {
var leads []*models.Lead
var total int64
// 默认值
page := 1
pageSize := 10
search := ""
status := ""
if req != nil {
if req.Page > 0 {
page = req.Page
}
if req.PageSize > 0 {
pageSize = req.PageSize
}
if pageSize > 100 {
pageSize = 100
}
search = req.Search
status = req.Status
}
offset := (page - 1) * pageSize
query := r.db.WithContext(ctx).Model(&models.Lead{})
if status != "" {
query = query.Where("status = ?", status)
}
if search != "" {
like := "%" + search + "%"
query = query.Where("(company_name LIKE ? OR contact_name LIKE ? OR contact_phone LIKE ?)", like, like, like)
}
// 获取总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// 获取列表,按创建时间降序
err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&leads).Error
if err != nil {
return nil, 0, err
}
return leads, total, nil
}
+159
View File
@@ -0,0 +1,159 @@
package service
import (
"context"
"license-manager/internal/models"
"license-manager/internal/repository"
pkgcontext "license-manager/pkg/context"
"license-manager/pkg/i18n"
"time"
"gorm.io/gorm"
)
// LeadService 线索服务接口
type LeadService interface {
CreateLead(ctx context.Context, req *models.LeadCreateRequest) (*models.Lead, error)
UpdateLead(ctx context.Context, id string, req *models.LeadUpdateRequest) (*models.Lead, error)
DeleteLead(ctx context.Context, id string) error
GetLead(ctx context.Context, id string) (*models.Lead, error)
GetLeadList(ctx context.Context, req *models.LeadListRequest) (*models.LeadListResponse, error)
}
type leadService struct {
repo repository.LeadRepository
db *gorm.DB
}
// NewLeadService 创建线索服务
func NewLeadService(repo repository.LeadRepository, db *gorm.DB) LeadService {
return &leadService{
repo: repo,
db: db,
}
}
func (s *leadService) CreateLead(ctx context.Context, req *models.LeadCreateRequest) (*models.Lead, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
lead := &models.Lead{
CompanyName: req.CompanyName,
ContactName: req.ContactName,
ContactPhone: req.ContactPhone,
ContactEmail: req.ContactEmail,
Requirement: req.Requirement,
ExtraInfo: req.ExtraInfo,
Status: string(models.LeadStatusPending),
}
if err := s.repo.Create(lead); err != nil {
return nil, i18n.NewI18nError("900004", lang, err.Error())
}
return lead, nil
}
func (s *leadService) UpdateLead(ctx context.Context, id string, req *models.LeadUpdateRequest) (*models.Lead, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
lead, err := s.repo.GetByID(id)
if err != nil {
return nil, i18n.NewI18nError("900002", lang)
}
// 更新字段
if req.CompanyName != "" {
lead.CompanyName = req.CompanyName
}
if req.ContactName != "" {
lead.ContactName = req.ContactName
}
if req.ContactPhone != "" {
lead.ContactPhone = req.ContactPhone
}
if req.ContactEmail != "" {
lead.ContactEmail = req.ContactEmail
}
if req.Requirement != "" {
lead.Requirement = req.Requirement
}
if req.ExtraInfo != "" {
lead.ExtraInfo = req.ExtraInfo
}
if req.Status != "" {
lead.Status = req.Status
}
lead.FollowUpDate = req.FollowUpDate
if req.FollowUpRecord != "" {
lead.FollowUpRecord = req.FollowUpRecord
}
if req.InternalNote != "" {
lead.InternalNote = req.InternalNote
}
lead.UpdatedAt = time.Now()
if err := s.repo.Update(lead); err != nil {
return nil, i18n.NewI18nError("900004", lang, err.Error())
}
return lead, nil
}
func (s *leadService) DeleteLead(ctx context.Context, id string) error {
lang := pkgcontext.GetLanguageFromContext(ctx)
// 检查是否存在
_, err := s.repo.GetByID(id)
if err != nil {
return i18n.NewI18nError("900002", lang)
}
if err := s.repo.Delete(id); err != nil {
return i18n.NewI18nError("900004", lang, err.Error())
}
return nil
}
func (s *leadService) GetLead(ctx context.Context, id string) (*models.Lead, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
lead, err := s.repo.GetByID(id)
if err != nil {
return nil, i18n.NewI18nError("900002", lang)
}
return lead, nil
}
func (s *leadService) GetLeadList(ctx context.Context, req *models.LeadListRequest) (*models.LeadListResponse, error) {
leads, total, err := s.repo.GetList(ctx, req)
if err != nil {
lang := pkgcontext.GetLanguageFromContext(ctx)
return nil, i18n.NewI18nError("900004", lang, err.Error())
}
// 转换为响应
leadResponses := make([]*models.LeadResponse, len(leads))
for i, lead := range leads {
leadResponses[i] = lead.ToResponse()
}
page := 1
pageSize := 10
if req != nil {
if req.Page > 0 {
page = req.Page
}
if req.PageSize > 0 {
pageSize = req.PageSize
}
}
return &models.LeadListResponse{
Leads: leadResponses,
TotalCount: total,
Page: page,
PageSize: pageSize,
}, nil
}
@@ -0,0 +1,20 @@
-- 企业线索表
CREATE TABLE leads (
id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()),
lead_no VARCHAR(50) NOT NULL COMMENT '线索编号,系统生成',
company_name VARCHAR(200) NOT NULL COMMENT '企业/机构名称',
contact_name VARCHAR(100) NOT NULL COMMENT '联系人',
contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话',
contact_email VARCHAR(100) COMMENT '邮箱地址',
requirement TEXT NOT NULL COMMENT '需求描述',
extra_info TEXT COMMENT '补充信息',
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending-待联系, contacted-已联系, converted-已成交, invalid-已失效',
follow_up_date DATETIME COMMENT '跟进日期',
follow_up_record TEXT COMMENT '跟进记录',
internal_note TEXT COMMENT '内部备注',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_leads_status (status),
INDEX idx_leads_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业线索表';