328 lines
10 KiB
Go
328 lines
10 KiB
Go
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
|
||
}
|
||
}
|