ai_scheduler/internal/services/advice/customer_new.go

328 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package advice
import (
"ai_scheduler/internal/biz"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/wx"
"fmt"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
)
// CustomerService 微信好友数据服务
type CustomerService struct {
customerBiz *biz.AdviceCustomerBiz
labelBiz *biz.AdviceLabelBiz
projectBiz *biz.AdviceProjectBiz
advicerBiz *biz.AdviceAdvicerBiz
}
func NewCustomerService(customerBiz *biz.AdviceCustomerBiz, labelBiz *biz.AdviceLabelBiz, projectBiz *biz.AdviceProjectBiz, advicerBiz *biz.AdviceAdvicerBiz) *CustomerService {
return &CustomerService{customerBiz: customerBiz, labelBiz: labelBiz, projectBiz: projectBiz, advicerBiz: advicerBiz}
}
// Add 新增单条好友
func (s *CustomerService) Add(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerAddReq{}
if err := c.BodyParser(req); err != nil {
return err
}
err := s.customerBiz.Add(c.UserContext(), req)
return pkg.HandleResponse(c, nil, err)
}
// List 好友列表查询
func (s *CustomerService) List(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerListReq{}
if err := c.BodyParser(req); err != nil {
return err
}
list, err := s.customerBiz.List(c.UserContext(), req)
return pkg.HandleResponse(c, list, err)
}
// Del 删除好友
func (s *CustomerService) Del(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerDelReq{}
if err := c.BodyParser(req); err != nil {
return err
}
err := s.customerBiz.Del(c.UserContext(), req)
return pkg.HandleResponse(c, nil, err)
}
// Diff 好友差集查询(同时返回标签列表)
func (s *CustomerService) Diff(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerDiffReq{}
if err := c.BodyParser(req); err != nil {
return err
}
diff, err := s.customerBiz.Diff(c.UserContext(), req)
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
// 顺便查询标签列表
if req.SelfWxid != "" {
labels, _ := s.labelBiz.List(c.UserContext(), &entitys.AdvicerLabelListReq{SelfWxid: req.SelfWxid})
if labels != nil {
diff.Labels = labels
}
}
return pkg.HandleResponse(c, diff, nil)
}
// BatchAdd 批量新增好友
func (s *CustomerService) BatchAdd(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerBatchAddReq{}
if err := c.BodyParser(req); err != nil {
return err
}
err := s.customerBiz.BatchAdd(c.UserContext(), req)
return pkg.HandleResponse(c, nil, err)
}
// BatchUpdate 批量更新已有好友信息
func (s *CustomerService) BatchUpdate(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerBatchAddReq{}
if err := c.BodyParser(req); err != nil {
return err
}
err := s.customerBiz.BatchUpdate(c.UserContext(), req)
return pkg.HandleResponse(c, nil, err)
}
// UpdateLabels 更新好友标签列表(wx API + DB 一次完成)
func (s *CustomerService) UpdateLabels(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerUpdateLabelsReq{}
if err := c.BodyParser(req); err != nil {
return err
}
// 1. 查询项目的 wx_token
project, err := s.projectBiz.BaseInfo(req.ProjectId)
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
if project.WxToken == "" {
return pkg.HandleResponse(c, nil, fiber.NewError(fiber.StatusBadRequest, "项目未配置微信接入 Token"))
}
// 2. 调用微信 API 修改好友标签(全量替换)
var wxIds []string
if req.UserName != "" {
wxIds = strings.Split(req.UserName, ",")
}
wxReq := wx.ModifyMemberListReq{
AppId: req.AppId,
LabelIds: req.LabelList,
WxIds: wxIds,
}
var wxRes wx.ModifyMemberListResData
if err := wx.Request(c.UserContext(), project.WxToken, wx.ModifyMemberList, wxReq, &wxRes); err != nil {
return pkg.HandleResponse(c, nil, fmt.Errorf("微信标签修改失败: %w", err))
}
// 3. 更新 DB
if err := s.customerBiz.UpdateLabels(c.UserContext(), &entitys.AdvicerCustomerUpdateLabelsReq{
SelfWxid: req.SelfWxid,
UserName: req.UserName,
LabelList: req.LabelList,
}); err != nil {
return pkg.HandleResponse(c, nil, fmt.Errorf("数据库更新失败: %w", err))
}
return pkg.HandleResponse(c, nil, nil)
}
// HostedList 查询项目下所有托管客户(在托管标签分组内的客户)
func (s *CustomerService) HostedList(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerHostedListReq{}
if err := c.BodyParser(req); err != nil {
return err
}
ctx := c.UserContext()
// 1. 查项目下所有销售
advicers, err := s.advicerBiz.List(ctx, &entitys.AdvicerListReq{ProjectId: req.ProjectId})
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
// 构建 selfWxid -> advicerName / wx_device_id / advicer_id 映射,收集所有 selfWxid
// 关联逻辑:advicer.wx_id = ai_advice_customer.self_wxid
advicerMap := make(map[string]string)
wxDeviceMap := make(map[string]string)
advicerIdMap := make(map[string]int32)
var selfWxids []string
for _, adv := range advicers {
wxId, _ := adv["wx_id"].(string)
name, _ := adv["name"].(string)
deviceId, _ := adv["wx_device_id"].(string)
if wxId != "" {
advicerMap[wxId] = name
wxDeviceMap[wxId] = deviceId
advicerIdMap[wxId] = toAdvicerId(adv["advicer_id"])
selfWxids = append(selfWxids, wxId)
}
}
if len(selfWxids) == 0 {
return pkg.HandleResponse(c, []entitys.HostedCustomerItem{}, nil)
}
// 2. 获取每个销售的托管标签 ID,构建 selfWxid -> hostingLabelIds 和 labelId -> labelName 映射
hostingLabelsMap := make(map[string]map[int]bool)
hostingLabelIdsMap := make(map[string][]int)
labelNameMap := make(map[int]string)
for _, selfWxid := range selfWxids {
hostIds, _ := s.labelBiz.GetHostingLabelIds(ctx, selfWxid)
if len(hostIds) == 0 {
continue
}
hostingLabelsMap[selfWxid] = make(map[int]bool)
for _, id := range hostIds {
hostingLabelsMap[selfWxid][id] = true
}
hostingLabelIdsMap[selfWxid] = hostIds
labels, _ := s.labelBiz.List(ctx, &entitys.AdvicerLabelListReq{SelfWxid: selfWxid})
for _, l := range labels {
labelNameMap[int(l.LabelId)] = l.LabelName
}
}
if len(hostingLabelsMap) == 0 {
return pkg.HandleResponse(c, []entitys.HostedCustomerItem{}, nil)
}
// 3. 在 SQL 层用 FIND_IN_SET 直接查出命中各销售托管标签的客户(不再拉全量到内存过滤)
hostedCustomers, err := s.customerBiz.FindBySelfWxidHostingLabels(ctx, hostingLabelIdsMap)
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
result := make([]entitys.HostedCustomerItem, 0, len(hostedCustomers))
for _, cust := range hostedCustomers {
hostLabels, ok := hostingLabelsMap[cust.SelfWxid]
if !ok {
continue
}
// 托管标签列只展示命中该销售托管开关的标签名
var matchedNames []string
for _, ls := range strings.Split(cust.LabelList, ",") {
id, e := strconv.Atoi(strings.TrimSpace(ls))
if e == nil && hostLabels[id] {
if name, exists := labelNameMap[id]; exists {
matchedNames = append(matchedNames, name)
}
}
}
if len(matchedNames) == 0 {
continue
}
result = append(result, entitys.HostedCustomerItem{
AiAdviceCustomer: cust,
LabelNames: strings.Join(matchedNames, ","),
AdvicerName: advicerMap[cust.SelfWxid],
AdvicerWxDeviceId: wxDeviceMap[cust.SelfWxid],
AdvicerId: advicerIdMap[cust.SelfWxid],
})
}
return pkg.HandleResponse(c, result, nil)
}
// UpdateNote 更新客户备注(写入 description 字段,不会被微信同步覆盖)
func (s *CustomerService) UpdateNote(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerUpdateNoteReq{}
if err := c.BodyParser(req); err != nil {
return err
}
err := s.customerBiz.UpdateNote(c.UserContext(), req)
return pkg.HandleResponse(c, nil, err)
}
// RemoveLabel 将客户从托管标签中移除(微信 API 全量替换 + DB 同步)
// 移除后客户不再命中托管标签交集,即退出托管自动回复范围
func (s *CustomerService) RemoveLabel(c *fiber.Ctx) error {
req := &entitys.AdvicerCustomerRemoveLabelReq{}
if err := c.BodyParser(req); err != nil {
return err
}
ctx := c.UserContext()
// 1. 查客户记录
cust, err := s.customerBiz.FindByCustomerId(ctx, req.CustomerId)
if err != nil {
return pkg.HandleResponse(c, nil, fiber.NewError(fiber.StatusBadRequest, "客户记录不存在"))
}
// 2. 取该销售的托管标签 ID,从客户 label_list 中剔除
hostIds, err := s.labelBiz.GetHostingLabelIds(ctx, cust.SelfWxid)
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
hostSet := make(map[int]bool, len(hostIds))
for _, id := range hostIds {
hostSet[id] = true
}
remain := make([]string, 0)
for _, ls := range strings.Split(cust.LabelList, ",") {
s2 := strings.TrimSpace(ls)
if s2 == "" {
continue
}
if id, e := strconv.Atoi(s2); e == nil && hostSet[id] {
continue
}
remain = append(remain, s2)
}
newLabelList := strings.Join(remain, ",")
// 3. 取销售的微信设备 appId 与项目 wx_token,调微信 API 全量替换好友标签
adv, err := s.advicerBiz.FindByWxId(ctx, cust.SelfWxid)
if err != nil || adv.WxDeviceId == "" {
return pkg.HandleResponse(c, nil, fiber.NewError(fiber.StatusBadRequest, "未找到该客户归属销售的微信设备"))
}
project, err := s.projectBiz.BaseInfo(req.ProjectId)
if err != nil {
return pkg.HandleResponse(c, nil, err)
}
if project.WxToken == "" {
return pkg.HandleResponse(c, nil, fiber.NewError(fiber.StatusBadRequest, "项目未配置微信接入 Token"))
}
wxReq := wx.ModifyMemberListReq{
AppId: adv.WxDeviceId,
LabelIds: newLabelList,
WxIds: []string{cust.UserName},
}
var wxRes wx.ModifyMemberListResData
if err := wx.Request(ctx, project.WxToken, wx.ModifyMemberList, wxReq, &wxRes); err != nil {
return pkg.HandleResponse(c, nil, fmt.Errorf("微信标签修改失败: %w", err))
}
// 4. 同步 DB
if err := s.customerBiz.UpdateLabelListById(ctx, req.CustomerId, newLabelList); err != nil {
return pkg.HandleResponse(c, nil, fmt.Errorf("数据库更新失败: %w", err))
}
return pkg.HandleResponse(c, fiber.Map{"label_list": newLabelList}, nil)
}
// toAdvicerId 将 GetRange 返回 map 中的 advicer_id(主键,驱动可能扫为 int64/int32/int/float64)统一转为 int32。
func toAdvicerId(v interface{}) int32 {
switch n := v.(type) {
case int32:
return n
case int64:
return int32(n)
case int:
return int32(n)
case uint32:
return int32(n)
case float64:
return int32(n)
default:
return 0
}
}