hejianhao
2026-08-28 cc31d303d12f9c7cd2c4574351b67db04069ae0d
828优化功能新需求
2个文件已删除
22个文件已修改
617 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterRegionService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/controller/ClinicController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/ClinicQueryMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/row/ClinicListRow.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/ClinicAppService.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/vo/ClinicListItemVO.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/mapper/ClinicQueryMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterRegionServiceTest.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/admin.zip 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/config/routes.ts 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/app.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/live/broadcast/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin/src/pages/member/clinic/index.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/articleDetail/articleDetail.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/good/projectDetail.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/mine/mine.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/app-version.js 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterRegionService.java
@@ -101,6 +101,9 @@
                }
                List<AppRegisterRegionPickerOptionVO> currentDistrictOptions = new ArrayList<AppRegisterRegionPickerOptionVO>(districts.size());
                for (SysRegion district : districts) {
                    if (shouldHideInAppPicker(district)) {
                        continue;
                    }
                    currentDistrictOptions.add(toPickerOption(district, 3));
                }
                currentAreaOptions.add(currentDistrictOptions);
@@ -117,6 +120,13 @@
    }
    /**
     * APP 地区选择器不展示行政区划占位节点。
     */
    private boolean shouldHideInAppPicker(SysRegion region) {
        return region.getName() != null && region.getName().contains("市辖区");
    }
    /**
     * 将海诊通诊所地区编码或名称归一化为本地三级行政区划。
     */
    public RegionSelection resolveRegionSelection(String provinceCode,
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/register/service/AppRegisterService.java
@@ -184,6 +184,9 @@
        clinic.setBusinessLicenseUrl(trimToNull(request.getBusinessLicenseUrl()));
        clinic.setInviteCode(generateInviteCode());
        clinic.setStatus(1);
        clinic.setAgentId(4L); // 绑定到默认的省代上面
        clinicMapper.insert(clinic);
        AppUser user = new AppUser();
@@ -194,19 +197,24 @@
        user.setClinicId(clinic.getId());
        userMapper.insert(user);
//        Integer provinceCode = parseRegionCode(clinic.getProvinceCode(), "省编码");
//        Integer cityCode = parseRegionCode(clinic.getCityCode(), "市编码");
//        Integer areaCode = parseRegionCode(clinic.getDistrictCode(), "区县编码");
//        log.info("调用微赞创建门店,入参 title={}, provinceCode={}, cityCode={}, areaCode={}, address={}, telephone={}",
//                clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
//        try {
//            JsonNode vzanResponse = vzanHttpUtil.createStore(clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
//            log.info("微赞创建门店成功,出参={}", vzanResponse);
//        } catch (RuntimeException ex) {
//            log.error("微赞创建门店失败,本地事务将回滚,入参 title={}, provinceCode={}, cityCode={}, areaCode={}, address={}, telephone={}",
//                    clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone(), ex);
//            throw new IllegalStateException("微赞创建门店失败", ex);
//        }
        Integer provinceCode = parseRegionCode(clinic.getProvinceCode(), "省编码");
        Integer cityCode = parseRegionCode(clinic.getCityCode(), "市编码");
        Integer areaCode = parseRegionCode(clinic.getDistrictCode(), "区县编码");
        log.info("调用微赞创建门店,入参 title={}, provinceCode={}, cityCode={}, areaCode={}, address={}, telephone={}"
                , clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
        try {
            JsonNode vzanResponse = vzanHttpUtil.createStore(clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
            log.info("微赞创建门店成功,出参={}", vzanResponse);
            // 注册阶段暂未绑定区域代理,parentMobile 传空,由微赞按一级代理处理。
            log.info("调用微赞创建一级代理,入参 agentMobile={}, agentName={}, levelCode=1, storeName={}, parentMobile=18510939636",
                    clinic.getContactPhone(), clinic.getName(), clinic.getName());
            JsonNode vzanAgentResponse = vzanHttpUtil.createAgent(
                    clinic.getContactPhone(), clinic.getName(), 1, "18510939636", clinic.getName());
            log.info("微赞创建一级代理成功,出参={}", vzanAgentResponse);
        } catch (RuntimeException ex) {
            log.error("微赞创建门店或一级代理失败,本地事务将回滚,clinicId={}", clinic.getId(), ex);
            throw new IllegalStateException("微赞门店或一级代理创建失败", ex);
        }
        clearDraft(request.getRegisterTicket());
        return createLoginResponse(user);
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -20,7 +20,7 @@
import java.util.ArrayList;
import java.util.List;
@Component
//@Component
public class DataInitializer implements ApplicationRunner {
    private final SysUserService sysUserService;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/controller/ClinicController.java
@@ -44,10 +44,11 @@
            @RequestParam(required = false) String regionCode,
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String phone,
            @RequestParam(required = false) String agentName,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdFrom,
            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdTo,
            @RequestParam(required = false) Integer status) {
        return ApiResponse.ok(clinicAppService.page(page, pageSize, regionCode, name, phone, createdFrom, createdTo, status));
        return ApiResponse.ok(clinicAppService.page(page, pageSize, regionCode, name, phone, agentName, createdFrom, createdTo, status));
    }
    @GetMapping("/clinics/clinic-import-template")
@@ -65,10 +66,11 @@
                       @RequestParam(required = false) String regionCode,
                       @RequestParam(required = false) String name,
                       @RequestParam(required = false) String phone,
                       @RequestParam(required = false) String agentName,
                       @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdFrom,
                       @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdTo,
                       @RequestParam(required = false) Integer status) throws IOException {
        clinicAppService.export(regionCode, name, phone, createdFrom, createdTo, status, response);
        clinicAppService.export(regionCode, name, phone, agentName, createdFrom, createdTo, status, response);
    }
    @GetMapping("/clinics/regions")
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/ClinicQueryMapper.java
@@ -18,6 +18,7 @@
                                          @Param("regionCode") String regionCode,
                                          @Param("name") String name,
                                          @Param("phone") String phone,
                                          @Param("agentName") String agentName,
                                          @Param("createdFrom") LocalDateTime createdFrom,
                                          @Param("createdTo") LocalDateTime createdTo,
                                          @Param("status") Integer status);
@@ -25,6 +26,7 @@
    List<ClinicListRow> selectClinicList(@Param("regionCode") String regionCode,
                                         @Param("name") String name,
                                         @Param("phone") String phone,
                                         @Param("agentName") String agentName,
                                         @Param("createdFrom") LocalDateTime createdFrom,
                                         @Param("createdTo") LocalDateTime createdTo,
                                         @Param("status") Integer status);
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/row/ClinicListRow.java
@@ -16,6 +16,7 @@
    private String phone;
    private String regionName;
    private Integer status;
    private String agentName;
    private Integer userCount;
    private LocalDateTime createdAt;
    private String businessLicenseUrl;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/ClinicAppService.java
@@ -4,7 +4,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.tcm.training.admin.agent.entity.AgentRegion;
import com.tcm.training.admin.agent.entity.ProvincialAgent;
import com.tcm.training.admin.agent.excel.ClinicAgentImportRow;
@@ -29,7 +28,6 @@
import com.tcm.training.admin.member.vo.ClinicDetailVO;
import com.tcm.training.admin.member.vo.ClinicListItemVO;
import com.tcm.training.admin.member.vo.ClinicRegionOptionVO;
import com.tcm.training.admin.utils.VzanHttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
@@ -54,7 +52,6 @@
    private final ProvincialAgentMapper provincialAgentMapper;
    private final UserMapper userMapper;
    private final SysRegionMapper sysRegionMapper;
    private final VzanHttpUtil vzanHttpUtil;
    private final DataSourceTransactionManager transactionManager;
    public ClinicAppService(ClinicQueryMapper clinicQueryMapper,
@@ -62,14 +59,12 @@
                           ProvincialAgentMapper provincialAgentMapper,
                           UserMapper userMapper,
                           SysRegionMapper sysRegionMapper,
                           VzanHttpUtil vzanHttpUtil,
                           DataSourceTransactionManager transactionManager) {
        this.clinicQueryMapper = clinicQueryMapper;
        this.clinicMapper = clinicMapper;
        this.provincialAgentMapper = provincialAgentMapper;
        this.userMapper = userMapper;
        this.sysRegionMapper = sysRegionMapper;
        this.vzanHttpUtil = vzanHttpUtil;
        this.transactionManager = transactionManager;
    }
@@ -78,6 +73,7 @@
                                               String regionCode,
                                               String name,
                                               String phone,
                                               String agentName,
                                               LocalDateTime createdFrom,
                                               LocalDateTime createdTo,
                                               Integer status) {
@@ -86,6 +82,7 @@
                trimToNull(regionCode),
                trimToNull(name),
                trimToNull(phone),
                trimToNull(agentName),
                createdFrom,
                createdTo,
                status
@@ -177,6 +174,7 @@
    public void export(String regionCode,
                       String name,
                       String phone,
                       String agentName,
                       LocalDateTime createdFrom,
                       LocalDateTime createdTo,
                       Integer status,
@@ -190,6 +188,7 @@
                trimToNull(regionCode),
                trimToNull(name),
                trimToNull(phone),
                trimToNull(agentName),
                createdFrom,
                createdTo,
                status
@@ -307,61 +306,7 @@
            throw e;
        }
        if (request.getAgentId() == null) {
            return detail(id);
        }
        // 事务提交后,调用微赞接口(不回滚本地事务)
        try {
            Clinic clinic = clinicMapper.selectById(id);
            // 调用微赞创建门店
            Integer provinceCode = parseRegionCode(clinic.getProvinceCode(), "省编码");
            Integer cityCode = parseRegionCode(clinic.getCityCode(), "市编码");
            Integer areaCode = parseRegionCode(clinic.getDistrictCode(), "区县编码");
            log.info("调用微赞创建门店,入参 title={}, provinceCode={}, cityCode={}, areaCode={}, address={}, telephone={}",
                    clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
            JsonNode vzanStoreResponse = vzanHttpUtil.createStore(clinic.getName(), provinceCode, cityCode, areaCode, clinic.getAddress(), clinic.getContactPhone());
            log.info("微赞创建门店成功,出参={}", vzanStoreResponse);
            // 查询代理信息获取parentMobile
            ProvincialAgent agent = provincialAgentMapper.selectById(request.getAgentId());
            String parentMobile = agent.getContactInfo();
            if (parentMobile == null || parentMobile.trim().isEmpty()) {
                throw new IllegalArgumentException("代理联系方式为空,agentId=" + request.getAgentId());
            }
            // 调用微赞创建代理
            log.info("调用微赞创建代理,入参 agentMobile={}, agentName={}, levelCode=1, storeName={}, parentMobile={}",
                    clinic.getContactPhone(), clinic.getName(), clinic.getName(), parentMobile);
            JsonNode vzanAgentResponse = vzanHttpUtil.createAgent(
                    clinic.getContactPhone(),  // agentMobile传门店的手机号
                    clinic.getName(),          // agentName传门店的名称
                    1,                        // levelCode传1
                    parentMobile,              // parentMobile传代理的contact_info
                    clinic.getName()          // storeName传门店的name
            );
            log.info("微赞创建代理成功,出参={}", vzanAgentResponse);
        } catch (Exception e) {
            // 捕获微赞接口异常,不回滚本地事务
            log.error("调用微赞接口失败,但本地数据已保存成功,clinicId={}", id, e);
            throw new IllegalStateException("调用微赞失败", e);
        }
        return detail(id);
    }
    private Integer parseRegionCode(String code, String fieldName) {
        if (code == null || code.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + "不能为空");
        }
        try {
            return Integer.parseInt(code);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(fieldName + "格式错误: " + code);
        }
    }
    private void addCode(Set<String> codes, String code) {
@@ -432,6 +377,7 @@
        item.setPhone(row.getPhone() == null ? "-" : PhoneMasker.mask(row.getPhone()));
        item.setRegionName(defaultText(row.getRegionName()));
        item.setStatus(row.getStatus());
        item.setAgentName(defaultText(row.getAgentName()));
        item.setUserCount(row.getUserCount() == null ? 0 : row.getUserCount());
        item.setCreatedAt(row.getCreatedAt());
        item.setBusinessLicenseUrl(trimToNull(row.getBusinessLicenseUrl()));
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/vo/ClinicListItemVO.java
@@ -16,6 +16,7 @@
    private String phone;
    private String regionName;
    private Integer status;
    private String agentName;
    private Integer userCount;
    private LocalDateTime createdAt;
    private String businessLicenseUrl;
tcm-training-admin-backend/src/main/resources/mapper/ClinicQueryMapper.xml
@@ -18,6 +18,9 @@
        <if test="phone != null and phone.trim() != ''">
            AND c.contact_phone LIKE CONCAT('%', #{phone}, '%')
        </if>
        <if test="agentName != null and agentName.trim() != ''">
            AND pa.agent_name LIKE CONCAT('%', #{agentName}, '%')
        </if>
        <if test="createdFrom != null">
            AND c.created_at &gt;= #{createdFrom}
        </if>
@@ -44,10 +47,12 @@
                ELSE NULL
            END AS regionName,
            c.status,
            pa.agent_name AS agentName,
            COUNT(u.id) AS userCount,
            c.created_at AS createdAt,
            c.business_license_url AS businessLicenseUrl
        FROM tcm_clinic c
        LEFT JOIN tcm_provincial_agent pa ON pa.id = c.agent_id AND pa.deleted = 0
        LEFT JOIN tcm_app_user u ON u.clinic_id = c.id AND u.deleted = 0
        <include refid="ClinicWhere"/>
        GROUP BY
@@ -58,6 +63,7 @@
            c.city_name,
            c.district_name,
            c.status,
            pa.agent_name,
            c.created_at,
            c.business_license_url
        ORDER BY c.id DESC
@@ -78,10 +84,12 @@
                ELSE NULL
            END AS regionName,
            c.status,
            pa.agent_name AS agentName,
            COUNT(u.id) AS userCount,
            c.created_at AS createdAt,
            c.business_license_url AS businessLicenseUrl
        FROM tcm_clinic c
        LEFT JOIN tcm_provincial_agent pa ON pa.id = c.agent_id AND pa.deleted = 0
        LEFT JOIN tcm_app_user u ON u.clinic_id = c.id AND u.deleted = 0
        <include refid="ClinicWhere"/>
        GROUP BY
@@ -92,6 +100,7 @@
            c.city_name,
            c.district_name,
            c.status,
            pa.agent_name,
            c.created_at,
            c.business_license_url
        ORDER BY c.id DESC
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterRegionServiceTest.java
@@ -2,6 +2,7 @@
import com.tcm.training.admin.app.register.entity.SysRegion;
import com.tcm.training.admin.app.register.mapper.SysRegionMapper;
import com.tcm.training.admin.app.register.vo.AppRegisterRegionDatasetVO;
import com.tcm.training.admin.app.register.vo.AppRegisterRegionOptionVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -9,6 +10,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -52,6 +54,28 @@
    }
    /**
     * APP 完整地区数据集应过滤区县层级的市辖区占位节点。
     */
    @Test
    void buildRegionDatasetShouldHideMunicipalDistrictPlaceholder() {
        SysRegion province = buildRegion(1L, "江西省", "360000", 0L);
        SysRegion city = buildRegion(2L, "吉安市", "360800", 1L);
        SysRegion placeholder = buildRegion(3L, "吉安市市辖区", "360801", 2L);
        SysRegion district = buildRegion(4L, "吉州区", "360802", 2L);
        when(sysRegionMapper.selectAllOrdered()).thenReturn(
                Arrays.asList(province, city, placeholder, district)
        );
        AppRegisterRegionDatasetVO dataset = appRegisterRegionService.buildRegionDataset();
        assertEquals(1, dataset.getAreas().size());
        assertEquals(1, dataset.getAreas().get(0).size());
        assertEquals(1, dataset.getAreas().get(0).get(0).size());
        assertEquals("吉州区", dataset.getAreas().get(0).get(0).get(0).getLabel());
        assertEquals("360802", dataset.getAreas().get(0).get(0).get(0).getValue());
    }
    /**
     * 海诊通仅返回地区名称时,应通过 sys_region 解析出完整编码。
     */
    @Test
tcm-training-admin/admin.zip
Binary files differ
tcm-training-admin/config/routes.ts
@@ -192,22 +192,28 @@
            component: './member/user/redirect',
          },
          {
            name: '用户管理',
            path: '/member/user/manage',
            access: 'member:user:view',
            component: './member/user',
            name: '省代用户',
            path: '/member/user/agent',
            access: 'member:agent:view',
            component: './member/agent',
          },
          {
            name: '诊所管理',
            name: '诊所用户',
            path: '/member/user/clinic',
            access: 'member:clinic:view',
            component: './member/clinic',
          },
          {
            name: '省代理管理',
            path: '/member/user/agent',
            access: 'member:agent:view',
            component: './member/agent',
            name: '普通用户',
            path: '/member/user/manage',
            access: 'member:user:view',
            component: './member/user',
          },
          {
            name: '会员用户',
            path: '/member/user/member-users',
            access: 'member:user:member:view',
            component: './member/user/member-users',
          },
          {
            name: '编辑诊所',
@@ -222,12 +228,6 @@
            hideInMenu: true,
            access: 'member:clinic:view',
            component: './member/clinic/detail',
          },
          {
            name: '会员用户',
            path: '/member/user/member-users',
            access: 'member:user:member:view',
            component: './member/user/member-users',
          },
          {
            name: '会员用户详情',
@@ -273,26 +273,21 @@
  },
  {
    path: '/live',
    name: '课程与学习',
    name: '内容',
    icon: 'read',
    access: 'canViewLiveModule',
    routes: [
      {
        path: '/live',
        redirect: '/live/course',
        redirect: '/live/course/broadcast',
      },
      {
        path: '/live/expert',
        redirect: '/live/course/expert',
      },
      {
        path: '/live/course',
        name: '课程管理',
        name: '类直播',
        routes: [
          {
            path: '/live/course',
            redirect: '/live/course/broadcast',
          },
          {
            name: '养生直播',
            path: '/live/course/broadcast',
@@ -314,36 +309,67 @@
            component: './live/broadcast/edit',
          },
          {
            name: '微赞直播间设置',
            path: '/live/course/live-room-setting',
            access: 'live:room-setting:view',
            component: './live/live-room-setting',
          },          {
            name: '直播预约记录',
            name: '预约记录',
            path: '/live/course/reserve-records',
            access: 'live:reserve:view',
            component: './live/reserve-records',
          },
          {
            name: '直播观看记录',
            name: '观看记录',
            path: '/live/course/watch-records',
            access: 'live:watch:view',
            component: './live/watch-records',
          },
          {
            name: '分类管理',
            name: '直播预约记录跳转',
            path: '/live/course/broadcast/:courseId/sessions/:sessionId/reserves',
            hideInMenu: true,
            access: 'live:reserve:view',
            component: './live/record-redirect',
          },
          {
            name: '直播观看记录跳转',
            path: '/live/course/broadcast/:courseId/sessions/:sessionId/watch-records',
            hideInMenu: true,
            access: 'live:watch:view',
            component: './live/record-redirect',
          },
        ],
      },
      {
        name: '微赞直播',
        routes: [
          {
            name: '直播设置',
            path: '/live/course/live-room-setting',
            access: 'live:room-setting:view',
            component: './live/live-room-setting',
          },
        ],
      },
      {
        name: '课程',
        routes: [
          {
            name: '添加分类',
            path: '/live/course/category',
            access: 'live:category:view',
            component: './live/category',
          },
          {
            name: '养生课程',
            name: '添加专家',
            path: '/live/course/expert',
            access: 'live:expert:view',
            component: './live/expert',
          },
          {
            name: '添加课堂',
            path: '/live/course/health',
            access: 'live:health-course:view',
            component: './live/health',
          },
          {
            name: '视频与课程资源管理',
            name: '添加视频',
            path: '/live/course/video-resources',
            access: 'live:health-course:view',
            component: './live/video-resources',
@@ -397,61 +423,11 @@
            access: 'live:health-course:video:edit',
            component: './live/health/video-manage',
          },
          {
            name: '直播预约记录跳转',
            path: '/live/course/broadcast/:courseId/sessions/:sessionId/reserves',
            hideInMenu: true,
            access: 'live:reserve:view',
            component: './live/record-redirect',
          },
          {
            name: '直播观看记录跳转',
            path: '/live/course/broadcast/:courseId/sessions/:sessionId/watch-records',
            hideInMenu: true,
            access: 'live:watch:view',
            component: './live/record-redirect',
          },
          {
            name: '授课专家管理',
            path: '/live/course/expert',
            access: 'live:expert:view',
            component: './live/expert',
          },
          {
            path: '/live/course/project-introduction/create',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction/create',
          },
          {
            path: '/live/course/project-introduction',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction',
          },
          {
            path: '/live/course/project-introduction-password',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction-password',
          },
          {
            path: '/live/course/health-popularization/create',
            hideInMenu: true,
            redirect: '/live/promotion/health-popularization/create',
          },
          {
            path: '/live/course/health-popularization',
            hideInMenu: true,
            redirect: '/live/promotion/health-popularization',
          },
        ],
      },
      {
        path: '/live/promotion',
        name: '宣传知识',
        name: '科普',
        routes: [
          {
            path: '/live/promotion',
            redirect: '/live/promotion/project-introduction',
          },
          {
            name: '健康知识',
            path: '/live/promotion/health-popularization',
@@ -492,6 +468,17 @@
            access: 'live:health-promotion:edit',
            component: './live/health-promotion/manage',
          },
        ],
      },
      {
        name: '项目',
        routes: [
          {
            name: '项目密码',
            path: '/live/promotion/project-introduction-password',
            access: 'live:project-introduction:view',
            component: './live/project-introduction-password',
          },
          {
            name: '项目介绍',
            path: '/live/promotion/project-introduction',
@@ -513,10 +500,29 @@
            component: './live/project-introduction/manage',
          },
          {
            name: '项目介绍密码',
            path: '/live/promotion/project-introduction-password',
            access: 'live:project-introduction:view',
            component: './live/project-introduction-password',
            path: '/live/course/project-introduction/create',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction/create',
          },
          {
            path: '/live/course/project-introduction',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction',
          },
          {
            path: '/live/course/project-introduction-password',
            hideInMenu: true,
            redirect: '/live/promotion/project-introduction-password',
          },
          {
            path: '/live/course/health-popularization/create',
            hideInMenu: true,
            redirect: '/live/promotion/health-popularization/create',
          },
          {
            path: '/live/course/health-popularization',
            hideInMenu: true,
            redirect: '/live/promotion/health-popularization',
          },
        ],
      },
tcm-training-admin/src/app.tsx
@@ -1,4 +1,4 @@
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import type { MenuDataItem, Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max';
import { history } from '@umijs/max';
@@ -26,6 +26,16 @@
  loading?: boolean;
};
const ensureMenuKeys = (menuData: MenuDataItem[], parentKey = 'menu'): MenuDataItem[] =>
  menuData.map((item, index) => {
    const key = item.key || item.path || `${parentKey}-${String(item.name || index)}`;
    return {
      ...item,
      key,
      children: item.children ? ensureMenuKeys(item.children, String(key)) : item.children,
    };
  });
/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
@@ -79,6 +89,7 @@
      );
    },
    ...initialState?.settings,
    menuDataRender: ensureMenuKeys,
    pageTitleRender: () => '管理后台',
  };
};
tcm-training-admin/src/pages/live/broadcast/index.tsx
@@ -170,7 +170,7 @@
        <Space size={16} className="live-broadcast-table__actions">
          {canDetail ? (
            <button type="button" onClick={() => history.push(`/live/course/broadcast/${record.id}`)}>
              详情
              类直播
            </button>
          ) : null}
          {canEdit ? (
tcm-training-admin/src/pages/member/clinic/index.tsx
@@ -12,6 +12,7 @@
  regionCode?: string[];
  name?: string;
  phone?: string;
  agentName?: string;
  status?: number;
  createdRange?: Dayjs[];
};
@@ -22,6 +23,7 @@
  phone?: string;
  regionName?: string;
  status: number;
  agentName?: string;
  userCount: number;
  createdAt: string;
  businessLicenseUrl?: string;
@@ -34,6 +36,7 @@
  regionCode: getSelectedRegionCode(values.regionCode),
  name: values.name?.trim() || undefined,
  phone: values.phone?.trim() || undefined,
  agentName: values.agentName?.trim() || undefined,
  status: values.status,
  createdFrom: values.createdRange?.[0]
    ? dayjs(values.createdRange[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss')
@@ -145,6 +148,12 @@
      render: (value) => (value === 1 ? '启用' : value === 2 ? '注销' : '禁用'),
    },
    {
      title: '省代',
      dataIndex: 'agentName',
      width: 140,
      render: (value) => value || '-',
    },
    {
      title: '注册时间',
      dataIndex: 'createdAt',
      width: 180,
@@ -197,6 +206,9 @@
                ]}
              />
            </Form.Item>
            <Form.Item label="省代" name="agentName">
              <Input placeholder="请输入" allowClear />
            </Form.Item>
            <Form.Item label="注册时间" name="createdRange">
              <DatePicker.RangePicker className="member-clinic-search-form__range" />
            </Form.Item>
traditionalApp/App.vue
@@ -11,10 +11,6 @@
    import { cacheInviteCode } from '@/utils/invite-cache.js'
    import { initJPushLifecycle } from '@/utils/push.js'
    import {
        checkAppVersionUpdate,
        resumeForceUpdatePrompt
    } from '@/utils/app-version.js'
    import {
        getOpeninstallInstallData,
        initOpeninstall,
        registerOpeninstallWakeUp
@@ -30,7 +26,6 @@
        initOpeninstall()
        registerOpeninstallWakeUp(handleOpeninstallPayload)
        getOpeninstallInstallData(handleOpeninstallPayload)
        checkAppVersionUpdate()
        // #endif
    })
@@ -39,9 +34,6 @@
     */
    onShow(() => {
        handleOpenByScheme()
        // #ifdef APP-PLUS
        resumeForceUpdatePrompt()
        // #endif
    })
    /**
traditionalApp/config/index.js
@@ -2,7 +2,6 @@
//     BASE_URL: 'http://221.182.45.100:48080',
    BASE_URL: 'https://haizhentong.com',
    OPENINSTALL_URL: 'https://m.malink.cn/s/7bIzeu',
    APP_STORE_URL: '',
    uploadUrl: "https://haizhentong.com/api/open/file/upload",
    imageUrl: 'http://221.182.45.100:11224/images/'
};
traditionalApp/manifest.json
@@ -3,8 +3,8 @@
    "name" : "海诊康道",
    "appid" : "__UNI__7B5B0CA",
    "description" : "",
    "versionName" : "1.0.23",
    "versionCode" : 122,
    "versionName" : "1.0.0",
    "versionCode" : 100,
    "transformPx" : false,
    /* 5+App特有相关 */
    "app-plus" : {
traditionalApp/pages/articleDetail/articleDetail.vue
@@ -21,7 +21,7 @@
        <!-- 内容区域 -->
        <view class="content-area">
            <text class="content-title">{{ detail.title || '健康科普详情' }}</text>
            <text v-if="isVideoType" class="content-title">{{ detail.title || '健康科普详情' }}</text>
            <text v-if="isVideoType && videoSubtitle" class="content-subtitle">{{ videoSubtitle }}</text>
            <rich-text v-if="!isVideoType" :nodes="contentNodes" class="rich-content"></rich-text>
            <view v-if="loading" class="status-text">加载中...</view>
@@ -288,7 +288,6 @@
    }
    .rich-content {
        margin-top: 24rpx;
        width: 100%;
        font-size: 28rpx;
        line-height: 1.8;
traditionalApp/pages/good/projectDetail.vue
@@ -6,7 +6,7 @@
        <!-- 导航栏 -->
        <view class="nav-bar">
            <image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
            <text class="nav-title">{{ detail.title || '详情' }}</text>
            <text class="nav-title">详情</text>
            <view class="nav-right-placeholder"></view>
        </view>
@@ -34,8 +34,6 @@
                <!-- 图文内容 -->
                <view v-if="detail.contentType === 1">
                    <!-- 标题 -->
                    <view class="detail-title">{{ detail.title }}</view>
                    <!-- 内容 -->
                    <view v-if="detail.content" class="html-content">
                        <rich-text :nodes="detail.content"></rich-text>
traditionalApp/pages/home/home.vue
@@ -376,7 +376,7 @@
            const userAgent = res?.data?.ua?.trim();
            console.log('[goWeiZanLive] 解析目标URL:', targetUrl);
            if (res?.code !== 0 || !targetUrl || !userAgent) {
            if (res?.code !== 0 || !targetUrl) {
                console.log('[goWeiZanLive] 获取跳转地址失败,code:', res?.code, 'msg:', res?.msg);
                uni.showToast({ title: res?.msg || '获取直播间地址失败', icon: 'none' });
                return;
@@ -1091,12 +1091,14 @@
            .course-info {
                flex: 1;
                min-width: 0;
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                .course-top {
                    display: flex;
                    min-width: 0;
                    align-items: center;
                    margin-top: 4rpx;
@@ -1114,6 +1116,7 @@
                        font-weight: bold;
                        color: #333;
                        flex: 1;
                        min-width: 0;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        white-space: nowrap;
traditionalApp/pages/login/login.vue
@@ -166,13 +166,9 @@
    })
    /**
     * 关闭登录页,优先返回上一页,没有页面栈时回首页。
     * 关闭登录页统一回到首页,避免从需要登录的 tabbar 页面返回后再次触发登录拦截。
     */
    const handleClose = () => {
        if (getCurrentPages().length > 1) {
            uni.navigateBack()
            return
        }
        uni.switchTab({
            url: '/pages/home/home'
        })
traditionalApp/pages/mine/mine.vue
@@ -36,7 +36,7 @@
            </view>
            <view class="list-section card-box">
                <view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="goPage(item)">
                <view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="handleBottomMenuClick(item)">
                    <view class="list-left">
                        <image class="list-icon" :class="item.iconClass" :src="item.icon" mode="aspectFit"></image>
                        <text class="list-title">{{ item.title }}</text>
@@ -55,7 +55,6 @@
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getNoticeList } from '../notice/service'
import { getUserCenter } from './service'
import { getRuntimeVersionName } from '@/utils/app-version.js'
import { hasStoredSession } from '@/utils/auth-session.js'
import manifestConfig from '../../manifest.json'
@@ -104,7 +103,7 @@
const statusBarHeight = ref(0)
const unreadNoticeTotal = ref(0)
const manifestVersionName = String(manifestConfig.versionName || '')
const appVersionText = ref(manifestVersionName)
const appVersionText = manifestVersionName
const userCenter = ref<UserCenterData>(createDefaultUserCenter())
const userRole = computed<UserRole>(() => {
    return isDoctorLikeUser(userCenter.value) ? 'doctor' : 'personal'
@@ -216,7 +215,6 @@
onLoad(() => {
    const systemInfo = uni.getSystemInfoSync()
    statusBarHeight.value = systemInfo.statusBarHeight || 20
    resolveAppVersion()
    syncCachedUserInfo()
})
@@ -384,6 +382,13 @@
}
/**
 * 处理底部普通菜单跳转。
 */
async function handleBottomMenuClick(item: MenuItem) {
    goPage(item)
}
/**
 * 统一跳转个人中心相关页面。
 */
function goPage(target: string | MenuItem) {
@@ -413,17 +418,6 @@
            url: '/pages/login/login',
        })
    }, 200)
}
/**
 * APP 环境读取当前安装包版本,非 APP 环境使用 manifest 版本兜底。
 */
function resolveAppVersion() {
    getRuntimeVersionName(manifestVersionName).then((versionName) => {
        appVersionText.value = versionName || manifestVersionName
    }).catch(() => {
        appVersionText.value = manifestVersionName
    })
}
/**
@@ -710,6 +704,7 @@
                color: #1A1A1A;
                margin-left: 22rpx;
            }
        }
    }
}
traditionalApp/utils/app-version.js
File was deleted