“mitao”
2026-04-07 4ba9020b7c67cc318448a8aeac741492b23b4189
feat:APP端验证码登录及联调
1个文件已删除
16个文件已添加
8个文件已修改
17713 ■■■■■ 已修改文件
README.md 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/haizhentong.sql 16313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppSmsAuthProperties.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppSmsCodeSendRequest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppSmsLoginRequest.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppSmsCodeService.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppSmsSender.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/LoggingAppSmsSender.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppLoginResponse.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppLoginUserVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppSmsCodeSendResponse.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/statistics/controller/UserEntryLogController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/UserFeedbackController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/config/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/service.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/request.js 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md
@@ -1,4 +1,102 @@
## tcm-training
# tcm-training
中医在线培训
中医在线培训项目仓库,当前包含后台管理后端、后台管理前端,以及新增的 APP 端 `uni-app` 工程。
## 项目结构
```text
tcm-training/
├── tcm-training-admin-backend/   # Spring Boot 2.7 后端
├── tcm-training-admin/           # React 18 + Umi 管理后台
├── traditionalApp/               # uni-app APP 前端工程
├── doc/                          # 业务文档与原型说明
└── AGENTS.md                     # 仓库协作说明
```
## 子项目说明
### 1. `tcm-training-admin-backend`
- 技术栈:Spring Boot 2.7.18、MyBatis-Plus、Sa-Token、Redis、MySQL
- 主要代码目录:`src/main/java/com/tcm/training/admin/`
- 配置目录:`src/main/resources/`
- SQL 初始化脚本:`sql/init.sql`
常用命令:
```bash
cd "tcm-training-admin-backend"
mvn -DskipTests spring-boot:run
```
```bash
cd "tcm-training-admin-backend"
mvn test
```
### 2. `tcm-training-admin`
- 技术栈:React 18、Umi Max、Ant Design Pro、Jest、TypeScript
- 主要代码目录:`src/`
- 配置目录:`config/`
常用命令:
```bash
cd "tcm-training-admin"
npm install
npm run start:dev
```
```bash
cd "tcm-training-admin"
npm run build:prod
npm run test
npm run prettier
npm run tsc
```
### 3. `traditionalApp`
- 技术栈:`uni-app`、Vue 3、Pinia、`uview-pro`
- 入口文件:`main.js`、`App.vue`
- 页面配置:`pages.json`
- 应用配置:`manifest.json`
- 页面目录:`pages/`
- 公共组件目录:`components/`
- 公共工具目录:`utils/`
说明:
- 当前工程为标准 `uni-app` 项目结构,适合使用 HBuilderX 导入和运行。
- `traditionalApp/package.json` 目前是 `uni_modules` 组件包元数据,不是常规的工程脚本入口;因此仓库内没有统一的 `npm run dev`/`npm run build` 命令。
- `manifest.json` 已包含 App 打包配置,同时配置了微信小程序相关字段,后续若扩展多端发布,请以该文件为准维护。
推荐开发方式:
1. 使用 HBuilderX 打开 `traditionalApp/`
2. 通过 HBuilderX 运行到浏览器、模拟器或真机
3. 如需补充 npm 化构建流程,再单独整理脚本与依赖管理方案
## 开发建议
- 优先按子项目分别安装依赖,避免在仓库根目录执行包管理命令。
- 不要提交 `node_modules/`、`target/`、`unpackage/`、运行日志和本地环境配置。
- 前后端联调时,建议先启动后端,再启动管理后台或 APP 端。
- 每个函数或方法都需要编写注释,说明用途、参数或关键行为。
- 关键业务代码必须补充注释,重点解释业务规则、分支原因和约束条件。
- 代码注释语言应与所在文件现有注释语言保持一致。
## 文档目录
- `doc/`:业务需求、原型说明、字段文档
- `AGENTS.md`:面向协作代理/自动化工具的仓库约定
## 当前状态说明
仓库当前实际生效的前端包含两套:
- `tcm-training-admin/`:管理后台前端
- `traditionalApp/`:APP 端前端(新增 `uni-app` 工程)
后续新增子项目时,请同步更新本文件与 `AGENTS.md`,确保目录和启动方式始终与仓库一致。
tcm-training-admin-backend/sql/haizhentong.sql
New file
Diff too large
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppSmsAuthProperties.java
New file
@@ -0,0 +1,58 @@
package com.tcm.training.admin.app.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * APP 短信验证码登录配置。
 */
@Component
@ConfigurationProperties(prefix = "app.auth.sms")
public class AppSmsAuthProperties {
    private int expireSeconds = 300;
    private boolean debugEnabled = false;
    private String debugCode = "";
    /**
     * 获取验证码有效期秒数。
     */
    public int getExpireSeconds() {
        return expireSeconds;
    }
    /**
     * 设置验证码有效期秒数。
     */
    public void setExpireSeconds(int expireSeconds) {
        this.expireSeconds = expireSeconds;
    }
    /**
     * 获取是否启用开发调试验证码。
     */
    public boolean isDebugEnabled() {
        return debugEnabled;
    }
    /**
     * 设置是否启用开发调试验证码。
     */
    public void setDebugEnabled(boolean debugEnabled) {
        this.debugEnabled = debugEnabled;
    }
    /**
     * 获取开发调试验证码。
     */
    public String getDebugCode() {
        return debugCode;
    }
    /**
     * 设置开发调试验证码。
     */
    public void setDebugCode(String debugCode) {
        this.debugCode = debugCode;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java
New file
@@ -0,0 +1,49 @@
package com.tcm.training.admin.app.auth.controller;
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
import com.tcm.training.admin.app.auth.dto.AppSmsLoginRequest;
import com.tcm.training.admin.app.auth.service.AppAuthService;
import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import com.tcm.training.admin.common.ApiResponse;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
 * APP 登录认证接口。
 */
@Validated
@RestController
@RequestMapping({"/api/app/auth", "/api/v1/app/auth"})
public class AppAuthController {
    private final AppAuthService appAuthService;
    /**
     * 注入 APP 登录服务。
     */
    public AppAuthController(AppAuthService appAuthService) {
        this.appAuthService = appAuthService;
    }
    /**
     * 发送 APP 登录短信验证码。
     */
    @PostMapping("/sms-code")
    public ApiResponse<AppSmsCodeSendResponse> sendSmsCode(@Valid @RequestBody AppSmsCodeSendRequest request) {
        return ApiResponse.ok(appAuthService.sendLoginCode(request));
    }
    /**
     * 使用短信验证码完成 APP 登录。
     */
    @PostMapping("/sms-login")
    public ApiResponse<AppLoginResponse> smsLogin(@Valid @RequestBody AppSmsLoginRequest request) {
        return ApiResponse.ok(appAuthService.smsLogin(request));
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppSmsCodeSendRequest.java
New file
@@ -0,0 +1,19 @@
package com.tcm.training.admin.app.auth.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
 * APP 发送短信验证码请求。
 */
@Getter
@Setter
@NoArgsConstructor
public class AppSmsCodeSendRequest {
    @NotBlank(message = "请输入正确的手机号")
    private String phone;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppSmsLoginRequest.java
New file
@@ -0,0 +1,22 @@
package com.tcm.training.admin.app.auth.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
 * APP 短信验证码登录请求。
 */
@Getter
@Setter
@NoArgsConstructor
public class AppSmsLoginRequest {
    @NotBlank(message = "请输入正确的手机号")
    private String phone;
    @NotBlank(message = "请输入验证码")
    private String code;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
New file
@@ -0,0 +1,136 @@
package com.tcm.training.admin.app.auth.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
import com.tcm.training.admin.app.auth.dto.AppSmsLoginRequest;
import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
import com.tcm.training.admin.app.auth.vo.AppLoginUserVO;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import cn.dev33.satoken.stp.SaTokenInfo;
import org.springframework.stereotype.Service;
import java.util.regex.Pattern;
/**
 * APP 短信验证码登录服务。
 */
@Service
public class AppAuthService {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
    private final UserMapper userMapper;
    private final AppSmsCodeService appSmsCodeService;
    /**
     * 注入 APP 用户查询能力与短信验证码服务。
     */
    public AppAuthService(UserMapper userMapper, AppSmsCodeService appSmsCodeService) {
        this.userMapper = userMapper;
        this.appSmsCodeService = appSmsCodeService;
    }
    /**
     * 为已注册 APP 用户生成登录验证码。
     */
    public AppSmsCodeSendResponse sendLoginCode(AppSmsCodeSendRequest request) {
        String phone = normalizePhone(request.getPhone());
        requireEnabledUserByPhone(phone);
        return appSmsCodeService.createLoginCode(phone);
    }
    /**
     * 使用短信验证码完成 APP 用户登录。
     */
    public AppLoginResponse smsLogin(AppSmsLoginRequest request) {
        String phone = normalizePhone(request.getPhone());
        AppUser user = requireEnabledUserByPhone(phone);
        appSmsCodeService.validateLoginCode(phone, normalizeCode(request.getCode()));
        StpAppUserUtil.login(user.getId());
        SaTokenInfo tokenInfo = StpAppUserUtil.getTokenInfo();
        return new AppLoginResponse(
                tokenInfo.getTokenValue(),
                "Bearer",
                tokenInfo.getTokenTimeout(),
                buildLoginUser(user)
        );
    }
    /**
     * 将 APP 用户实体转换为登录响应中的用户摘要。
     */
    private AppLoginUserVO buildLoginUser(AppUser user) {
        AppLoginUserVO loginUser = new AppLoginUserVO();
        loginUser.setId(user.getId());
        loginUser.setNickname(resolveNickname(user));
        loginUser.setPhone(user.getPhone());
        loginUser.setAvatar(user.getAvatar());
        loginUser.setType(user.getType());
        loginUser.setStatus(user.getStatus());
        loginUser.setClinicId(user.getClinicId());
        loginUser.setMemberExpireAt(user.getMemberExpireAt());
        return loginUser;
    }
    /**
     * 按手机号查询并校验 APP 用户状态。
     */
    private AppUser requireEnabledUserByPhone(String phone) {
        AppUser user = userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getPhone, phone)
                .eq(AppUser::getDeleted, 0)
                .last("limit 1"));
        if (user == null) {
            throw new IllegalArgumentException("账号不存在,请先注册");
        }
        if (user.getStatus() == null || user.getStatus() != 1) {
            throw new IllegalArgumentException("账号已被禁用");
        }
        return user;
    }
    /**
     * 归一化并校验手机号格式。
     */
    private String normalizePhone(String phone) {
        String normalized = trimToNull(phone);
        if (normalized == null || !PHONE_PATTERN.matcher(normalized).matches()) {
            throw new IllegalArgumentException("请输入正确的手机号");
        }
        return normalized;
    }
    /**
     * 归一化并校验验证码。
     */
    private String normalizeCode(String code) {
        String normalized = trimToNull(code);
        if (normalized == null) {
            throw new IllegalArgumentException("请输入验证码");
        }
        return normalized;
    }
    /**
     * 优先使用昵称,缺失时回退到手机号作为展示名称。
     */
    private String resolveNickname(AppUser user) {
        String nickname = trimToNull(user.getNickname());
        return nickname == null ? user.getPhone() : nickname;
    }
    /**
     * 将空白字符串归一化为 null。
     */
    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppSmsCodeService.java
New file
@@ -0,0 +1,94 @@
package com.tcm.training.admin.app.auth.service;
import com.tcm.training.admin.app.auth.config.AppSmsAuthProperties;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
 * APP 登录短信验证码服务,负责验证码的生成、缓存和校验。
 */
@Service
public class AppSmsCodeService {
    private static final String LOGIN_CODE_CACHE_PREFIX = "app:sms:login:";
    private final StringRedisTemplate stringRedisTemplate;
    private final AppSmsSender appSmsSender;
    private final AppSmsAuthProperties appSmsAuthProperties;
    /**
     * 注入验证码缓存、发送器与调试配置。
     */
    public AppSmsCodeService(StringRedisTemplate stringRedisTemplate,
                             AppSmsSender appSmsSender,
                             AppSmsAuthProperties appSmsAuthProperties) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.appSmsSender = appSmsSender;
        this.appSmsAuthProperties = appSmsAuthProperties;
    }
    /**
     * 生成并发送登录验证码,同时写入缓存。
     */
    public AppSmsCodeSendResponse createLoginCode(String phone) {
        String code = generateCode();
        stringRedisTemplate.opsForValue().set(
                buildCacheKey(phone),
                code,
                appSmsAuthProperties.getExpireSeconds(),
                TimeUnit.SECONDS
        );
        appSmsSender.sendLoginCode(phone, code);
        return new AppSmsCodeSendResponse(appSmsAuthProperties.getExpireSeconds());
    }
    /**
     * 校验登录验证码,成功后销毁缓存中的一次性验证码。
     */
    public void validateLoginCode(String phone, String code) {
        if (matchesDebugCode(code)) {
            // 开发环境允许固定验证码直通,便于前后端联调。
            return;
        }
        String cacheKey = buildCacheKey(phone);
        String cachedCode = stringRedisTemplate.opsForValue().get(cacheKey);
        if (cachedCode == null || cachedCode.trim().isEmpty()) {
            throw new IllegalArgumentException("验证码已失效,请重新获取");
        }
        if (!cachedCode.equals(code)) {
            throw new IllegalArgumentException("验证码错误");
        }
        // 验证码登录成功后立即删除,避免重复使用。
        stringRedisTemplate.delete(cacheKey);
    }
    /**
     * 生成 6 位数字验证码。
     */
    private String generateCode() {
        int code = ThreadLocalRandom.current().nextInt(100000, 1000000);
        return String.valueOf(code);
    }
    /**
     * 判断是否命中本地调试验证码。
     */
    private boolean matchesDebugCode(String code) {
        if (!appSmsAuthProperties.isDebugEnabled()) {
            return false;
        }
        String debugCode = appSmsAuthProperties.getDebugCode();
        return debugCode != null && !debugCode.trim().isEmpty() && debugCode.equals(code);
    }
    /**
     * 构造手机号维度的验证码缓存 key。
     */
    private String buildCacheKey(String phone) {
        return LOGIN_CODE_CACHE_PREFIX + phone;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppSmsSender.java
New file
@@ -0,0 +1,12 @@
package com.tcm.training.admin.app.auth.service;
/**
 * 短信发送抽象,后续接入具体供应商时仅替换实现。
 */
public interface AppSmsSender {
    /**
     * 发送 APP 登录验证码。
     */
    void sendLoginCode(String phone, String code);
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/LoggingAppSmsSender.java
New file
@@ -0,0 +1,23 @@
package com.tcm.training.admin.app.auth.service;
import com.tcm.training.admin.common.PhoneMasker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
 * 当前阶段的短信发送占位实现,仅记录日志用于联调调试。
 */
@Service
public class LoggingAppSmsSender implements AppSmsSender {
    private static final Logger log = LoggerFactory.getLogger(LoggingAppSmsSender.class);
    /**
     * 记录验证码日志,待供应商确定后替换为真实发送逻辑。
     */
    @Override
    public void sendLoginCode(String phone, String code) {
        log.info("APP登录验证码已生成,手机号={}, code={}", PhoneMasker.mask(phone), code);
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/support/StpAppUserUtil.java
New file
@@ -0,0 +1,57 @@
package com.tcm.training.admin.app.auth.support;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpLogic;
/**
 * APP 用户独立登录态工具,避免与后台管理员 token 混用。
 */
public final class StpAppUserUtil {
    private static final StpLogic STP_LOGIC = new StpLogic("app-user");
    private StpAppUserUtil() {
    }
    /**
     * 为 APP 用户建立登录态。
     */
    public static void login(Object loginId) {
        STP_LOGIC.login(loginId);
    }
    /**
     * 校验当前请求是否已登录 APP 用户。
     */
    public static void checkLogin() {
        STP_LOGIC.checkLogin();
    }
    /**
     * 判断当前请求是否存在 APP 登录态。
     */
    public static boolean isLogin() {
        return STP_LOGIC.isLogin();
    }
    /**
     * 获取当前 APP 用户登录 ID。
     */
    public static Long getLoginIdAsLong() {
        return STP_LOGIC.getLoginIdAsLong();
    }
    /**
     * 获取当前 APP 登录态的 token 信息。
     */
    public static SaTokenInfo getTokenInfo() {
        return STP_LOGIC.getTokenInfo();
    }
    /**
     * 注销当前 APP 登录态。
     */
    public static void logout() {
        STP_LOGIC.logout();
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppLoginResponse.java
New file
@@ -0,0 +1,30 @@
package com.tcm.training.admin.app.auth.vo;
import lombok.Getter;
import lombok.Setter;
/**
 * APP 登录响应。
 */
@Getter
@Setter
public class AppLoginResponse {
    private String token;
    private String tokenType;
    private long expiresIn;
    private AppLoginUserVO user;
    public AppLoginResponse() {
    }
    /**
     * 使用完整字段构造 APP 登录响应。
     */
    public AppLoginResponse(String token, String tokenType, long expiresIn, AppLoginUserVO user) {
        this.token = token;
        this.tokenType = tokenType;
        this.expiresIn = expiresIn;
        this.user = user;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppLoginUserVO.java
New file
@@ -0,0 +1,23 @@
package com.tcm.training.admin.app.auth.vo;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
 * APP 登录成功后返回的用户摘要。
 */
@Getter
@Setter
public class AppLoginUserVO {
    private Long id;
    private String nickname;
    private String phone;
    private String avatar;
    private Integer type;
    private Integer status;
    private Long clinicId;
    private LocalDateTime memberExpireAt;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/vo/AppSmsCodeSendResponse.java
New file
@@ -0,0 +1,24 @@
package com.tcm.training.admin.app.auth.vo;
import lombok.Getter;
import lombok.Setter;
/**
 * APP 发送验证码响应。
 */
@Getter
@Setter
public class AppSmsCodeSendResponse {
    private int expireInSeconds;
    public AppSmsCodeSendResponse() {
    }
    /**
     * 使用验证码有效期构造响应。
     */
    public AppSmsCodeSendResponse(int expireInSeconds) {
        this.expireInSeconds = expireInSeconds;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/statistics/controller/AppUserEntryLogController.java
New file
@@ -0,0 +1,36 @@
package com.tcm.training.admin.app.statistics.controller;
import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.statistics.service.AppUserEntryLogAppService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * APP 用户进入记录接口。
 */
@Validated
@RestController
@RequestMapping({"/api/app", "/api/v1/app"})
public class AppUserEntryLogController {
    private final AppUserEntryLogAppService appUserEntryLogAppService;
    /**
     * 注入 APP 用户进入记录服务。
     */
    public AppUserEntryLogController(AppUserEntryLogAppService appUserEntryLogAppService) {
        this.appUserEntryLogAppService = appUserEntryLogAppService;
    }
    /**
     * 记录 APP 用户进入行为。
     */
    @PostMapping("/user-entry-logs")
    public ApiResponse<Void> record() {
        appUserEntryLogAppService.record(StpAppUserUtil.getLoginIdAsLong());
        return ApiResponse.ok(null);
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/system/controller/AppVersionController.java
New file
@@ -0,0 +1,37 @@
package com.tcm.training.admin.app.system.controller;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.system.service.AppVersionSettingsAppService;
import com.tcm.training.admin.system.vo.AppVersionCheckVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
 * APP 版本检查接口。
 */
@Validated
@RestController
@RequestMapping({"/api/app/version", "/api/v1/app/version"})
public class AppVersionController {
    private final AppVersionSettingsAppService appVersionSettingsAppService;
    /**
     * 注入 APP 版本检查服务。
     */
    public AppVersionController(AppVersionSettingsAppService appVersionSettingsAppService) {
        this.appVersionSettingsAppService = appVersionSettingsAppService;
    }
    /**
     * 检查 APP 是否存在可升级版本。
     */
    @GetMapping("/check")
    public ApiResponse<AppVersionCheckVO> checkUpdate(@RequestParam String platform,
                                                      @RequestParam Integer currentVersionCode) {
        return ApiResponse.ok(appVersionSettingsAppService.checkUpdate(platform, currentVersionCode));
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserFeedbackController.java
New file
@@ -0,0 +1,40 @@
package com.tcm.training.admin.app.user.controller;
import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.system.dto.UserFeedbackSubmitRequest;
import com.tcm.training.admin.system.service.UserFeedbackAppService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
 * APP 用户意见反馈接口。
 */
@Validated
@RestController
@RequestMapping({"/api/app", "/api/v1/app"})
public class AppUserFeedbackController {
    private final UserFeedbackAppService userFeedbackAppService;
    /**
     * 注入 APP 用户反馈服务。
     */
    public AppUserFeedbackController(UserFeedbackAppService userFeedbackAppService) {
        this.userFeedbackAppService = userFeedbackAppService;
    }
    /**
     * 提交 APP 用户意见反馈。
     */
    @PostMapping("/user-feedbacks")
    public ApiResponse<Void> submit(@Valid @RequestBody UserFeedbackSubmitRequest request) {
        userFeedbackAppService.submit(StpAppUserUtil.getLoginIdAsLong(), request);
        return ApiResponse.ok(null);
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
@@ -1,5 +1,6 @@
package com.tcm.training.admin.config;
import com.tcm.training.admin.app.auth.support.StpAppUserUtil;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.stp.StpUtil;
@@ -27,30 +28,73 @@
        this.xssParamInterceptor = xssParamInterceptor;
    }
    /**
     * 注册 Sa-Token 与 XSS 拦截器。
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor(handle -> {
                    if ("OPTIONS".equalsIgnoreCase(SaHolder.getRequest().getMethod())) {
                        return;
                    }
                    StpUtil.checkLogin();
                }))
        registry.addInterceptor(new SaInterceptor(handle -> handleAuth()))
                .addPathPatterns("/api/**", "/api/v1/**")
                .excludePathPatterns(
                        "/api/auth/login",
                        "/api/auth/captcha",
                        "/api/app-version-settings/check",
                        "/api/v1/app-version-settings/check"
                        pathPrefix + "/**"
                );
        registry.addInterceptor(xssParamInterceptor)
                .addPathPatterns("/api/**", "/api/v1/**");
    }
    /** 将本地上传目录映射为静态资源,可通过 /uploads/** 访问 */
    /**
     * 按请求路径切换管理员鉴权与 APP 鉴权。
     */
    private void handleAuth() {
        if ("OPTIONS".equalsIgnoreCase(SaHolder.getRequest().getMethod())) {
            return;
        }
        String requestPath = SaHolder.getRequest().getRequestPath();
        if (isAppApi(requestPath)) {
            if (isPublicAppApi(requestPath)) {
                return;
            }
            StpAppUserUtil.checkLogin();
            return;
        }
        if (isPublicAdminApi(requestPath)) {
            return;
        }
        StpUtil.checkLogin();
    }
    /**
     * 判断是否属于 APP 路由空间。
     */
    private boolean isAppApi(String requestPath) {
        return requestPath.startsWith("/api/app/") || requestPath.startsWith("/api/v1/app/");
    }
    /**
     * 判断当前 APP 路由是否允许匿名访问。
     */
    private boolean isPublicAppApi(String requestPath) {
        return requestPath.startsWith("/api/app/auth/")
                || requestPath.startsWith("/api/v1/app/auth/")
                || "/api/app/version/check".equals(requestPath)
                || "/api/v1/app/version/check".equals(requestPath);
    }
    /**
     * 判断当前后台路由是否允许匿名访问。
     */
    private boolean isPublicAdminApi(String requestPath) {
        return "/api/auth/login".equals(requestPath)
                || "/api/auth/captcha".equals(requestPath);
    }
    /**
     * 将本地上传目录映射为静态资源,可通过 /uploads/** 访问。
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 转为绝对路径,确保 file: 协议正确
        // 转为绝对路径,确保 file: 协议正确。
        String absPath = new File(uploadDir).getAbsolutePath();
        registry.addResourceHandler(pathPrefix + "/**")
                .addResourceLocations("file:" + absPath + "/");
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/statistics/controller/UserEntryLogController.java
File was deleted
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java
@@ -4,7 +4,6 @@
import com.tcm.training.admin.common.PageResponse;
import com.tcm.training.admin.system.dto.AppVersionCreateRequest;
import com.tcm.training.admin.system.service.AppVersionSettingsAppService;
import com.tcm.training.admin.system.vo.AppVersionCheckVO;
import com.tcm.training.admin.system.vo.AppVersionDetailVO;
import com.tcm.training.admin.system.vo.AppVersionListItemVO;
import org.springframework.validation.annotation.Validated;
@@ -25,10 +24,16 @@
    private final AppVersionSettingsAppService appVersionSettingsAppService;
    /**
     * 注入 APP 版本后台管理服务。
     */
    public AppVersionSettingsController(AppVersionSettingsAppService appVersionSettingsAppService) {
        this.appVersionSettingsAppService = appVersionSettingsAppService;
    }
    /**
     * 分页查询 APP 版本配置。
     */
    @GetMapping("/app-version-settings")
    public ApiResponse<PageResponse<AppVersionListItemVO>> page(
            @RequestParam(required = false, defaultValue = "1") int page,
@@ -36,18 +41,17 @@
        return ApiResponse.ok(appVersionSettingsAppService.page(page, pageSize));
    }
    @GetMapping("/app-version-settings/check")
    public ApiResponse<AppVersionCheckVO> checkUpdate(
            @RequestParam String platform,
            @RequestParam Integer currentVersionCode) {
        return ApiResponse.ok(appVersionSettingsAppService.checkUpdate(platform, currentVersionCode));
    }
    /**
     * 查询指定版本配置详情。
     */
    @GetMapping("/app-version-settings/{id}")
    public ApiResponse<AppVersionDetailVO> detail(@PathVariable("id") Long id) {
        return ApiResponse.ok(appVersionSettingsAppService.detail(id));
    }
    /**
     * 新增 APP 版本配置。
     */
    @PostMapping("/app-version-settings")
    public ApiResponse<AppVersionDetailVO> create(@Valid @RequestBody AppVersionCreateRequest request) {
        return ApiResponse.ok(appVersionSettingsAppService.create(request));
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/system/controller/UserFeedbackController.java
@@ -1,20 +1,14 @@
package com.tcm.training.admin.system.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.tcm.training.admin.common.ApiResponse;
import com.tcm.training.admin.common.PageResponse;
import com.tcm.training.admin.system.dto.UserFeedbackSubmitRequest;
import com.tcm.training.admin.system.service.UserFeedbackAppService;
import com.tcm.training.admin.system.vo.UserFeedbackListItemVO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@Validated
@RestController
@@ -23,21 +17,21 @@
    private final UserFeedbackAppService userFeedbackAppService;
    /**
     * 注入用户反馈后台查询服务。
     */
    public UserFeedbackController(UserFeedbackAppService userFeedbackAppService) {
        this.userFeedbackAppService = userFeedbackAppService;
    }
    /**
     * 分页查询用户反馈列表,供后台管理端使用。
     */
    @GetMapping("/user-feedbacks")
    public ApiResponse<PageResponse<UserFeedbackListItemVO>> page(
            @RequestParam(required = false, defaultValue = "1") int page,
            @RequestParam(required = false, defaultValue = "10") int pageSize,
            @RequestParam(required = false) String content) {
        return ApiResponse.ok(userFeedbackAppService.page(page, pageSize, content));
    }
    @PostMapping("/user-feedbacks")
    public ApiResponse<Void> submit(@Valid @RequestBody UserFeedbackSubmitRequest request) {
        userFeedbackAppService.submit(StpUtil.getLoginIdAsLong(), request);
        return ApiResponse.ok(null);
    }
}
traditionalApp/config/index.js
@@ -1,5 +1,5 @@
export default {
    BASE_URL: 'http://192.168.110.111:9100',
    BASE_URL: 'http://192.168.110.188:48080',
    // BASE_URL: 'https://jiashikang.cn/applet',
    uploadUrl: "https://jiashikang.cn/applet/open/file/upload",
    imageUrl: 'http://221.182.45.100:11224/images/'
traditionalApp/pages/login/login.vue
@@ -1,38 +1,53 @@
<template>
    <view class="container">
        <!-- Close Icon -->
        <image class="close-icon" src="@/static/image/login/close@2x.png" mode="aspectFit"></image>
        <!-- Title -->
        <image class="close-icon" src="@/static/image/login/close@2x.png" mode="aspectFit" @tap="handleClose"></image>
        <view class="title">欢迎登录</view>
        <!-- Tabs -->
        <view class="tabs">
            <view class="tab" :class="loginType === 'code' ? 'active' : 'normal'" @tap="loginType = 'code'">验证码登录</view>
            <view class="tab" :class="loginType === 'password' ? 'active' : 'normal'" @tap="loginType = 'password'">密码登录</view>
            <view class="tab" :class="loginType === 'code' ? 'active' : 'normal'" @tap="switchLoginType('code')">验证码登录</view>
            <view class="tab" :class="loginType === 'password' ? 'active' : 'normal'" @tap="switchLoginType('password')">密码登录</view>
        </view>
        <!-- Inputs -->
        <view class="form">
            <view class="input-group">
                <input class="input" type="number" placeholder="输入手机号" placeholder-class="placeholder-style" />
                <view class="get-code-btn" v-if="loginType === 'code'">获取验证码</view>
                <input
                    class="input"
                    type="number"
                    maxlength="11"
                    v-model="phone"
                    placeholder="输入手机号"
                    placeholder-class="placeholder-style"
                />
                <view
                    class="get-code-btn"
                    :class="{ disabled: countdown > 0 }"
                    v-if="loginType === 'code'"
                    @tap="handleSendCode"
                >
                    {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
                </view>
            </view>
            <view class="input-group">
                <input class="input" :type="loginType === 'code' ? 'number' : 'text'" :password="loginType === 'password'" :placeholder="loginType === 'code' ? '输入验证码' : '输入密码'" placeholder-class="placeholder-style" />
                <input
                    class="input"
                    :type="loginType === 'code' ? 'number' : 'text'"
                    :password="loginType === 'password'"
                    :maxlength="loginType === 'code' ? 6 : 32"
                    v-model="credential"
                    :placeholder="loginType === 'code' ? '输入验证码' : '输入密码'"
                    placeholder-class="placeholder-style"
                />
            </view>
        </view>
        <!-- Login Button -->
        <view class="login-btn">登录</view>
        <view class="login-btn" @tap="handleLogin">{{ loading ? '登录中...' : '登录' }}</view>
        <!-- Action Links -->
        <view class="action-links" :class="{ 'center': loginType === 'code', 'space-between': loginType === 'password' }">
            <text class="register-link" @tap="goRegister">没有账号?点击注册</text>
            <text class="forgot-password" v-if="loginType === 'password'" @tap="goForgotPassword">忘记密码?</text>
        </view>
        <!-- WeChat Login -->
        <view class="wechat-btn" @tap="handleWechatLogin">
            <image class="wechat-icon" src="/static/image/login/login@2x.png" mode="aspectFit"></image>
            <text class="wechat-text">微信登录</text>
@@ -41,81 +56,282 @@
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted } from "vue";
import { onShow, onLoad } from "@dcloudio/uni-app";
import { login, getCode, loginCode, confirmSuperior } from "./service.js";
import { onLoad } from '@dcloudio/uni-app'
import { onUnmounted, ref } from 'vue'
import { sendSmsCode, smsLogin } from './service.js'
// 登录类型:'code' 验证码登录,'password' 密码登录
const loginType = ref<'code' | 'password'>('code');
const loginType = ref<'code' | 'password'>('code')
const phone = ref('')
const credential = ref('')
const countdown = ref(0)
const loading = ref(false)
const inviteUserId = ref('')
const inviteType = ref('')
let countdownTimer: ReturnType<typeof setInterval> | null = null
interface AppLoginUser {
    id: number
    nickname: string
    phone: string
    avatar?: string
    type?: number
    status?: number
    clinicId?: number
    memberExpireAt?: string
}
interface AppLoginPayload {
    token: string
    tokenType: string
    expiresIn: number
    user: AppLoginUser
}
interface ApiResponse<T> {
    code: number
    msg?: string
    message?: string
    data: T
}
/**
 * 页面加载时记录来源参数,便于后续扩展回跳逻辑。
 */
onLoad((options) => {
    inviteUserId.value = options?.inviteUserId || ''
    inviteType.value = options?.inviteType || ''
})
/**
 * 页面销毁时清理倒计时,避免重复定时器泄漏。
 */
onUnmounted(() => {
    clearCountdown()
})
/**
 * 切换登录方式时重置当前输入态。
 */
const switchLoginType = (type: 'code' | 'password') => {
    loginType.value = type
    credential.value = ''
}
/**
 * 关闭登录页,优先返回上一页,没有页面栈时回首页。
 */
const handleClose = () => {
    if (getCurrentPages().length > 1) {
        uni.navigateBack()
        return
    }
    uni.switchTab({
        url: '/pages/home/home'
    })
}
/**
 * 跳转到注册页。
 */
const goRegister = () => {
    uni.navigateTo({
        url: '/pages/register/register'
    });
};
    })
}
/**
 * 跳转到忘记密码页。
 */
const goForgotPassword = () => {
    uni.navigateTo({ url: '/pages/forgotPassword/forgotPassword' });
};
    uni.navigateTo({
        url: '/pages/forgotPassword/forgotPassword'
    })
}
/**
 * 执行微信登录占位逻辑。
 */
const handleWechatLogin = () => {
    // #ifdef APP-PLUS
    uni.login({
        provider: 'weixin',
        success: (res) => {
            console.log('Wechat login success:', res);
            // 获取用户信息
            uni.getUserInfo({
                provider: 'weixin',
                success: (infoRes) => {
                    console.log('Wechat user info:', infoRes);
                    uni.showToast({
                        title: '微信授权成功',
                        icon: 'success'
                    });
                    // 假设这里后端返回该微信未绑定手机号,跳转到绑定手机号页面
                    setTimeout(() => {
                        uni.navigateTo({
                            url: '/pages/bindPhone/bindPhone'
                        });
                    }, 1000);
                    // 在这里将 res.authResult 或者 infoRes.userInfo 发送给后端接口进行登录
                    // 例如:api.wechatLogin({ openid: infoRes.userInfo.openId, ... })
                },
                fail: (err) => {
                    console.error('Get user info failed:', err);
                    uni.showToast({
                        title: '获取用户信息失败',
                        icon: 'none'
                    });
                }
            });
        success: () => {
            uni.showToast({
                title: '微信登录暂未开放',
                icon: 'none'
            })
        },
        fail: (err) => {
            console.error('Wechat login failed:', err);
        fail: () => {
            uni.showToast({
                title: '微信登录失败',
                icon: 'none'
            });
            })
        }
    });
    })
    // #endif
    // #ifndef APP-PLUS
    uni.showToast({
        title: '请在App中使用微信登录',
        icon: 'none'
    });
    })
    // #endif
};
}
/**
 * 发送短信验证码并启动倒计时。
 */
const handleSendCode = async () => {
    if (countdown.value > 0 || loading.value) {
        return
    }
    const normalizedPhone = normalizePhone()
    if (!normalizedPhone) {
        return
    }
    const res = await sendSmsCode({ phone: normalizedPhone })
    if (!isSuccessResponse(res)) {
        return
    }
    startCountdown()
    uni.showToast({
        title: '验证码已发送',
        icon: 'success'
    })
}
/**
 * 执行登录动作,当前仅开放验证码登录。
 */
const handleLogin = async () => {
    if (loading.value) {
        return
    }
    if (loginType.value === 'password') {
        uni.showToast({
            title: '密码登录暂未开放',
            icon: 'none'
        })
        return
    }
    const normalizedPhone = normalizePhone()
    if (!normalizedPhone) {
        return
    }
    const normalizedCode = normalizeCode()
    if (!normalizedCode) {
        return
    }
    loading.value = true
    try {
        const res = await smsLogin({
            phone: normalizedPhone,
            code: normalizedCode
        })
        if (!isSuccessResponse(res)) {
            return
        }
        saveLoginSession(res.data)
        uni.showToast({
            title: '登录成功',
            icon: 'success'
        })
        setTimeout(() => {
            uni.switchTab({
                url: '/pages/home/home'
            })
        }, 300)
    } finally {
        loading.value = false
    }
}
/**
 * 将登录响应写入本地缓存,供后续请求与页面读取。
 */
const saveLoginSession = (loginData: AppLoginPayload) => {
    if (!loginData) {
        return
    }
    const authToken = `${loginData.tokenType || 'Bearer'} ${loginData.token || ''}`.trim()
    uni.setStorageSync('token', authToken)
    uni.setStorageSync('appUserInfo', loginData.user || {})
    uni.setStorageSync('showLogoutModal', 1)
    // 先保留来源参数,后续如需登录后回跳可直接复用。
    uni.setStorageSync('loginRedirectMeta', {
        inviteUserId: inviteUserId.value,
        inviteType: inviteType.value
    })
}
/**
 * 校验手机号输入是否合法。
 */
const normalizePhone = () => {
    const normalizedPhone = (phone.value || '').trim()
    if (!/^1\d{10}$/.test(normalizedPhone)) {
        uni.showToast({
            title: '请输入正确的手机号',
            icon: 'none'
        })
        return ''
    }
    return normalizedPhone
}
/**
 * 校验验证码输入是否合法。
 */
const normalizeCode = () => {
    const normalizedCode = (credential.value || '').trim()
    if (!/^\d{6}$/.test(normalizedCode)) {
        uni.showToast({
            title: '请输入6位验证码',
            icon: 'none'
        })
        return ''
    }
    return normalizedCode
}
/**
 * 判断接口响应是否为成功态。
 */
const isSuccessResponse = (response: ApiResponse<unknown> | undefined) => {
    return !!response && (response.code === 0 || response.code === 200)
}
/**
 * 启动 60 秒验证码发送倒计时。
 */
const startCountdown = () => {
    clearCountdown()
    countdown.value = 60
    countdownTimer = setInterval(() => {
        if (countdown.value <= 1) {
            clearCountdown()
            return
        }
        countdown.value -= 1
    }, 1000)
}
/**
 * 清理验证码倒计时。
 */
const clearCountdown = () => {
    if (countdownTimer) {
        clearInterval(countdownTimer)
        countdownTimer = null
    }
    countdown.value = 0
}
</script>
<style></style>
<style lang="scss" scoped>
.container {
    padding: 0 40rpx;
    /* 距离状态栏151rpx, assuming var(--status-bar-height) is available */
    padding-top: calc(var(--status-bar-height, 0px) + 151rpx);
    box-sizing: border-box;
    min-height: 100vh;
@@ -202,6 +418,10 @@
            font-size: 26rpx;
            border-radius: 8rpx;
            margin-right: 24rpx;
            &.disabled {
                background-color: #D9D9D9;
            }
        }
    }
}
@@ -259,12 +479,12 @@
    .wechat-icon {
        width: 56rpx;
        height: 56rpx;
        margin-right: 48rpx;
        margin-right: 16rpx;
    }
    .wechat-text {
        font-size: 24rpx;
        color: #010F07;
        font-size: 30rpx;
        color: #333333;
    }
}
</style>
</style>
traditionalApp/pages/login/service.js
@@ -1,20 +1,29 @@
import request from '@/utils/request.js'
//一键登录
/**
 * 微信一键登录接口占位。
 */
export const login = (data) => {
    return request.post(`/openIdByJsCode`, data)
    return request.post('/openIdByJsCode', data)
}
//短信验证码登录
export const loginCode = (data) => {
    return request.post(`/loginCode`, data)
/**
 * 发送 APP 短信登录验证码。
 */
export const sendSmsCode = (data) => {
    return request.post('/api/app/auth/sms-code', data)
}
//获取验证码
export const getCode = (params) => {
    return request.get(`/getCode?phone=${params}`)
/**
 * 使用短信验证码完成 APP 登录。
 */
export const smsLogin = (data) => {
    return request.post('/api/app/auth/sms-login', data)
}
//确认上级用户-接受邀请
/**
 * 确认上级用户邀请。
 */
export const confirmSuperior = (params) => {
    return request.get(`/index/confirmSuperior`,params, 1)
}
    return request.get('/index/confirmSuperior', params, 1)
}
traditionalApp/utils/request.js
@@ -9,6 +9,29 @@
const waitingList = [] //等待队列
const excutingList = [] //执行队列
/**
 * 判断当前响应码是否属于成功态。
 */
function isSuccessCode(code) {
    return code === 0 || code === 200
}
/**
 * 从响应体中提取可展示的错误文案。
 */
function resolveMessage(payload) {
    if (!payload) {
        return '服务器错误'
    }
    if (typeof payload.data === 'string' && payload.data) {
        return payload.data
    }
    return payload.msg || payload.message || '服务器错误'
}
/**
 * 统一发起请求,并兼容当前仓库 `code=0` 的后端成功协议。
 */
function request(method, url, data, type, lodingFlag = true) {
    if (lodingFlag) {
        uni.showLoading({
@@ -28,71 +51,66 @@
            data: data,
            header: header,
            success: (res) => {
                //判断状态码
                if (lodingFlag) {
                    uni.hideLoading()
                }
                if (res.data.code == 200) {
                    resolve(res.data);
                    uni.hideLoading()
                const payload = res.data || {}
                if (isSuccessCode(payload.code)) {
                    resolve(payload);
                    return
                }
                if (res.data.code == 502) {
                    resolve(res.data);
                if (payload.code == 502) {
                    resolve(payload);
                    uni.showToast({
                        title: res.data.msg,
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    uni.hideLoading()
                    return
                }
                if (res.data.code == 1 || res.data.code == 500) {
                if (payload.code == 1 || payload.code == 500) {
                    uni.showToast({
                        title: res.data.data || res.data.msg,
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    uni.hideLoading()
                    resolve(payload)
                    return
                }
                if (res.data.code == 401 || res.data.code == 510 || res.data.code == 504 || res
                    .data
                    .code == 505) {
                if (payload.code == 401 || payload.code == 510 || payload.code == 504 || payload.code == 505) {
                    // uni.clearStorageSync()
                    // handleLogout('登录过期,请重新登录')
                    // uni.navigateTo({
                    //     url: '/pages/login/login'
                    // })
                    // return
                    resolve(payload)
                    return
                }
                if (res.data.code == 501) {
                if (payload.code == 501) {
                    uni.showToast({
                        title: res.data.msg,
                        title: resolveMessage(payload),
                        duration: 2000,
                        icon: 'none',
                        mask: true,
                    })
                    uni.hideLoading()
                    resolve(payload)
                    return
                }
                if (res.data.code == 506) {
                    resolve(res.data);
                    uni.hideLoading()
                if (payload.code == 506) {
                    resolve(payload);
                    return
                }
                uni.showToast({
                    title: res.data.msg || '服务器错误',
                    title: resolveMessage(payload),
                    duration: 2000,
                    icon: 'none',
                    mask: true,
                })
                uni.hideLoading()
                resolve(payload)
            },
            fail(err) {
                if (lodingFlag) {
@@ -116,10 +134,11 @@
}
// 被冻结跳转到登录页并取消后续请求
/**
 * 被冻结时跳转到登录页并取消后续请求。
 */
function handleLogout(str) {
    let routeStr = getCurrentPages()[getCurrentPages().length - 1].route
    // 登录成功后需要设置 showLogoutModal 为 1
    if (uni.getStorageSync('showLogoutModal') == 1) {
        uni.setStorageSync('showLogoutModal', 0)
        uni.showModal({
@@ -141,7 +160,9 @@
    }
}
// 清除请求队列
/**
 * 清除请求队列。
 */
function clearAllRequest() {
    if (excutingList.length > 0) {
        for (var i = 0; i < excutingList.length; i++) {
@@ -155,10 +176,45 @@
    }
}
/**
 * 发起 GET 请求。
 */
function get(url, data, type = false, lodingFlag = true) {
    return request(GET, url, data, type, lodingFlag)
}
/**
 * 发起 POST 请求。
 */
function post(url, data, type = false, lodingFlag = true) {
    return request(POST, url, data, type, lodingFlag)
}
/**
 * 发起 PUT 请求。
 */
function put(url, data, type = false, lodingFlag = true) {
    return request(PUT, url, data, type, lodingFlag)
}
/**
 * 发起表单编码请求。
 */
function form(url, data, type = false, lodingFlag = true) {
    return request(FORM, url, data, type, lodingFlag)
}
/**
 * 发起 DELETE 请求。
 */
function remove(url, data, type = false, lodingFlag = true) {
    return request(DELETE, url, data, type, lodingFlag)
}
export default {
    get: (url, data, type = false, lodingFlag = true) => request(GET, url, data, type, lodingFlag),
    post: (url, data, type = false, lodingFlag = true) => request(POST, url, data, type, lodingFlag),
    put: (url, data, type = false, lodingFlag = true) => request(PUT, url, data, type, lodingFlag),
    FORM: (url, data, type = false, lodingFlag = true) => request(FORM, url, data, type, lodingFlag),
    delete: (url, data, type = false, lodingFlag = true) => request(DELETE, url, data, type, lodingFlag),
}
    get,
    post,
    put,
    FORM: form,
    delete: remove,
}