feat: 增加线索汇总
This commit is contained in:
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获取线索详细信息
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user