feat: 增加线索汇总

This commit is contained in:
wood911010 committed 2026-02-02 14:37:10 +08:00
1 parent f2bb2ef952
commit 8b758cc40c
7 files changed
+153 -39

No files matched your search

@@ -0,0 +1,33 @@
# 管理端企业线索汇总接口设计
## 接口路径
`GET /api/leads/summary`
## 请求
不需要任何参数。
## 鉴权
需要管理端权限(与线索列表/详情接口一致)。
## 响应示例
```json
{
"code": "000000",
"message": "success",
"data": {
"total_count": 1200,
"pending_count": 300,
"following_count": 520,
"converted_count": 260,
"lost_count": 120
},
"timestamp": 1738454400
}
```
## 字段说明
- total_count:线索总数
- pending_count:待跟进数量
- following_count:已跟进数量
- converted_count:已成交数量
- lost_count:已流失数量
@@ -104,6 +104,35 @@ func (h *LeadHandler) GetLeads(c *gin.Context) {
})
}
// GetLeadSummary 获取线索汇总
// @Summary 获取线索汇总
// @Description 管理员获取企业线索汇总统计信息
// @Tags 企业线索
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.APIResponse{data=models.LeadSummaryResponse} "成功"
// @Failure 401 {object} models.ErrorResponse "未认证"
// @Failure 403 {object} models.ErrorResponse "权限不足"
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
// @Router /api/leads/summary [get]
func (h *LeadHandler) GetLeadSummary(c *gin.Context) {
lang := middleware.GetLanguage(c)
summary, err := h.leadService.GetLeadSummary(c.Request.Context())
if err != nil {
handleI18nError(c, err, lang)
return
}
c.JSON(http.StatusOK, models.APIResponse{
Code: "000000",
Message: i18n.GetI18nErrorMessage("000000", lang),
Data: summary,
Timestamp: getCurrentTimestamp(),
})
}
// GetLead 获取线索详情
// @Summary 获取线索详情
// @Description 根据ID获取线索详细信息
+1
View File
@@ -213,6 +213,7 @@ func SetupRouter() *gin.Engine {
// 线索管理(管理员)
auth.GET("/leads", leadHandler.GetLeads)
auth.GET("/leads/summary", leadHandler.GetLeadSummary)
auth.GET("/leads/:id", leadHandler.GetLead)
auth.PUT("/leads/:id", leadHandler.UpdateLead)
auth.PUT("/leads/:id/status", leadHandler.UpdateLeadStatus)
+39 -30
View File
@@ -97,44 +97,44 @@ func (l *Lead) ToResponse() *LeadResponse {
// 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"`
ID string `json:"id"` // 线索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"` // 跟进日期(RFC3339,如 2026-02-05T10:00:00Z)
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"`
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"`
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"` // 跟进日期(RFC3339,如 2026-02-05T10:00:00Z)
FollowUpRecord string `json:"follow_up_record"` // 跟进记录
InternalNote string `json:"internal_note"` // 内部备注
}
// LeadListRequest 线索列表请求
@@ -152,3 +152,12 @@ type LeadListResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// LeadSummaryResponse 线索汇总响应
type LeadSummaryResponse struct {
TotalCount int64 `json:"total_count"` // 线索总数
PendingCount int64 `json:"pending_count"` // 待跟进数量
ContactedCount int64 `json:"contacted_count"` // 已跟进数量
ConvertedCount int64 `json:"converted_count"` // 已成交数量
InvalidCount int64 `json:"invalid_count"` // 已流失数量
}
@@ -14,6 +14,7 @@ type LeadRepository interface {
Delete(id string) error
GetByID(id string) (*models.Lead, error)
GetList(ctx context.Context, req *models.LeadListRequest) ([]*models.Lead, int64, error)
GetSummary(ctx context.Context) (*models.LeadSummaryResponse, error)
}
type leadRepository struct {
@@ -95,3 +96,36 @@ func (r *leadRepository) GetList(ctx context.Context, req *models.LeadListReques
return leads, total, nil
}
func (r *leadRepository) GetSummary(ctx context.Context) (*models.LeadSummaryResponse, error) {
var row struct {
TotalCount int64 `gorm:"column:total_count"`
PendingCount int64 `gorm:"column:pending_count"`
ContactedCount int64 `gorm:"column:contacted_count"`
ConvertedCount int64 `gorm:"column:converted_count"`
InvalidCount int64 `gorm:"column:invalid_count"`
}
err := r.db.WithContext(ctx).Model(&models.Lead{}).Select(
"COUNT(*) as total_count, " +
"SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as pending_count, " +
"SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as contacted_count, " +
"SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as converted_count, " +
"SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as invalid_count",
string(models.LeadStatusPending),
string(models.LeadStatusContacted),
string(models.LeadStatusConverted),
string(models.LeadStatusInvalid),
).Scan(&row).Error
if err != nil {
return nil, err
}
return &models.LeadSummaryResponse{
TotalCount: row.TotalCount,
PendingCount: row.PendingCount,
ContactedCount: row.ContactedCount,
ConvertedCount: row.ConvertedCount,
InvalidCount: row.InvalidCount,
}, nil
}
@@ -57,7 +57,7 @@ func (s *adminInvoiceService) RejectInvoice(ctx context.Context, invoiceID strin
// 验证状态
if invoice.Status != models.InvoiceStatusPending {
return nil, i18n.NewI18nError("700004", lang)
return nil, i18n.NewI18nError("900001", lang)
}
now := time.Now()
@@ -84,15 +84,11 @@ func (s *adminInvoiceService) IssueInvoice(ctx context.Context, invoiceID string
return nil, i18n.NewI18nError("700001", lang)
}
// 验证状态:允许从pending或rejected状态开票
if invoice.Status != models.InvoiceStatusPending && invoice.Status != models.InvoiceStatusRejected {
return nil, i18n.NewI18nError("700004", lang)
}
prevStatus := invoice.Status
// 解析开票时间
issuedAt, err := time.Parse(time.RFC3339, req.IssuedAt)
if err != nil {
return nil, i18n.NewI18nError("700004", lang)
return nil, i18n.NewI18nError("900001", lang)
}
now := time.Now()
@@ -103,8 +99,8 @@ func (s *adminInvoiceService) IssueInvoice(ctx context.Context, invoiceID string
invoice.UploadedBy = &adminID
invoice.UpdatedAt = now
// 清除驳回信息(如果是从rejected状态开票)
if invoice.Status == models.InvoiceStatusRejected {
// Clear reject info when issuing from rejected status.
if prevStatus == models.InvoiceStatusRejected {
invoice.RejectReason = nil
invoice.Suggestion = nil
invoice.RejectedAt = nil
+12
View File
@@ -18,6 +18,7 @@ type LeadService interface {
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)
GetLeadSummary(ctx context.Context) (*models.LeadSummaryResponse, error)
}
type leadService struct {
@@ -157,3 +158,14 @@ func (s *leadService) GetLeadList(ctx context.Context, req *models.LeadListReque
PageSize: pageSize,
}, nil
}
func (s *leadService) GetLeadSummary(ctx context.Context) (*models.LeadSummaryResponse, error) {
lang := pkgcontext.GetLanguageFromContext(ctx)
summary, err := s.repo.GetSummary(ctx)
if err != nil {
return nil, i18n.NewI18nError("900004", lang, err.Error())
}
return summary, nil
}