refactor(chat): 欢迎语仅保留账号群配置

This commit is contained in:
yml2213
2026-10-30 17:50:37 +08:00
parent 77b5c6123a
commit 0a85634a58
18 changed files with 29 additions and 236 deletions
@@ -78,26 +78,6 @@ func (h *Handler) AdminDeleteQuickReply(c *gin.Context) {
response.OK(c, gin.H{"deleted": true})
}
func (h *Handler) AdminGetAutoWelcome(c *gin.Context) {
message := h.service.GetAutoWelcomeMessage(c.Request.Context())
response.OK(c, gin.H{"message": message})
}
func (h *Handler) AdminUpdateAutoWelcome(c *gin.Context) {
var req struct {
Message string `json:"message" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "话术内容不能为空")
return
}
if err := h.service.UpdateAutoWelcomeMessage(c.Request.Context(), req.Message); err != nil {
writeChatError(c, err)
return
}
response.OK(c, gin.H{"updated": true})
}
func (h *Handler) AdminGetListingGroupWelcome(c *gin.Context) {
message := h.service.GetListingGroupWelcomeMessage(c.Request.Context())
response.OK(c, gin.H{"message": message})
@@ -433,12 +433,6 @@ func adminIsSupport(tx *gorm.DB, id uint64) bool {
}
return count > 0
}
func orderConversationTitle(order model.RentalOrder) string {
if order.OrderNo == "" {
return fmt.Sprintf("订单群聊 #%d", order.ID)
}
return "订单群聊 " + order.OrderNo
}
func fallbackName(participantType string, id uint64, name string) string {
if name != "" {
return name
@@ -11,8 +11,6 @@ import (
)
const (
autoWelcomeConfigKey = "chat.auto_welcome_message"
defaultAutoWelcomeMessage = "欢迎加入订单群聊!如有任何问题,请随时沟通。"
listingGroupWelcomeConfigKey = "chat.listing_group_welcome"
defaultListingGroupWelcome = "欢迎加入账号群!请号主扫描下方二维码加入企业微信群,方便客服与您及时联系。"
)
@@ -104,19 +102,6 @@ func (r *Repository) DeleteQuickReply(ctx context.Context, adminID uint64, reply
return r.db.WithContext(ctx).Where("id = ? AND admin_user_id = ?", replyID, adminID).
Delete(&model.ChatQuickReply{}).Error
}
func (r *Repository) GetAutoWelcomeMessage(ctx context.Context) string {
var cfg model.SystemConfig
if err := r.db.WithContext(ctx).Where("`key` = ?", autoWelcomeConfigKey).First(&cfg).Error; err != nil {
return defaultAutoWelcomeMessage
}
return cfg.Value
}
func (r *Repository) UpdateAutoWelcomeMessage(ctx context.Context, message string) error {
return r.db.WithContext(ctx).Model(&model.SystemConfig{}).
Where("`key` = ?", autoWelcomeConfigKey).
Update("value", message).Error
}
func (r *Repository) GetListingGroupWelcomeMessage(ctx context.Context) string {
var cfg model.SystemConfig
if err := r.db.WithContext(ctx).Where("`key` = ?", listingGroupWelcomeConfigKey).First(&cfg).Error; err != nil {
@@ -2,15 +2,12 @@ package chat
import (
"context"
"errors"
"net/http"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/chathub"
)
@@ -77,88 +74,6 @@ func NewRepository(db *gorm.DB, hub *chathub.Hub, redis *redis.Client, options .
}
return repo
}
func EnsureOrderConversation(tx *gorm.DB, order model.RentalOrder) (*model.ChatConversation, error) {
var existing model.ChatConversation
err := tx.Where("order_id = ?", order.ID).First(&existing).Error
if err == nil {
return &existing, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
var siteID *uint64
if order.DistributionSiteID != nil && *order.DistributionSiteID > 0 && order.DistributionMode != "two_party_distributor_receive" {
siteID = order.DistributionSiteID
}
now := time.Now()
conversation := model.ChatConversation{
OrderID: &order.ID,
DistributionSiteID: siteID,
Type: "order_group",
Title: orderConversationTitle(order),
Status: "active",
}
if err := tx.Create(&conversation).Error; err != nil {
return nil, err
}
participants := []model.ChatParticipant{
{
ConversationID: conversation.ID,
ParticipantType: "user",
ParticipantID: order.RenterID,
Role: "renter",
JoinedAt: now,
},
{
ConversationID: conversation.ID,
ParticipantType: "user",
ParticipantID: order.OwnerID,
Role: "owner",
JoinedAt: now,
},
}
if supportID := defaultSupportAdminID(tx); supportID > 0 {
participants = append(participants, model.ChatParticipant{
ConversationID: conversation.ID,
ParticipantType: "admin",
ParticipantID: supportID,
Role: "support",
JoinedAt: now,
})
}
for _, participant := range participants {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&participant).Error; err != nil {
return nil, err
}
}
// 获取自动话术
autoMessage := "订单已支付,群聊已创建。租客、号主和客服可在这里沟通交接与结账问题。"
var cfg model.SystemConfig
if err := tx.Where("`key` = ?", "chat.auto_welcome_message").First(&cfg).Error; err == nil && cfg.Value != "" {
autoMessage = cfg.Value
}
message := model.ChatMessage{
ConversationID: conversation.ID,
SenderType: "system",
SenderRole: "system",
ContentType: "system",
Content: autoMessage,
AttachmentURLS: emptyJSONList(),
}
if err := tx.Create(&message).Error; err != nil {
return nil, err
}
conversation.LastMessageID = &message.ID
conversation.LastMessagePreview = truncatePreview(message.Content)
conversation.LastMessageAt = &message.CreatedAt
if err := tx.Save(&conversation).Error; err != nil {
return nil, err
}
return &conversation, nil
}
func (r *Repository) NotifyNewConversation(conversationID uint64) {
if r.hub == nil {
return
-14
View File
@@ -216,20 +216,6 @@ func (s *Service) DeleteQuickReply(ctx context.Context, adminID uint64, replyID
return s.repo.DeleteQuickReply(ctx, adminID, replyID)
}
func (s *Service) GetAutoWelcomeMessage(ctx context.Context) string {
if s.repo == nil {
return ""
}
return s.repo.GetAutoWelcomeMessage(ctx)
}
func (s *Service) UpdateAutoWelcomeMessage(ctx context.Context, message string) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
return s.repo.UpdateAutoWelcomeMessage(ctx, message)
}
func (s *Service) GetListingGroupWelcomeMessage(ctx context.Context) string {
if s.repo == nil {
return ""
@@ -45,7 +45,6 @@ var defaultConfigs = []defaultConfig{
{Key: rentergrowth.ConfigKey, Value: rentergrowth.DefaultConfigValue(), Description: "租客成长等级规则(积分门槛、租金折扣、等级免押额度)"},
{Key: smsnode.ConfigKey, Value: smsnode.DefaultConfigValue(), Description: "节点短信模板配置(总开关、各业务节点模板 CODE)"},
{Key: "chat.default_support_admin_id", Value: "2", Description: "默认客服 ID(必须是启用状态的客服角色)"},
{Key: "chat.auto_welcome_message", Value: "欢迎加入订单群聊!如有任何问题,请随时沟通。", Description: "建群后自动发送的欢迎话术"},
{Key: "chat.listing_group_welcome", Value: "欢迎加入账号群!请号主扫描下方二维码加入企业微信群,方便客服与您及时联系。", Description: "发布群创建后自动发送的欢迎语"},
{Key: "chat.renter_retention_days_after_order_end", Value: "5", Description: "订单结束后租客保留在发布群的天数(3-7)"},
{Key: "integration.paddle_ocr_token", Value: "", Description: "PaddleOCR API Token,用于二维码群名自动识别"},
@@ -66,7 +65,6 @@ var defaultConfigs = []defaultConfig{
}
var adminVisibleConfigKeys = []string{
"chat.auto_welcome_message",
"chat.default_support_admin_id",
"chat.listing_group_welcome",
"chat.renter_retention_days_after_order_end",
-2
View File
@@ -896,8 +896,6 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST("/chats/quick-replies", requirePerm("chat:send"), chatHandler.AdminCreateQuickReply)
adminRoutes.PUT("/chats/quick-replies/:id", requirePerm("chat:send"), chatHandler.AdminUpdateQuickReply)
adminRoutes.DELETE("/chats/quick-replies/:id", requirePerm("chat:send"), chatHandler.AdminDeleteQuickReply)
adminRoutes.GET("/chats/auto-welcome", requirePerm("system_config:view"), chatHandler.AdminGetAutoWelcome)
adminRoutes.PUT("/chats/auto-welcome", requirePerm("system_config:update"), chatHandler.AdminUpdateAutoWelcome)
adminRoutes.GET("/chats/listing-group-welcome", requirePerm("system_config:view"), chatHandler.AdminGetListingGroupWelcome)
adminRoutes.PUT("/chats/listing-group-welcome", requirePerm("system_config:update"), chatHandler.AdminUpdateListingGroupWelcome)
+1 -2
View File
@@ -666,8 +666,7 @@ WHERE r.code = 'finance' AND p.code IN (
INSERT INTO system_configs (`key`, `value`, description) VALUES
('listing.review_required', 'true', '商品是否需要审核'),
('order.pending_payment_timeout_minutes', '15', '待支付超时时间(分钟)'),
('chat.default_support_admin_id', '2', '默认客服 ID(必须是启用状态的客服角色)'),
('chat.auto_welcome_message', '欢迎加入订单群聊!如有任何问题,请随时沟通。', '建群后自动发送的欢迎话术')
('chat.default_support_admin_id', '2', '默认客服 ID(必须是启用状态的客服角色)')
ON DUPLICATE KEY UPDATE
`value` = VALUES(`value`),
description = VALUES(description);
@@ -82,5 +82,5 @@
- 所有写操作(注册、下单、支付、退款、分类/商品增删改、备份计划保存)未执行真实写入;支付与短信/实名仍为空配置。
- 摸红分类状态值后端为 `enabled/disabled`,后台分类页下拉目前使用 `active/disabled`,需在真实数据联调时统一(已记录待修)。
- 商品合集 `/listings/collections` 新旧后端均无收藏表/接口,旧站该路由实为「商品合集」,本轮未复刻。
- 二维码池已支持批量导入(单次 ≤20 张)与 OCR 识别群名;欢迎语配置(订单群 / 账号群)已落地 `/console-jy8k3m/chat-welcome`。OCR 依赖系统配置中的 PaddleOCR Token,未配置时页面会提示失败,需真实 Token 验证识别效果。
- 二维码池已支持批量导入(单次 ≤20 张)与 OCR 识别群名;欢迎语配置仅保留账号群(`/console-jy8k3m/chat-welcome` → `/admin/chats/listing-group-welcome`),订单群欢迎语相关前后端代码已移除。OCR 依赖系统配置中的 PaddleOCR Token,未配置时页面会提示失败,需真实 Token 验证识别效果。
- 主入口已接近 500 kB,后续新增页面建议按路由懒加载并在需要时再评估拆包。
+1 -1
View File
@@ -414,7 +414,7 @@ hfb_jysh/
- 已落地:`/wallet`(余额 + 流水)、`/wallet/payment-accounts`、`/wallet/withdrawal`、移动端 `/m/wallet/withdrawal`、卖家 `/seller/earnings`、`/seller/handoffs`、`/seller/pickups`。
- 申诉仲裁、手工订单、提号、客服群、快捷回复。
- 已落地:客服群组(`/chat-support-groups`,权限 `chat:manage`),角色权限矩阵(`/roles`,权限 `role:manage`),手工录单(`/manual-entries`,权限 `cs_manual_entry:*`),快捷回复(`/chat-quick-replies`,权限 `chat:view` / `chat:send`),线下提号(`/pickup`、`/pickup/:id`,权限 `order:pickup`,口径统一为「线下提号」)。
- 已落地:欢迎语配置 `/chat-welcome`(订单群 + 账号群),二维码池批量导入与 OCR 识别群名。
- 已落地:欢迎语配置 `/chat-welcome`(仅账号群),二维码池批量导入与 OCR 识别群名。
- 分销站点、规则、订单、结算、二维码池、分销用户、公告。
- 公告、邀请返利、推送、追踪链接。
- 已落地:公告管理、邀请管理(返利配置/邀请关系/返利流水)、推广追踪(`/tracking-links`)、通知中心(`/notifications`)、推送通知(`/push`)。
@@ -3,12 +3,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Alert, App, Button, Card, Form, Input, Space, Tag, Typography } from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import {
fetchChatAutoWelcome,
fetchListingGroupWelcome,
updateChatAutoWelcome,
updateListingGroupWelcome,
} from '@/features/admin/api'
import { fetchListingGroupWelcome, updateListingGroupWelcome } from '@/features/admin/api'
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
import { useAdminSessionStore } from '@/stores/adminSession'
@@ -20,30 +15,20 @@ function errorText(error: unknown) {
export function AdminChatWelcomePage() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [autoForm] = Form.useForm<{ message: string }>()
const [groupForm] = Form.useForm<{ message: string }>()
const [saving, setSaving] = useState<'auto' | 'group' | null>(null)
const [saving, setSaving] = useState(false)
const permissions = useAdminSessionStore(state => state.admin?.permissions)
const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view'))
const canManage = Boolean(
permissions?.includes('*') || permissions?.includes('system_config:update')
)
const autoQuery = useQuery({
queryKey: ['chat-auto-welcome'],
queryFn: fetchChatAutoWelcome,
enabled: canView,
})
const groupQuery = useQuery({
queryKey: ['chat-listing-group-welcome'],
queryFn: fetchListingGroupWelcome,
enabled: canView,
})
useEffect(() => {
if (autoQuery.data != null) autoForm.setFieldsValue({ message: autoQuery.data })
}, [autoQuery.data, autoForm])
useEffect(() => {
if (groupQuery.data != null) groupForm.setFieldsValue({ message: groupQuery.data })
}, [groupQuery.data, groupForm])
@@ -57,23 +42,17 @@ export function AdminChatWelcomePage() {
)
}
const save = async (kind: 'auto' | 'group') => {
const form = kind === 'auto' ? autoForm : groupForm
const values = await form.validateFields()
setSaving(kind)
const save = async () => {
const values = await groupForm.validateFields()
setSaving(true)
try {
if (kind === 'auto') {
await updateChatAutoWelcome(values.message.trim())
await queryClient.invalidateQueries({ queryKey: ['chat-auto-welcome'] })
} else {
await updateListingGroupWelcome(values.message.trim())
await queryClient.invalidateQueries({ queryKey: ['chat-listing-group-welcome'] })
}
await updateListingGroupWelcome(values.message.trim())
await queryClient.invalidateQueries({ queryKey: ['chat-listing-group-welcome'] })
message.success('欢迎语已保存')
} catch (error) {
message.error(errorText(error))
} finally {
setSaving(null)
setSaving(false)
}
}
@@ -82,49 +61,24 @@ export function AdminChatWelcomePage() {
<AdminPageHeader
eyebrow="JIEYOU OPS · 客服"
title="欢迎语配置"
description="维护订单群与账号群的自动欢迎语,客服建群后自动发送。"
description="维护账号群的自动欢迎语,客服建群后自动发送。"
meta={
<Space size={8} wrap>
<Tag color="geekblue">订单群</Tag>
<Tag color="purple">账号群</Tag>
</Space>
}
extra={
<Button
icon={<ReloadOutlined />}
loading={autoQuery.isFetching || groupQuery.isFetching}
onClick={() => {
void autoQuery.refetch()
void groupQuery.refetch()
}}
loading={groupQuery.isFetching}
onClick={() => void groupQuery.refetch()}
>
刷新
</Button>
}
/>
<Card title="订单群欢迎语" className="admin-table-card">
<Form form={autoForm} layout="vertical" requiredMark={false}>
<Form.Item
name="message"
label="欢迎语"
rules={[{ required: true, message: '请输入欢迎语' }, { max: 500 }]}
extra="订单群建立后自动发送给租客与号主"
>
<Input.TextArea rows={4} maxLength={500} showCount disabled={!canManage} />
</Form.Item>
<Button
type="primary"
loading={saving === 'auto'}
disabled={!canManage}
onClick={() => void save('auto')}
>
保存订单群欢迎语
</Button>
</Form>
</Card>
<Card title="账号群欢迎语" className="admin-table-card" style={{ marginTop: 16 }}>
<Card title="账号群欢迎语" className="admin-table-card">
<Form form={groupForm} layout="vertical" requiredMark={false}>
<Form.Item
name="message"
@@ -136,9 +90,9 @@ export function AdminChatWelcomePage() {
</Form.Item>
<Button
type="primary"
loading={saving === 'group'}
loading={saving}
disabled={!canManage}
onClick={() => void save('group')}
onClick={() => void save()}
>
保存账号群欢迎语
</Button>
@@ -146,7 +100,7 @@ export function AdminChatWelcomePage() {
</Card>
<Typography.Paragraph type="secondary" style={{ marginTop: 12 }}>
欢迎语保存在系统配置中,修改后对新建立的群生效。
欢迎语保存在系统配置中,修改后对新建立的账号群生效。
</Typography.Paragraph>
</div>
)
-15
View File
@@ -2027,21 +2027,6 @@ export async function updateCrashConfig(payload: Partial<AdminCrashConfig>) {
return data.data
}
export async function fetchChatAutoWelcome() {
const { data } = await adminClient.get<ApiResponse<{ message: string }>>(
'/admin/chats/auto-welcome'
)
return data.data.message ?? ''
}
export async function updateChatAutoWelcome(message: string) {
const { data } = await adminClient.put<ApiResponse<{ message: string }>>(
'/admin/chats/auto-welcome',
{ message }
)
return data.data
}
export async function fetchListingGroupWelcome() {
const { data } = await adminClient.get<ApiResponse<{ message: string }>>(
'/admin/chats/listing-group-welcome'
+1 -1
View File
@@ -210,7 +210,7 @@ export function ChatPage() {
<BrandLogo height={48} variant="seal" />
<Typography.Title level={3}>登录后进入会话</Typography.Title>
<Typography.Paragraph type="secondary">
订单群聊与客服会话只对当前账号开放。
账号群聊与客服会话只对当前账号开放。
</Typography.Paragraph>
<Button type="primary" onClick={() => navigate(`/login?redirect=/messages/${id}`)}>
去登录
@@ -31,8 +31,7 @@ import { formatRelativeTime } from '@/shared/utils/format'
function conversationMeta(type: string) {
if (type === 'general_support') return { label: '平台客服', color: 'gold' }
if (type === 'order_group') return { label: '订单群聊', color: 'green' }
if (type === 'listing_group') return { label: '商品咨询', color: 'blue' }
if (type === 'listing_group') return { label: '账号群', color: 'blue' }
return { label: '交易会话', color: 'default' }
}
@@ -154,7 +153,7 @@ export function MessagesPage() {
<BrandLogo height={48} variant="seal" />
<Typography.Title level={3}>登录后查看消息</Typography.Title>
<Typography.Paragraph type="secondary">
订单群聊、客服回复与系统通知只对当前账号开放。
账号群聊、客服回复与系统通知只对当前账号开放。
</Typography.Paragraph>
<Button
type="primary"
@@ -176,7 +175,7 @@ export function MessagesPage() {
消息中心
</Typography.Title>
<Typography.Paragraph type="secondary">
订单群聊会随下单自动建立,交接、结账与申诉都建议在群内留痕。
账号群聊随商品发布建立,交接、结账与申诉都建议在群内留痕。
</Typography.Paragraph>
</div>
<Space wrap>
@@ -140,7 +140,7 @@ export function MobileMessagesPage() {
<div className="mobile-orders__login">
<BrandLogo height={40} variant="seal" />
<h2>先登录,再看消息</h2>
<p>订单群聊与客服会话只对交易双方开放。</p>
<p>账号群聊与客服会话只对交易双方开放。</p>
<Button block color="primary" onClick={() => navigate('/m/login?redirect=/m/messages')}>
去登录
</Button>
@@ -185,7 +185,7 @@ export function MobileMessagesPage() {
) : chatItems.length === 0 ? (
<div className="mobile-messages__empty">
<Empty description="还没有会话" />
<span>下单后会自动创建订单群聊,也可以先联系客服。</span>
<span>商品发布后会自动创建账号群,也可以先联系客服。</span>
</div>
) : (
<div className="mobile-message-list">
@@ -54,7 +54,7 @@ export function MobileOrderDetailPage() {
} catch (error) {
Toast.show({
icon: 'fail',
content: error instanceof Error ? error.message : '订单群聊暂时无法打开',
content: error instanceof Error ? error.message : '账号群暂时无法打开',
})
} finally {
setOpeningChat(false)
@@ -254,7 +254,7 @@ export function MobileOrderDetailPage() {
loading={openingChat}
onClick={() => void openOrderChat()}
>
进入订单群聊
进入账号群
</Button>
<MobileOrderActionPanel
order={order}
@@ -57,7 +57,7 @@ export function OrderDetailPage() {
const conversation = await fetchOrderConversation(order.id)
navigate(`/messages/${conversation.id}`)
} catch (error) {
message.error(error instanceof Error ? error.message : '订单群聊暂时无法打开')
message.error(error instanceof Error ? error.message : '账号群暂时无法打开')
} finally {
setOpeningChat(false)
}
@@ -321,7 +321,7 @@ export function OrderDetailPage() {
className="pc-order-chat-button"
onClick={() => void openOrderChat()}
>
进入订单群聊
进入账号群
</Button>
<OrderActionPanel
order={order}
+1 -1
View File
@@ -245,7 +245,7 @@ const sectionMeta: Record<string, { title: string; description: string }> = {
},
'/chat-welcome': {
title: '欢迎语配置',
description: '维护订单群与账号群的自动欢迎语。',
description: '维护账号群的自动欢迎语。',
},
'/qrcode-pool': {
title: '二维码池',