浏览代码

fix(credit-balance-ui): 优化用户未开通额度账户的交互体验

- 修改额度查询API错误处理,区分404(用户额度账户不存在)和其他错误
- 添加hasCreditAccount状态判断逻辑
- 当用户未开通额度账户时,显示友好的提示信息和开通按钮
- 复用现有额度设置表单,提供开通额度功能
- 开通成功后自动刷新数据,显示正常的额度管理界面
- 更新集成测试,添加用户未开通额度账户场景测试
- 修复类型检查错误,移除未使用的balance变量

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 6 天之前
父节点
当前提交
86c00a66d1

+ 49 - 2
docs/stories/004.002.credit-balance-management-ui-mt.story.md

@@ -74,6 +74,14 @@ Ready for Review
   - [x] 配置包导出,确保可以正确导入和使用
   - [x] 更新根package.json的workspace配置
 
+- [x] **优化用户未开通额度账户的交互体验** (AC: 1, 2, 5, 7)
+  - [x] 修改额度查询API错误处理,区分404(用户额度账户不存在)和其他错误
+  - [x] 添加用户额度账户状态判断逻辑
+  - [x] 当用户未开通额度账户时,显示友好的提示信息和开通按钮
+  - [x] 复用现有额度设置表单,提供开通额度功能
+  - [x] 开通成功后自动刷新数据,显示正常的额度管理界面
+  - [x] 更新集成测试,覆盖用户未开通额度账户的场景
+
 ## Dev Notes
 
 ### 技术栈信息 [Source: architecture/tech-stack.md]
@@ -346,6 +354,26 @@ CREATE TABLE credit_balance_log_mt (
      4. 更新集成测试mock配置,同时mock `creditBalanceClient`和`creditBalanceClientManager`
    - **测试验证**: 所有6个集成测试通过,类型检查通过
    - **架构一致性**: 与用户管理UI包保持一致的客户端使用模式,符合项目RPC客户端架构最佳实践
+10. **用户未开通额度账户交互优化**: 优化用户未开通额度账户时的交互体验
+    - **问题发现**: 当用户没有开通额度账户时,API返回404错误,页面显示错误提示,体验不佳
+    - **问题分析**: 404(用户额度账户不存在)是正常情况,不应该显示错误提示
+    - **解决方案设计**:
+      1. 修改额度查询API错误处理,区分404和其他错误
+      2. 添加`hasCreditAccount`状态,表示用户是否已开通额度账户
+      3. 当用户未开通额度账户时,显示友好的提示信息和开通按钮
+      4. 复用现有额度设置表单,提供开通额度功能
+      5. 开通成功后自动刷新数据,显示正常的额度管理界面
+      6. 更新集成测试,覆盖用户未开通额度账户的场景
+    - **实现细节**:
+      1. 修改CreditBalanceDialog.tsx中的额度查询逻辑,404返回null而不是抛出错误
+      2. 添加`hasCreditAccount`状态变量,基于`balanceData !== null && balanceData !== undefined`判断
+      3. 在用户信息卡片中显示友好的提示信息
+      4. 在额度概览标签页中添加开通额度表单(复用设置额度表单)
+      5. 当用户没有额度账户时,额度操作和变更记录标签页显示提示信息
+      6. 额度变更记录查询只在用户有额度账户时启用
+      7. 开通成功后显示"信用额度开通成功"的toast提示
+    - **测试更新**: 添加新的集成测试用例"应该处理用户未开通额度账户的场景"
+    - **验证结果**: 所有7个集成测试通过,类型检查通过
 
 ### Completion Notes List
 1. ✅ **包结构创建**: 完成credit-balance-management-ui-mt包的所有配置文件
@@ -401,6 +429,14 @@ CREATE TABLE credit_balance_log_mt (
     - 修复CreditBalanceDialog.tsx:类型推断使用`creditBalanceClient`,实际调用使用`creditBalanceClientManager.get()`
     - 更新集成测试mock配置,支持客户端管理器模式
     - 所有测试通过,类型检查通过,符合RPC客户端架构最佳实践
+15. ✅ **用户未开通额度账户交互优化**: 优化用户未开通额度账户时的交互体验
+    - 修改额度查询API错误处理,区分404(用户额度账户不存在)和其他错误
+    - 添加`hasCreditAccount`状态判断逻辑
+    - 当用户未开通额度账户时,显示友好的提示信息和开通按钮
+    - 复用现有额度设置表单,提供开通额度功能
+    - 开通成功后自动刷新数据,显示正常的额度管理界面
+    - 更新集成测试,添加"应该处理用户未开通额度账户的场景"测试用例
+    - 验证结果:所有7个集成测试通过,类型检查通过
 
 ### File List
 **已创建/修改的文件**:
@@ -408,13 +444,13 @@ CREATE TABLE credit_balance_log_mt (
 2. `packages/credit-balance-management-ui-mt/package.json` - 包配置和依赖
 3. `packages/credit-balance-management-ui-mt/src/api/creditBalanceClient.ts` - API客户端
 4. `packages/credit-balance-management-ui-mt/src/api/index.ts` - API导出文件
-5. `packages/credit-balance-management-ui-mt/src/components/CreditBalanceDialog.tsx` - 主对话框组件(已优化:移除按钮多余的onClick处理程序,修复RPC客户端管理器使用)
+5. `packages/credit-balance-management-ui-mt/src/components/CreditBalanceDialog.tsx` - 主对话框组件(已优化:移除按钮多余的onClick处理程序,修复RPC客户端管理器使用,优化用户未开通额度账户的交互体验
 6. `packages/credit-balance-management-ui-mt/src/components/index.ts` - 组件导出文件
 7. `packages/credit-balance-management-ui-mt/src/hooks/index.ts` - Hooks导出文件
 8. `packages/credit-balance-management-ui-mt/src/types/creditBalance.ts` - 类型定义
 9. `packages/credit-balance-management-ui-mt/src/types/index.ts` - 类型导出文件
 10. `packages/credit-balance-management-ui-mt/src/index.ts` - 主导出文件
-11. `packages/credit-balance-management-ui-mt/tests/integration/creditBalanceDialog.integration.test.tsx` - 集成测试(已修复表单验证测试,清理调试信息,更新客户端mock配置)
+11. `packages/credit-balance-management-ui-mt/tests/integration/creditBalanceDialog.integration.test.tsx` - 集成测试(已修复表单验证测试,清理调试信息,更新客户端mock配置,添加用户未开通额度账户场景测试
 12. `packages/credit-balance-management-ui-mt/tests/setup.ts` - 测试配置
 13. `packages/credit-balance-management-ui-mt/tests/unit/CreditBalanceDialog.test.tsx` - 单元测试
 14. `packages/credit-balance-management-ui-mt/tsconfig.json` - TypeScript配置
@@ -424,6 +460,17 @@ CREATE TABLE credit_balance_log_mt (
 18. `packages/credit-balance-module-mt/src/routes/set-limit.mt.ts` - 查看路由实现了解业务逻辑
 19. `packages/credit-balance-module-mt/src/schemas/index.ts` - 查看schema定义确认API参数
 
+**本次优化修改的文件**:
+1. `packages/credit-balance-management-ui-mt/src/components/CreditBalanceDialog.tsx` - 优化用户未开通额度账户的交互体验
+   - 修改额度查询API错误处理,区分404和其他错误
+   - 添加`hasCreditAccount`状态判断逻辑
+   - 当用户未开通额度账户时,显示友好的提示信息和开通按钮
+   - 复用现有额度设置表单,提供开通额度功能
+   - 开通成功后自动刷新数据,显示正常的额度管理界面
+2. `packages/credit-balance-management-ui-mt/tests/integration/creditBalanceDialog.integration.test.tsx` - 添加用户未开通额度账户场景测试
+   - 添加新的测试用例"应该处理用户未开通额度账户的场景"
+   - 测试404响应处理、开通额度功能、标签页提示等
+
 **技术特性**:
 - React 19.1.0 + TypeScript
 - shadcn/ui组件库(基于Radix UI)

+ 389 - 268
packages/credit-balance-management-ui-mt/src/components/CreditBalanceDialog.tsx

@@ -103,13 +103,21 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
     limit: 10
   });
 
-  // 额度查询
+  // 额度查询 - 区分404(用户额度账户不存在)和其他错误
   const { data: balanceData, isLoading: isLoadingBalance, refetch: refetchBalance, error: balanceError } = useQuery({
     queryKey: ['credit-balance', userId, tenantId],
     queryFn: async () => {
       const res = await creditBalanceClientManager.get()[':userId'].$get({
         param: { userId: userId.toString() }
       });
+
+      // 处理404错误 - 用户额度账户不存在,这是正常情况
+      if (res.status === 404) {
+        // 返回null表示用户没有额度账户
+        return null;
+      }
+
+      // 其他错误情况
       if (res.status !== 200) throw new Error('获取信用额度失败');
       return await res.json();
     },
@@ -118,14 +126,17 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
     gcTime: 10 * 60 * 1000,
   });
 
-  // 处理额度查询错误
+  // 处理额度查询错误 - 只处理非404错误
   useEffect(() => {
     if (balanceError) {
       toast.error(`获取信用额度失败: ${balanceError.message}`);
     }
   }, [balanceError]);
 
-  // 额度变更记录查询
+  // 判断用户是否有额度账户
+  const hasCreditAccount = balanceData !== null && balanceData !== undefined;
+
+  // 额度变更记录查询 - 只在用户有额度账户时查询
   const { data: logsData, isLoading: isLoadingLogs, error: logsError } = useQuery({
     queryKey: ['credit-balance-logs', userId, logsQueryParams, tenantId],
     queryFn: async () => {
@@ -139,7 +150,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
       if (res.status !== 200) throw new Error('获取额度变更记录失败');
       return await res.json();
     },
-    enabled: open && !!userId && activeTab === 'logs',
+    enabled: open && !!userId && activeTab === 'logs' && hasCreditAccount,
     staleTime: 5 * 60 * 1000,
     gcTime: 10 * 60 * 1000,
   });
@@ -180,7 +191,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
     }
   });
 
-  // 设置额度mutation
+  // 设置额度mutation - 也用于开通额度账户
   const setLimitMutation = useMutation({
     mutationFn: async (data: SetLimitFormData) => {
       const res = await creditBalanceClientManager.get()[':userId'].$put({
@@ -193,7 +204,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
       queryClient.invalidateQueries({ queryKey: ['credit-balance', userId] });
       queryClient.invalidateQueries({ queryKey: ['credit-balance-logs', userId] });
       setLimitForm.reset();
-      toast.success('额度设置成功');
+      toast.success(hasCreditAccount ? '额度设置成功' : '信用额度开通成功');
     },
     onError: (error) => {
       toast.error(`设置失败: ${error.message}`);
@@ -256,7 +267,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
 
   // 计算欠款信息
   const overdueInfo = useMemo(() => {
-    if (!balanceData) return null;
+    if (!balanceData || !hasCreditAccount) return null;
     const balance = balanceData;
     const isOverdue = balance.usedAmount > balance.totalLimit;
     const overdueAmount = isOverdue ? balance.usedAmount - balance.totalLimit : 0;
@@ -266,7 +277,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
       overdueAmount,
       severity: isOverdue ? 'high' : 'none' as 'high' | 'medium' | 'low' | 'none'
     };
-  }, [balanceData]);
+  }, [balanceData, hasCreditAccount]);
 
   // 获取对话框尺寸类名
   const getDialogSizeClass = () => {
@@ -294,7 +305,6 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
 
   if (!open) return null;
 
-  const balance = balanceData;
   const logs = logsData?.data || [];
   const logsPagination = logsData?.pagination;
 
@@ -332,7 +342,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                   <Skeleton className="h-4 w-32" />
                   <Skeleton className="h-4 w-48" />
                 </div>
-              ) : balance ? (
+              ) : hasCreditAccount ? (
                 <div className="grid grid-cols-2 gap-4">
                   <div>
                     <p className="text-sm text-muted-foreground">用户ID</p>
@@ -344,23 +354,37 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                   </div>
                   <div>
                     <p className="text-sm text-muted-foreground">额度状态</p>
-                    <Badge variant={balance.isEnabled ? 'default' : 'secondary'}>
-                      {balance.isEnabled ? '启用' : '禁用'}
+                    <Badge variant={balanceData!.isEnabled ? 'default' : 'secondary'}>
+                      {balanceData!.isEnabled ? '启用' : '禁用'}
                     </Badge>
                   </div>
                   <div>
                     <p className="text-sm text-muted-foreground">最后更新</p>
                     <p className="font-medium text-sm">
-                      {format(new Date(balance.updatedAt), 'yyyy-MM-dd HH:mm')}
+                      {format(new Date(balanceData!.updatedAt), 'yyyy-MM-dd HH:mm')}
                     </p>
                   </div>
                 </div>
               ) : (
-                <Alert variant="destructive">
-                  <AlertCircle className="h-4 w-4" />
-                  <AlertTitle>数据加载失败</AlertTitle>
-                  <AlertDescription>无法加载用户信用额度信息</AlertDescription>
-                </Alert>
+                <div className="space-y-4">
+                  <div className="grid grid-cols-2 gap-4">
+                    <div>
+                      <p className="text-sm text-muted-foreground">用户ID</p>
+                      <p className="font-medium">{userId}</p>
+                    </div>
+                    <div>
+                      <p className="text-sm text-muted-foreground">用户名</p>
+                      <p className="font-medium">{userName}</p>
+                    </div>
+                  </div>
+                  <Alert>
+                    <AlertCircle className="h-4 w-4" />
+                    <AlertTitle>用户尚未开通信用额度</AlertTitle>
+                    <AlertDescription>
+                      该用户目前没有信用额度账户,是否要为其开通信用额度?
+                    </AlertDescription>
+                  </Alert>
+                </div>
               )}
             </CardContent>
           </Card>
@@ -380,7 +404,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                   <Skeleton className="h-32 w-full" />
                   <Skeleton className="h-24 w-full" />
                 </div>
-              ) : balance ? (
+              ) : hasCreditAccount ? (
                 <>
                   {/* 额度统计卡片 */}
                   <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -393,7 +417,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                       </CardHeader>
                       <CardContent>
                         <div className="text-2xl font-bold">
-                          ¥{balance.totalLimit.toFixed(2)}
+                          ¥{balanceData!.totalLimit.toFixed(2)}
                         </div>
                         <p className="text-xs text-muted-foreground mt-1">
                           用户可用的最大信用额度
@@ -410,7 +434,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                       </CardHeader>
                       <CardContent>
                         <div className="text-2xl font-bold">
-                          ¥{balance.usedAmount.toFixed(2)}
+                          ¥{balanceData!.usedAmount.toFixed(2)}
                         </div>
                         <p className="text-xs text-muted-foreground mt-1">
                           用户当前已使用的额度
@@ -427,7 +451,7 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                       </CardHeader>
                       <CardContent>
                         <div className="text-2xl font-bold">
-                          ¥{balance.availableAmount.toFixed(2)}
+                          ¥{balanceData!.availableAmount.toFixed(2)}
                         </div>
                         <p className="text-xs text-muted-foreground mt-1">
                           剩余可用的信用额度
@@ -456,274 +480,371 @@ export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
                       <div className="space-y-2">
                         <div className="flex justify-between text-sm">
                           <span>使用进度</span>
-                          <span>{((balance.usedAmount / balance.totalLimit) * 100).toFixed(1)}%</span>
+                          <span>{((balanceData!.usedAmount / balanceData!.totalLimit) * 100).toFixed(1)}%</span>
                         </div>
                         <div className="h-2 bg-secondary rounded-full overflow-hidden">
                           <div
                             className="h-full bg-primary"
-                            style={{ width: `${Math.min((balance.usedAmount / balance.totalLimit) * 100, 100)}%` }}
+                            style={{ width: `${Math.min((balanceData!.usedAmount / balanceData!.totalLimit) * 100, 100)}%` }}
                           />
                         </div>
                         <div className="flex justify-between text-xs text-muted-foreground">
                           <span>0</span>
-                          <span>¥{balance.totalLimit.toFixed(2)}</span>
+                          <span>¥{balanceData!.totalLimit.toFixed(2)}</span>
                         </div>
                       </div>
                     </CardContent>
                   </Card>
                 </>
               ) : (
-                <Alert variant="destructive">
-                  <AlertCircle className="h-4 w-4" />
-                  <AlertTitle>数据加载失败</AlertTitle>
-                  <AlertDescription>无法加载信用额度信息</AlertDescription>
-                </Alert>
+                <div className="space-y-6">
+                  <Alert>
+                    <AlertCircle className="h-4 w-4" />
+                    <AlertTitle>用户尚未开通信用额度</AlertTitle>
+                    <AlertDescription>
+                      该用户目前没有信用额度账户,请为其设置初始信用额度以开通账户。
+                    </AlertDescription>
+                  </Alert>
+
+                  {/* 开通额度表单 */}
+                  <Card data-testid="open-credit-account-card">
+                    <CardHeader>
+                      <CardTitle className="text-sm font-medium flex items-center gap-2">
+                        <CreditCard className="h-4 w-4" />
+                        开通信用额度
+                      </CardTitle>
+                      <CardDescription>
+                        为该用户设置初始信用额度以开通信用账户
+                      </CardDescription>
+                    </CardHeader>
+                    <CardContent>
+                      <Form {...setLimitForm}>
+                        <form onSubmit={setLimitForm.handleSubmit(onSetLimitSubmit)} className="space-y-4">
+                          <FormField
+                            control={setLimitForm.control}
+                            name="totalLimit"
+                            render={({ field }) => (
+                              <FormItem>
+                                <FormLabel>初始额度</FormLabel>
+                                <FormControl>
+                                  <Input
+                                    type="number"
+                                    step="0.01"
+                                    placeholder="请输入初始额度"
+                                    data-testid="open-credit-total-limit-input"
+                                    value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
+                                    onChange={(e) => {
+                                      const value = e.target.value;
+                                      if (value === '') {
+                                        field.onChange('');
+                                      } else {
+                                        field.onChange(parseFloat(value));
+                                      }
+                                    }}
+                                    onBlur={field.onBlur}
+                                    name={field.name}
+                                    ref={field.ref}
+                                  />
+                                </FormControl>
+                                <FormDescription>
+                                  设置用户的初始信用额度,可以为0
+                                </FormDescription>
+                                <FormMessage />
+                              </FormItem>
+                            )}
+                          />
+                          <FormField
+                            control={setLimitForm.control}
+                            name="remark"
+                            render={({ field }) => (
+                              <FormItem>
+                                <FormLabel>备注</FormLabel>
+                                <FormControl>
+                                  <Input placeholder="请输入开通备注(可选)" {...field} />
+                                </FormControl>
+                                <FormMessage />
+                              </FormItem>
+                            )}
+                          />
+                          <Button
+                            type="submit"
+                            className="w-full"
+                            disabled={setLimitMutation.isPending}
+                            data-testid="open-credit-account-button"
+                          >
+                            {setLimitMutation.isPending ? '开通中...' : '开通信用额度'}
+                          </Button>
+                        </form>
+                      </Form>
+                    </CardContent>
+                  </Card>
+                </div>
               )}
             </TabsContent>
 
-            {/* 额度操作标签页 */}
+            {/* 额度操作标签页 - 只在用户有额度账户时显示 */}
             <TabsContent value="operations" className="space-y-4">
-              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-                {/* 设置额度表单 */}
-                <Card data-testid="set-limit-form-card">
-                  <CardHeader>
-                    <CardTitle className="text-sm font-medium flex items-center gap-2">
-                      <Settings className="h-4 w-4" />
-                      设置额度
-                    </CardTitle>
-                    <CardDescription>
-                      设置用户的总信用额度
-                    </CardDescription>
-                  </CardHeader>
-                  <CardContent>
-                    <Form {...setLimitForm}>
-                      <form onSubmit={setLimitForm.handleSubmit(onSetLimitSubmit)} className="space-y-4">
-                        <FormField
-                          control={setLimitForm.control}
-                          name="totalLimit"
-                          render={({ field }) => (
-                            <FormItem>
-                              <FormLabel>总额度</FormLabel>
-                              <FormControl>
-                                <Input
-                                  type="number"
-                                  step="0.01"
-                                  placeholder="请输入总额度"
-                                  data-testid="total-limit-input"
-                                  value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
-                                  onChange={(e) => {
-                                    const value = e.target.value;
-                                    if (value === '') {
-                                      field.onChange('');
-                                    } else {
-                                      field.onChange(parseFloat(value));
-                                    }
-                                  }}
-                                  onBlur={field.onBlur}
-                                  name={field.name}
-                                  ref={field.ref}
-                                />
-                              </FormControl>
-                              <FormDescription>
-                                设置用户可用的最大信用额度
-                              </FormDescription>
-                              <FormMessage />
-                            </FormItem>
-                          )}
-                        />
-                        <FormField
-                          control={setLimitForm.control}
-                          name="remark"
-                          render={({ field }) => (
-                            <FormItem>
-                              <FormLabel>备注</FormLabel>
-                              <FormControl>
-                                <Input placeholder="请输入备注(可选)" {...field} />
-                              </FormControl>
-                              <FormMessage />
-                            </FormItem>
-                          )}
-                        />
-                        <Button
-                          type="submit"
-                          className="w-full"
-                          disabled={setLimitMutation.isPending}
-                          data-testid="set-limit-button"
-                        >
-                          {setLimitMutation.isPending ? '设置中...' : '设置额度'}
-                        </Button>
-                      </form>
-                    </Form>
-                  </CardContent>
-                </Card>
-
-                {/* 调整额度表单 */}
-                <Card data-testid="adjust-limit-form-card">
-                  <CardHeader>
-                    <CardTitle className="text-sm font-medium flex items-center gap-2">
-                      <Edit className="h-4 w-4" />
-                      调整额度
-                    </CardTitle>
-                    <CardDescription>
-                      增加或减少用户的信用额度
-                    </CardDescription>
-                  </CardHeader>
-                  <CardContent>
-                    <Form {...adjustLimitForm}>
-                      <form onSubmit={adjustLimitForm.handleSubmit(onAdjustLimitSubmit)} className="space-y-4">
-                        <FormField
-                          control={adjustLimitForm.control}
-                          name="adjustAmount"
-                          render={({ field }) => (
-                            <FormItem>
-                              <FormLabel>调整金额</FormLabel>
-                              <FormControl>
-                                <Input
-                                  type="number"
-                                  step="0.01"
-                                  placeholder="正数增加,负数减少"
-                                  data-testid="adjust-amount-input"
-                                  value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
-                                  onChange={(e) => {
-                                    const value = e.target.value;
-                                    if (value === '') {
-                                      field.onChange('');
-                                    } else {
-                                      field.onChange(parseFloat(value));
-                                    }
-                                  }}
-                                  onBlur={field.onBlur}
-                                  name={field.name}
-                                  ref={field.ref}
-                                />
-                              </FormControl>
-                              <FormDescription>
-                                正数表示增加额度,负数表示减少额度
-                              </FormDescription>
-                              <FormMessage />
-                            </FormItem>
-                          )}
-                        />
-                        <FormField
-                          control={adjustLimitForm.control}
-                          name="remark"
-                          render={({ field }) => (
-                            <FormItem>
-                              <FormLabel>备注</FormLabel>
-                              <FormControl>
-                                <Input placeholder="请输入调整原因(可选)" {...field} />
-                              </FormControl>
-                              <FormMessage />
-                            </FormItem>
-                          )}
-                        />
-                        <Button
-                          type="submit"
-                          className="w-full"
-                          disabled={adjustLimitMutation.isPending}
-                          data-testid="adjust-limit-button"
-                        >
-                          {adjustLimitMutation.isPending ? '调整中...' : '调整额度'}
-                        </Button>
-                      </form>
-                    </Form>
-                  </CardContent>
-                </Card>
-              </div>
-
-              {/* 结账恢复额度表单 */}
-              <Card data-testid="checkout-form-card">
-                <CardHeader>
-                  <CardTitle className="text-sm font-medium flex items-center gap-2">
-                    <CheckCircle className="h-4 w-4" />
-                    结账恢复额度
-                  </CardTitle>
-                  <CardDescription>
-                    手动恢复用户的信用额度(通常用于结账后)
-                  </CardDescription>
-                </CardHeader>
-                <CardContent>
-                  <Form {...checkoutForm}>
-                    <form onSubmit={checkoutForm.handleSubmit(onCheckoutSubmit)} className="space-y-4">
-                      <FormField
-                        control={checkoutForm.control}
-                        name="amount"
-                        render={({ field }) => (
-                          <FormItem>
-                            <FormLabel>恢复金额</FormLabel>
-                            <FormControl>
-                              <Input
-                                type="number"
-                                step="0.01"
-                                placeholder="请输入恢复金额"
-                                data-testid="checkout-amount-input"
-                                value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
-                                onChange={(e) => {
-                                  const value = e.target.value;
-                                  if (value === '') {
-                                    field.onChange('');
-                                  } else {
-                                    field.onChange(parseFloat(value));
-                                  }
-                                }}
-                                onBlur={field.onBlur}
-                                name={field.name}
-                                ref={field.ref}
-                              />
-                            </FormControl>
-                            <FormDescription>
-                              输入要恢复的额度金额
-                            </FormDescription>
-                            <FormMessage />
-                          </FormItem>
-                        )}
-                      />
-                      <FormField
-                        control={checkoutForm.control}
-                        name="referenceId"
-                        render={({ field }) => (
-                          <FormItem>
-                            <FormLabel>关联ID(订单号等)</FormLabel>
-                            <FormControl>
-                              <Input placeholder="请输入关联ID(可选)" {...field} />
-                            </FormControl>
-                            <FormDescription>
-                              关联的订单号或其他标识符
-                            </FormDescription>
-                            <FormMessage />
-                          </FormItem>
-                        )}
-                      />
-                      <FormField
-                        control={checkoutForm.control}
-                        name="remark"
-                        render={({ field }) => (
-                          <FormItem>
-                            <FormLabel>备注</FormLabel>
-                            <FormControl>
-                              <Input placeholder="请输入备注(可选)" {...field} />
-                            </FormControl>
-                            <FormMessage />
-                          </FormItem>
-                        )}
-                      />
-                      <Button
-                        type="submit"
-                        className="w-full"
-                        disabled={checkoutMutation.isPending}
-                        data-testid="checkout-button"
-                      >
-                        {checkoutMutation.isPending ? '恢复中...' : '结账恢复额度'}
-                      </Button>
-                    </form>
-                  </Form>
-                </CardContent>
-              </Card>
+              {hasCreditAccount ? (
+                <>
+                  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    {/* 设置额度表单 */}
+                    <Card data-testid="set-limit-form-card">
+                      <CardHeader>
+                        <CardTitle className="text-sm font-medium flex items-center gap-2">
+                          <Settings className="h-4 w-4" />
+                          设置额度
+                        </CardTitle>
+                        <CardDescription>
+                          设置用户的总信用额度
+                        </CardDescription>
+                      </CardHeader>
+                      <CardContent>
+                        <Form {...setLimitForm}>
+                          <form onSubmit={setLimitForm.handleSubmit(onSetLimitSubmit)} className="space-y-4">
+                            <FormField
+                              control={setLimitForm.control}
+                              name="totalLimit"
+                              render={({ field }) => (
+                                <FormItem>
+                                  <FormLabel>总额度</FormLabel>
+                                  <FormControl>
+                                    <Input
+                                      type="number"
+                                      step="0.01"
+                                      placeholder="请输入总额度"
+                                      data-testid="total-limit-input"
+                                      value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
+                                      onChange={(e) => {
+                                        const value = e.target.value;
+                                        if (value === '') {
+                                          field.onChange('');
+                                        } else {
+                                          field.onChange(parseFloat(value));
+                                        }
+                                      }}
+                                      onBlur={field.onBlur}
+                                      name={field.name}
+                                      ref={field.ref}
+                                    />
+                                  </FormControl>
+                                  <FormDescription>
+                                    设置用户可用的最大信用额度
+                                  </FormDescription>
+                                  <FormMessage />
+                                </FormItem>
+                              )}
+                            />
+                            <FormField
+                              control={setLimitForm.control}
+                              name="remark"
+                              render={({ field }) => (
+                                <FormItem>
+                                  <FormLabel>备注</FormLabel>
+                                  <FormControl>
+                                    <Input placeholder="请输入备注(可选)" {...field} />
+                                  </FormControl>
+                                  <FormMessage />
+                                </FormItem>
+                              )}
+                            />
+                            <Button
+                              type="submit"
+                              className="w-full"
+                              disabled={setLimitMutation.isPending}
+                              data-testid="set-limit-button"
+                            >
+                              {setLimitMutation.isPending ? '设置中...' : '设置额度'}
+                            </Button>
+                          </form>
+                        </Form>
+                      </CardContent>
+                    </Card>
+
+                    {/* 调整额度表单 */}
+                    <Card data-testid="adjust-limit-form-card">
+                      <CardHeader>
+                        <CardTitle className="text-sm font-medium flex items-center gap-2">
+                          <Edit className="h-4 w-4" />
+                          调整额度
+                        </CardTitle>
+                        <CardDescription>
+                          增加或减少用户的信用额度
+                        </CardDescription>
+                      </CardHeader>
+                      <CardContent>
+                        <Form {...adjustLimitForm}>
+                          <form onSubmit={adjustLimitForm.handleSubmit(onAdjustLimitSubmit)} className="space-y-4">
+                            <FormField
+                              control={adjustLimitForm.control}
+                              name="adjustAmount"
+                              render={({ field }) => (
+                                <FormItem>
+                                  <FormLabel>调整金额</FormLabel>
+                                  <FormControl>
+                                    <Input
+                                      type="number"
+                                      step="0.01"
+                                      placeholder="正数增加,负数减少"
+                                      data-testid="adjust-amount-input"
+                                      value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
+                                      onChange={(e) => {
+                                        const value = e.target.value;
+                                        if (value === '') {
+                                          field.onChange('');
+                                        } else {
+                                          field.onChange(parseFloat(value));
+                                        }
+                                      }}
+                                      onBlur={field.onBlur}
+                                      name={field.name}
+                                      ref={field.ref}
+                                    />
+                                  </FormControl>
+                                  <FormDescription>
+                                    正数表示增加额度,负数表示减少额度
+                                  </FormDescription>
+                                  <FormMessage />
+                                </FormItem>
+                              )}
+                            />
+                            <FormField
+                              control={adjustLimitForm.control}
+                              name="remark"
+                              render={({ field }) => (
+                                <FormItem>
+                                  <FormLabel>备注</FormLabel>
+                                  <FormControl>
+                                    <Input placeholder="请输入调整原因(可选)" {...field} />
+                                  </FormControl>
+                                  <FormMessage />
+                                </FormItem>
+                              )}
+                            />
+                            <Button
+                              type="submit"
+                              className="w-full"
+                              disabled={adjustLimitMutation.isPending}
+                              data-testid="adjust-limit-button"
+                            >
+                              {adjustLimitMutation.isPending ? '调整中...' : '调整额度'}
+                            </Button>
+                          </form>
+                        </Form>
+                      </CardContent>
+                    </Card>
+                  </div>
+
+                  {/* 结账恢复额度表单 */}
+                  <Card data-testid="checkout-form-card">
+                    <CardHeader>
+                      <CardTitle className="text-sm font-medium flex items-center gap-2">
+                        <CheckCircle className="h-4 w-4" />
+                        结账恢复额度
+                      </CardTitle>
+                      <CardDescription>
+                        手动恢复用户的信用额度(通常用于结账后)
+                      </CardDescription>
+                    </CardHeader>
+                    <CardContent>
+                      <Form {...checkoutForm}>
+                        <form onSubmit={checkoutForm.handleSubmit(onCheckoutSubmit)} className="space-y-4">
+                          <FormField
+                            control={checkoutForm.control}
+                            name="amount"
+                            render={({ field }) => (
+                              <FormItem>
+                                <FormLabel>恢复金额</FormLabel>
+                                <FormControl>
+                                  <Input
+                                    type="number"
+                                    step="0.01"
+                                    placeholder="请输入恢复金额"
+                                    data-testid="checkout-amount-input"
+                                    value={field.value === undefined || field.value === null || String(field.value) === '' ? '' : String(field.value || 0)}
+                                    onChange={(e) => {
+                                      const value = e.target.value;
+                                      if (value === '') {
+                                        field.onChange('');
+                                      } else {
+                                        field.onChange(parseFloat(value));
+                                      }
+                                    }}
+                                    onBlur={field.onBlur}
+                                    name={field.name}
+                                    ref={field.ref}
+                                  />
+                                </FormControl>
+                                <FormDescription>
+                                  输入要恢复的额度金额
+                                </FormDescription>
+                                <FormMessage />
+                              </FormItem>
+                            )}
+                          />
+                          <FormField
+                            control={checkoutForm.control}
+                            name="referenceId"
+                            render={({ field }) => (
+                              <FormItem>
+                                <FormLabel>关联ID(订单号等)</FormLabel>
+                                <FormControl>
+                                  <Input placeholder="请输入关联ID(可选)" {...field} />
+                                </FormControl>
+                                <FormDescription>
+                                  关联的订单号或其他标识符
+                                </FormDescription>
+                                <FormMessage />
+                              </FormItem>
+                            )}
+                          />
+                          <FormField
+                            control={checkoutForm.control}
+                            name="remark"
+                            render={({ field }) => (
+                              <FormItem>
+                                <FormLabel>备注</FormLabel>
+                                <FormControl>
+                                  <Input placeholder="请输入备注(可选)" {...field} />
+                                </FormControl>
+                                <FormMessage />
+                              </FormItem>
+                            )}
+                          />
+                          <Button
+                            type="submit"
+                            className="w-full"
+                            disabled={checkoutMutation.isPending}
+                            data-testid="checkout-button"
+                          >
+                            {checkoutMutation.isPending ? '恢复中...' : '结账恢复额度'}
+                          </Button>
+                        </form>
+                      </Form>
+                    </CardContent>
+                  </Card>
+                </>
+              ) : (
+                <Alert>
+                  <AlertCircle className="h-4 w-4" />
+                  <AlertTitle>用户尚未开通信用额度</AlertTitle>
+                  <AlertDescription>
+                    请先在"额度概览"标签页中为该用户开通信用额度,然后才能进行额度操作。
+                  </AlertDescription>
+                </Alert>
+              )}
             </TabsContent>
 
             {/* 变更记录标签页 */}
             <TabsContent value="logs" className="space-y-4">
-              {isLoadingLogs ? (
+              {!hasCreditAccount ? (
+                <Alert>
+                  <AlertCircle className="h-4 w-4" />
+                  <AlertTitle>用户尚未开通信用额度</AlertTitle>
+                  <AlertDescription>
+                    请先在"额度概览"标签页中为该用户开通信用额度,然后才能查看变更记录。
+                  </AlertDescription>
+                </Alert>
+              ) : isLoadingLogs ? (
                 <div className="space-y-2">
                   <Skeleton className="h-8 w-full" />
                   <Skeleton className="h-64 w-full" />

+ 89 - 0
packages/credit-balance-management-ui-mt/tests/integration/creditBalanceDialog.integration.test.tsx

@@ -680,4 +680,93 @@ describe('信用额度管理对话框集成测试', () => {
     expect(screen.getByText('用户存在欠款')).toBeInTheDocument();
     expect(screen.getByText(/用户已超出信用额度 ¥2000\.00/)).toBeInTheDocument();
   });
+
+  it('应该处理用户未开通额度账户的场景', async () => {
+    const { toast } = await import('sonner');
+
+    // Mock 404响应 - 用户额度账户不存在
+    (creditBalanceClient[':userId'].$get as any).mockResolvedValue(
+      createMockResponse(404, { message: '用户额度账户不存在' })
+    );
+
+    // Mock 设置额度API成功响应(用于开通额度)
+    (creditBalanceClient[':userId'].$put as any).mockResolvedValue(
+      createMockResponse(200, { success: true })
+    );
+
+    renderWithProviders(
+      <CreditBalanceDialog
+        userId={123}
+        userName="测试用户"
+        open={true}
+        onOpenChange={() => {}}
+      />
+    );
+
+    // 等待数据加载完成(404不会抛出错误)
+    await waitFor(() => {
+      // 应该显示用户未开通额度的提示 - 使用getAllByText处理多个元素
+      const alerts = screen.getAllByText('用户尚未开通信用额度');
+      expect(alerts.length).toBeGreaterThan(0);
+
+      // 检查开通额度表单是否显示
+      expect(screen.getByTestId('open-credit-account-card')).toBeInTheDocument();
+    });
+
+    // 验证没有显示错误toast(404是正常情况)
+    expect(toast.error).not.toHaveBeenCalled();
+
+    // 应该显示开通额度表单
+    expect(screen.getByTestId('open-credit-account-card')).toBeInTheDocument();
+    // 使用getAllByText处理多个"开通信用额度"元素
+    const openCreditTexts = screen.getAllByText('开通信用额度');
+    expect(openCreditTexts.length).toBeGreaterThan(0);
+    expect(screen.getByTestId('open-credit-total-limit-input')).toBeInTheDocument();
+    expect(screen.getByTestId('open-credit-account-button')).toBeInTheDocument();
+
+    // 测试开通额度功能
+    const totalLimitInput = screen.getByTestId('open-credit-total-limit-input');
+    fireEvent.change(totalLimitInput, { target: { value: '5000' } });
+
+    const openCreditButton = screen.getByTestId('open-credit-account-button');
+    fireEvent.click(openCreditButton);
+
+    // 等待API调用
+    await waitFor(() => {
+      expect(creditBalanceClient[':userId'].$put).toHaveBeenCalledWith({
+        param: { userId: '123' },
+        json: {
+          totalLimit: 5000,
+          remark: ''
+        }
+      });
+    });
+
+    // 应该显示开通成功的toast
+    expect(toast.success).toHaveBeenCalledWith('信用额度开通成功');
+
+    // 测试切换到其他标签页时的提示
+    const user = userEvent.setup();
+
+    // 切换到额度操作标签页
+    const operationsTab = screen.getByText('额度操作');
+    await user.click(operationsTab);
+
+    // 应该显示提示信息,而不是操作表单
+    await waitFor(() => {
+      expect(screen.getByText('请先在"额度概览"标签页中为该用户开通信用额度,然后才能进行额度操作。')).toBeInTheDocument();
+    });
+
+    // 切换到变更记录标签页
+    const logsTab = screen.getByText('变更记录');
+    await user.click(logsTab);
+
+    // 应该显示提示信息,而不是记录表格
+    await waitFor(() => {
+      expect(screen.getByText('请先在"额度概览"标签页中为该用户开通信用额度,然后才能查看变更记录。')).toBeInTheDocument();
+    });
+
+    // 验证没有调用变更记录API(因为用户没有额度账户)
+    expect(creditBalanceClient[':userId'].logs.$get).not.toHaveBeenCalled();
+  });
 });