hejianhao
2026-08-18 3df055ea1038e0bbcbbbf80ccbeea43c82952932
feat0806: 优化代理搜索条件
7个文件已修改
178 ■■■■■ 已修改文件
tcm-training-admin-backend/sql/20260806_admin.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/controller/ProvincialAgentController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/dto/ProvincialAgentSaveRequest.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/entity/ProvincialAgent.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/service/ProvincialAgentAppService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/vo/ProvincialAgentListItemVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/member/agent/index.tsx 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/20260806_admin.sql
@@ -10,6 +10,7 @@
    account_holder VARCHAR(50) COMMENT '账户名称/户名(即开户人姓名或公司名称)',
    branch_name VARCHAR(100) COMMENT '开户支行名称(如:成都高新支行)',
    contact_info VARCHAR(200) COMMENT '联系信息(可填电话、邮箱、手机号等)',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用,2注销',
    address VARCHAR(255) COMMENT '详细地址(如:成都市高新区天府大道XX号)',
    `deleted` tinyint(1) NOT NULL DEFAULT '0',
  `created_by` varchar(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/controller/ProvincialAgentController.java
@@ -22,8 +22,8 @@
    }
    @GetMapping("/provincial-agents")
    public ApiResponse<PageResponse<ProvincialAgentListItemVO>> page(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String agentName, @RequestParam(required = false) String regionCode) {
        return ApiResponse.ok(service.page(page, pageSize, agentName, regionCode));
    public ApiResponse<PageResponse<ProvincialAgentListItemVO>> page(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) String regionCode, @RequestParam(required = false) String clinicName, @RequestParam(required = false) String contactInfo, @RequestParam(required = false) Integer status) {
        return ApiResponse.ok(service.page(page, pageSize, regionCode, clinicName, contactInfo, status));
    }
    @GetMapping("/provincial-agents/{id}")
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/dto/ProvincialAgentSaveRequest.java
@@ -5,6 +5,8 @@
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.List;
@@ -27,6 +29,9 @@
    private String contactInfo;
    @Size(max = 255)
    private String address;
    @Min(value = 0, message = "状态值不正确")
    @Max(value = 2, message = "状态值不正确")
    private Integer status;
    @Valid
    @Size(min = 1, message = "请选择负责省份")
    private List<RegionItem> regions = new ArrayList<RegionItem>();
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/entity/ProvincialAgent.java
@@ -37,6 +37,8 @@
    @TableField("contact_info")
    private String contactInfo;
    private Integer status;
    @TableField("address")
    private String address;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/service/ProvincialAgentAppService.java
@@ -37,17 +37,27 @@
        this.clinicMapper = clinicMapper;
    }
    public PageResponse<ProvincialAgentListItemVO> page(int page, int pageSize, String agentName, String regionCode) {
    public PageResponse<ProvincialAgentListItemVO> page(int page, int pageSize, String regionCode,
                                                        String clinicName, String contactInfo, Integer status) {
        LambdaQueryWrapper<ProvincialAgent> query = new LambdaQueryWrapper<ProvincialAgent>()
                .eq(ProvincialAgent::getDeleted, 0)
                .like(trim(agentName) != null, ProvincialAgent::getAgentName, trim(agentName))
                .orderByAsc(ProvincialAgent::getCreatedAt).orderByDesc(ProvincialAgent::getId);
                .like(trim(contactInfo) != null, ProvincialAgent::getContactInfo, trim(contactInfo))
                .eq(status != null, ProvincialAgent::getStatus, status)
                .orderByAsc(ProvincialAgent::getCreatedAt).orderByAsc(ProvincialAgent::getId);
        if (trim(regionCode) != null) {
            List<Long> ids = new ArrayList<Long>();
            for (AgentRegion region : regionMapper.selectList(new LambdaQueryWrapper<AgentRegion>()
                    .eq(AgentRegion::getRegionCode, trim(regionCode))))
                ids.add(region.getAgentId());
            if (ids.isEmpty()) return PageResponse.of(new ArrayList<ProvincialAgentListItemVO>(), 0, page, pageSize);
                    .eq(AgentRegion::getRegionCode, trim(regionCode)))) ids.add(region.getAgentId());
            if (ids.isEmpty()) return emptyPage(page, pageSize);
            query.in(ProvincialAgent::getId, ids);
        }
        if (trim(clinicName) != null) {
            List<Long> ids = new ArrayList<Long>();
            for (Clinic clinic : clinicMapper.selectList(new LambdaQueryWrapper<Clinic>()
                    .like(Clinic::getName, trim(clinicName)))) {
                if (clinic.getAgentId() != null && !ids.contains(clinic.getAgentId())) ids.add(clinic.getAgentId());
            }
            if (ids.isEmpty()) return emptyPage(page, pageSize);
            query.in(ProvincialAgent::getId, ids);
        }
        IPage<ProvincialAgent> result = agentMapper.selectPage(new Page<ProvincialAgent>(page, pageSize), query);
@@ -56,6 +66,9 @@
        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
    }
    private PageResponse<ProvincialAgentListItemVO> emptyPage(int page, int pageSize) {
        return PageResponse.of(new ArrayList<ProvincialAgentListItemVO>(), 0, page, pageSize);
    }
    public ProvincialAgentDetailVO detail(Long id) {
        return toDetail(require(id));
    }
@@ -99,6 +112,7 @@
        agent.setAccountHolder(trim(request.getAccountHolder()));
        agent.setBranchName(trim(request.getBranchName()));
        agent.setContactInfo(trim(request.getContactInfo()));
        agent.setStatus(request.getStatus() == null ? 1 : request.getStatus());
        agent.setAddress(trim(request.getAddress()));
    }
@@ -130,10 +144,14 @@
        vo.setContactInfo(agent.getContactInfo());
        vo.setAddress(agent.getAddress());
        vo.setCreatedAt(agent.getCreatedAt());
        vo.setStatus(agent.getStatus());
        vo.setClinicCount(clinicMapper.selectCount(new LambdaQueryWrapper<Clinic>().eq(Clinic::getAgentId, agent.getId())));
        List<AgentRegion> regions = regions(agent.getId());
        vo.setRegionCount(regions.size());
        List<String> names = new ArrayList<String>();
        for (AgentRegion region : regions) names.add(resolveRegion(region.getRegionCode()).name);
        for (AgentRegion region : regions) {
            names.add(resolveRegion(region.getRegionCode()).name);
        }
        vo.setRegionNames(names);
        return vo;
    }
@@ -148,6 +166,8 @@
        vo.setRegionCount(base.getRegionCount());
        vo.setRegionNames(base.getRegionNames());
        vo.setCreatedAt(base.getCreatedAt());
        vo.setStatus(base.getStatus());
        vo.setClinicCount(base.getClinicCount());
        vo.setBankName(agent.getBankName());
        vo.setAccountNumber(agent.getAccountNumber());
        vo.setAccountHolder(agent.getAccountHolder());
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/agent/vo/ProvincialAgentListItemVO.java
@@ -16,4 +16,8 @@
    private Integer regionCount;
    private List<String> regionNames;
    private LocalDateTime createdAt;
    private Integer status;
    private Long clinicCount;
}
tcm-training-admin/src/pages/member/agent/index.tsx
@@ -1,16 +1,19 @@
import RegionCascader from '@/components/RegionCascader';
import { PlusOutlined } from '@ant-design/icons';
import { history, useAccess } from '@umijs/max';
import { Button, Descriptions, Form, Input, Modal, Pagination, Space, Table, message } from 'antd';
import { Button, Descriptions, Form, Input, Modal, Pagination, Select, Space, Table, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { createAgent, deleteAgent, getAgentDetail, getAgentPage, updateAgent } from './service';
type RegionValue = { regionCode?: string[]; regionName?: string };
type AgentItem = { id: number; agentName: string; contactInfo?: string; address?: string; regionNames?: string[]; createdAt?: string };
type RegionValue = { regionCode?: string[] };
type SearchValues = { regionPath?: string[]; clinicName?: string; contactInfo?: string; status?: number };
type AgentItem = { id: number; agentName: string; contactInfo?: string; address?: string; regionNames?: string[]; clinicCount?: number; status?: number; createdAt?: string };
const statusOptions = [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }, { label: '注销', value: 2 }];
const statusText = (value?: number) => value === 1 ? '启用' : value === 2 ? '注销' : '禁用';
const AgentPage: React.FC = () => {
  const access = useAccess();
  const [searchForm] = Form.useForm<{ regionPath?: string[]; agentName?: string }>();
  const [searchForm] = Form.useForm<SearchValues>();
  const [form] = Form.useForm();
  const [list, setList] = useState<AgentItem[]>([]);
  const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
@@ -25,128 +28,89 @@
    setLoading(true);
    try {
      const response = await getAgentPage({
        page,
        pageSize,
        agentName: search.agentName?.trim() || undefined,
        page, pageSize,
        regionCode: search.regionPath?.[search.regionPath.length - 1],
        clinicName: search.clinicName?.trim() || undefined,
        contactInfo: search.contactInfo?.trim() || undefined,
        status: search.status,
      });
      setList(response.data?.list || []);
      setPagination({
        page: Number(response.data?.page || page),
        pageSize: Number(response.data?.pageSize || pageSize),
        total: Number(response.data?.total || 0),
      });
    } finally {
      setLoading(false);
    }
      setPagination({ page: Number(response.data?.page || page), pageSize: Number(response.data?.pageSize || pageSize), total: Number(response.data?.total || 0) });
    } finally { setLoading(false); }
  };
  useEffect(() => {
    if (!canView) history.replace('/Welcome');
    else void load();
  }, [canView]);
  useEffect(() => { if (!canView) history.replace('/Welcome'); else void load(); }, [canView]);
  const openForm = async (id?: number) => {
    setEditingId(id);
    setDetail(undefined);
    setOpen(true);
    form.resetFields();
    if (!id) {
      form.setFieldsValue({ regions: [{}] });
      return;
    }
    setEditingId(id); setDetail(undefined); setOpen(true); form.resetFields();
    if (!id) { form.setFieldsValue({ regions: [{}], status: 1 }); return; }
    const response = await getAgentDetail(id);
    form.setFieldsValue({
      ...response.data,
      regions: (response.data?.regions || []).map((region: any) => ({
        ...region,
        regionCode: region.regionPath,
      })),
    });
    form.setFieldsValue({ ...response.data, regions: (response.data?.regions || []).map((region: any) => ({ ...region, regionCode: region.regionPath })) });
  };
  const openDetail = async (id: number) => {
    const response = await getAgentDetail(id);
    setEditingId(undefined);
    setDetail(response.data);
    setOpen(true);
  };
  const openDetail = async (id: number) => { const response = await getAgentDetail(id); setEditingId(undefined); setDetail(response.data); setOpen(true); };
  const submit = async () => {
    const values = await form.validateFields();
    const regions = (values.regions || []).map((region: RegionValue) => ({
      regionCode: region.regionCode?.[region.regionCode.length - 1],
    }));
    const regions = (values.regions || []).map((region: RegionValue) => ({ regionCode: region.regionCode?.[region.regionCode.length - 1] }));
    try {
      if (editingId) await updateAgent(editingId, { ...values, regions });
      else await createAgent({ ...values, regions });
      message.success(editingId ? '保存成功' : '新增成功');
      setOpen(false);
      await load(editingId ? pagination.page : 1);
    } catch (error) {
      message.error((error as Error)?.message || '保存失败');
    }
      if (editingId) await updateAgent(editingId, { ...values, regions }); else await createAgent({ ...values, regions });
      message.success(editingId ? '保存成功' : '新增成功'); setOpen(false); await load(editingId ? pagination.page : 1);
    } catch (error) { message.error((error as Error)?.message || '保存失败'); }
  };
  const columns = [
    { title: '序号', width: 70, render: (_: any, __: any, index: number) => (pagination.page - 1) * pagination.pageSize + index + 1 },
    { title: '代理负责人', dataIndex: 'agentName' },
    { title: '负责人', dataIndex: 'agentName' },
    { title: '电话', dataIndex: 'contactInfo', render: (value: string) => value || '-' },
    { title: '负责省份', render: (_: any, record: AgentItem) => (record.regionNames || []).join('、') || '-' },
    { title: '联系信息', dataIndex: 'contactInfo', render: (value: string) => value || '-' },
    { title: '详细地址', dataIndex: 'address', render: (value: string) => value || '-' },
    { title: '下级诊所数', dataIndex: 'clinicCount', width: 110, render: (value: number) => value ?? 0 },
    { title: '状态', dataIndex: 'status', width: 90, render: statusText },
    { title: '创建时间', dataIndex: 'createdAt', width: 180 },
    {
      title: '操作',
      width: 180,
      render: (_: any, record: AgentItem, index: number) => <Space size={0}>
        {access['member:agent:detail'] && <Button type="link" onClick={() => openDetail(record.id)}>详情</Button>}
        {access['member:agent:edit'] && <Button type="link" onClick={() => openForm(record.id)}>编辑</Button>}
        {access['member:agent:delete'] && !(pagination.page === 1 && index === 0) && <Button type="link" danger onClick={() => Modal.confirm({
          title: '确定删除该省代理吗?',
          onOk: async () => { await deleteAgent(record.id); message.success('删除成功'); await load(); },
        })}>删除</Button>}
      </Space>,
    },
    { title: '操作', width: 180, render: (_: any, record: AgentItem, index: number) => <Space size={0}>
      {access['member:agent:detail'] && <Button type="link" onClick={() => openDetail(record.id)}>详情</Button>}
      {access['member:agent:edit'] && <Button type="link" onClick={() => openForm(record.id)}>编辑</Button>}
      {access['member:agent:delete'] && !(pagination.page === 1 && index === 0) && <Button type="link" danger onClick={() => Modal.confirm({ title: '确定删除该省代理吗?', onOk: async () => { await deleteAgent(record.id); message.success('删除成功'); await load(); } })}>删除</Button>}
    </Space> },
  ];
  return <div style={{ padding: 24 }}>
    <h2>省代理管理</h2>
    <Form form={searchForm} layout="inline" onFinish={() => load(1)}>
      <Form.Item name="regionPath" label="负责省份"><RegionCascader allowClear changeOnSelect placeholder="全部" /></Form.Item>
      <Form.Item name="agentName" label="代理负责人"><Input allowClear placeholder="请输入" /></Form.Item>
      <Form.Item name="regionPath" label="诊所区域"><RegionCascader allowClear changeOnSelect placeholder="全部" /></Form.Item>
      <Form.Item name="clinicName" label="诊所名称"><Input allowClear placeholder="请输入" /></Form.Item>
      <Form.Item name="contactInfo" label="电话"><Input allowClear placeholder="请输入" /></Form.Item>
      <Form.Item name="status" label="状态"><Select allowClear placeholder="请选择" style={{ width: 120 }} options={statusOptions} /></Form.Item>
      <Button onClick={() => { searchForm.resetFields(); void load(1); }}>重置</Button>
      <Button type="primary" htmlType="submit" style={{ marginLeft: 8 }}>查询</Button>
    </Form>
    <div style={{ margin: '16px 0' }}>{access['member:agent:add'] && <Button type="primary" icon={<PlusOutlined />} onClick={() => openForm()}>新增</Button>}</div>
    <Table rowKey="id" columns={columns as any} dataSource={list} loading={loading} pagination={false} />
    <Table rowKey="id" columns={columns as any} dataSource={list} loading={loading} pagination={false} scroll={{ x: 1200 }} />
    <Pagination style={{ marginTop: 16, textAlign: 'right' }} current={pagination.page} pageSize={pagination.pageSize} total={pagination.total} showSizeChanger onChange={load} />
    <Modal open={open} title={detail ? '省代理详情' : editingId ? '编辑省代理' : '新增省代理'} width={760} onCancel={() => setOpen(false)} footer={detail ? null : <><Button onClick={() => setOpen(false)}>取消</Button><Button type="primary" onClick={submit}>确定</Button></>}>
      {detail ? <Descriptions bordered column={2} items={[
        ['代理负责人', detail.agentName], ['负责省份', (detail.regionNames || []).join('、')],
        ['开户银行', detail.bankName], ['银行账号', detail.accountNumber], ['账户名称', detail.accountHolder],
        ['开户支行', detail.branchName], ['联系信息', detail.contactInfo], ['详细地址', detail.address],
      ].map(([label, children]) => ({ label, children: children || '-' }))} /> :
        ['负责人', detail.agentName], ['电话', detail.contactInfo], ['负责省份', (detail.regionNames || []).join('、')],
        ['详细地址', detail.address], ['下级诊所数', detail.clinicCount], ['状态', statusText(detail.status)],
        ['开户银行', detail.bankName], ['银行账号', detail.accountNumber], ['账户名称', detail.accountHolder], ['开户支行', detail.branchName],
      ].map(([label, children]) => ({ label, children: children ?? '-' }))} /> :
        <Form form={form} labelCol={{ span: 5 }}>
          <Form.Item name="agentName" label="代理负责人" rules={[{ required: true, message: '请输入代理负责人' }]}><Input maxLength={50} /></Form.Item>
          <Form.Item name="agentName" label="负责人" rules={[{ required: true, message: '请输入负责人' }]}><Input maxLength={50} /></Form.Item>
          <Form.List name="regions" rules={[{ validator: async (_, value) => { if (!value?.length) throw new Error('请选择负责省份'); } }]}>
            {(fields, { add, remove }, { errors }) => <Form.Item label="负责省份" required>
              {fields.map(({ key, name, ...rest }) => <Space key={key} align="baseline">
                <Form.Item {...rest} name={[name, 'regionCode']} rules={[{ required: true, message: '请选择负责省份' }]}><RegionCascader changeOnSelect /></Form.Item>
                {fields.length > 1 && <Button onClick={() => remove(name)}>删除</Button>}
              </Space>)}
              <Button type="dashed" onClick={() => add({})}>添加负责省份</Button>
              <Form.ErrorList errors={errors} />
              {fields.map(({ key, name, ...rest }) => <Space key={key} align="baseline"><Form.Item {...rest} name={[name, 'regionCode']} rules={[{ required: true, message: '请选择负责省份' }]}><RegionCascader changeOnSelect /></Form.Item>{fields.length > 1 && <Button onClick={() => remove(name)}>删除</Button>}</Space>)}
              <Button type="dashed" onClick={() => add({})}>添加负责省份</Button><Form.ErrorList errors={errors} />
            </Form.Item>}
          </Form.List>
          <Form.Item name="contactInfo" label="电话"><Input maxLength={200} /></Form.Item>
          <Form.Item name="address" label="详细地址"><Input.TextArea rows={3} maxLength={255} /></Form.Item>
          <Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}><Select options={statusOptions} /></Form.Item>
          <Form.Item name="bankName" label="开户银行"><Input maxLength={100} /></Form.Item>
          <Form.Item name="accountNumber" label="银行账号"><Input maxLength={30} /></Form.Item>
          <Form.Item name="accountHolder" label="账户名称"><Input maxLength={50} /></Form.Item>
          <Form.Item name="branchName" label="开户支行"><Input maxLength={100} /></Form.Item>
          <Form.Item name="contactInfo" label="联系信息"><Input maxLength={200} /></Form.Item>
          <Form.Item name="address" label="详细地址"><Input.TextArea rows={3} maxLength={255} /></Form.Item>
        </Form>}
    </Modal>
  </div>;
};
export default AgentPage;