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 } }