Browse Source

更新:用户认证、文档生成、问题修复

develop
王泽彦 2 months ago
parent
commit
fab305a52e
  1. 1
      .gitignore
  2. 18
      AGENTS.md
  3. 128
      PROJECT.md
  4. 0
      admin/bug.md
  5. 36
      admin/src/app/views/login/index.vue
  6. 38
      admin/src/router/index.ts
  7. 14
      admin/src/router/routers.ts
  8. 5
      admin/src/stores/modules/user.ts
  9. 9
      docker-compose.yml
  10. 15
      niucloud/app/adminapi/controller/auth/Auth.php
  11. 11
      niucloud/app/adminapi/middleware/AdminCheckToken.php
  12. 3
      niucloud/app/adminapi/route/auth.php
  13. 8
      niucloud/app/model/document/DocumentGenerateLog.php
  14. 1500
      niucloud/app/service/admin/document/DocumentTemplateService.php
  15. 16
      niucloud/app/service/admin/upgrade/UpgradeService.php
  16. 6
      niucloud/app/service/core/addon/CoreAddonInstallService.php
  17. 12
      niucloud/app/service/core/addon/CoreDependService.php
  18. 0
      niucloud/bug.md
  19. 84
      question.md
  20. 0
      uniapp/bug.md

1
.gitignore

@ -19,5 +19,6 @@ uniapp/TASK.md
uniapp/PLANNING.md uniapp/PLANNING.md
niucloud/TASK.md niucloud/TASK.md
niucloud/PLANNING.md niucloud/PLANNING.md
openspec

18
AGENTS.md

@ -0,0 +1,18 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->

128
PROJECT.md

@ -0,0 +1,128 @@
# 智慧教务系统
## 项目概述
课程预约、消课、服务管理的企业级教务系统,支持学员端和教练端。
## 项目结构
```
zhjwxt/
├── admin/ # 管理后台 (Vue3 + Element Plus + TypeScript)
├── uniapp/ # 移动端客户端 (UniApp + Vue3 + TypeScript)
├── niucloud/ # ThinkPHP8 后端服务
├── openspec/ # OpenSpec 规范文档
├── docker/ # Docker 配置
├── doc/ # 文档目录
└── vendor/ # Composer 依赖
```
## 技术栈
| 模块 | 技术栈 |
|------|--------|
| 后端 | ThinkPHP 8.0 + MySQL + Redis |
| 管理后台 | Vue3 + Element Plus + TypeScript + Vite |
| 移动端 | UniApp + Vue3 + TypeScript + Pinia |
## 运行环境
### 数据库
- **数据库名**: `niucloud`
- **用户名**: `niucloud`
- **密码**: `niucloud123`
### 服务端口
- **后端 API**: `http://localhost:20080`
- 客户端接口: `/api`
- 管理端接口: `/adminapi`
- **管理后台**: `http://localhost:23000`
- **移动端 H5**: `http://localhost:8080`
### 启动命令
```bash
# 启动所有服务 (Docker)
./start.sh
# 单独启动后端
cd niucloud && php think serve
# 启动管理后台
cd admin && npm run dev
# 启动移动端
cd uniapp && npm run dev:h5
```
## 核心功能模块
### 课程管理
- 课程创建/编辑/删除
- 课程排期
- 课程类型管理
### 预约管理
- 学员预约
- 预约审核
- 预约取消/改期
### 消课管理
- 消课记录
- 消课统计
- 消课审核
### 学员管理
- 学员信息
- 学员卡管理
- 学员消费记录
### 教练管理
- 教练信息
- 教练排班
- 教练绩效
## 端点说明
### 移动端目录
| 目录 | 说明 |
|------|------|
| `pages/common/` | 通用页面(登录、设置等) |
| `pages/student/` | 学员端页面 |
| `pages/coach/` | 教练端页面 |
| `pages-coach/` | 教练端专属页面 |
| `pages-common/` | 通用页面模块 |
### 后端目录
| 目录 | 说明 |
|------|------|
| `app/adminapi/controller/` | 管理后台接口 |
| `app/api/controller/` | 客户端接口 |
| `app/service/admin/` | 管理后台业务逻辑 |
| `app/service/api/` | 客户端业务逻辑 |
| `app/model/` | 数据模型 |
## 测试账号
### 管理后台
- **地址**: `http://localhost:23000`
- **用户名**: `admin`
- **密码**: `123123`
## 开发规范
### 新增功能流程
1. 后端:控制器 → Service → Model
2. 前端:API → 页面组件
3. 路由:注册新页面路由
### 文件命名
- 控制器:`Xxx.php`
- Service:`XxxService.php`
- Model:`Xxx.php`
- 页面:`.vue`
## 相关项目
- **钜惠云仓商城项目**: `/Users/mac/coding/juhuiyuncang/juhuiyuncloudadmin`
- **数据采集项目**: `/Users/mac/coding/juhuiyuncang/datahandle`
- **物流项目**: `/Users/mac/coding/juhuiyuncang/groupeddelivery`

0
admin/bug.md

36
admin/src/app/views/login/index.vue

@ -200,16 +200,40 @@ const loginFn = (data = {}) => {
loading.value = true loading.value = true
userStore userStore
.login({ username: form.username, password: form.password, ...data }) .login({ username: form.username, password: form.password, ...data })
.then((res) => { .then(async (res) => {
try {
//
await userStore.getAuthMenusFn()
//
const { const {
query: { redirect }, query: { redirect },
} = route } = route
const path = typeof redirect === 'string' ? redirect : '/'
const url = router.resolve(path) // 使
// console.log(url); if (redirect && typeof redirect === 'string') {
location.href = '/' console.log('登录成功,跳转到重定向页面:', redirect)
await router.push(redirect)
} else {
//
const { findFirstValidRoute } = await import('@/router/routers')
const firstRoute = findFirstValidRoute(userStore.routers)
if (firstRoute) {
console.log('登录成功,跳转到第一个有效路由:', firstRoute)
await router.push({ name: firstRoute })
} else {
console.error('未找到有效的菜单路由')
throw new Error('未找到有效的菜单路由')
}
}
console.log('跳转完成')
} catch (error) {
console.error('登录后跳转失败:', error)
loading.value = false
}
}) })
.catch(() => { .catch((err) => {
console.error('登录失败:', err)
loading.value = false loading.value = false
}) })
} }

38
admin/src/router/index.ts

@ -92,12 +92,28 @@ router.beforeEach(async (to: any, from, next) => {
} else if (userStore.token) { } else if (userStore.token) {
// 如果已加载路由 // 如果已加载路由
if (userStore.routers.length) { if (userStore.routers.length) {
console.log('路由守卫: 已加载路由数量=', userStore.routers.length, '目标路径=', to.path)
if (to.path === loginPath) { if (to.path === loginPath) {
console.log('路由守卫: 从登录页跳转到应用根路径')
next(`/${getAppType()}`) next(`/${getAppType()}`)
} else if (to.path === `/${getAppType()}`) {
// 如果访问的是应用根路径,重定向到第一个有效路由
const firstRoute = findFirstValidRoute(userStore.routers)
console.log('路由守卫: 访问根路径,找到的第一个路由=', firstRoute)
if (firstRoute) {
console.log('路由守卫: 重定向到第一个路由', { name: firstRoute })
next({ name: firstRoute })
} else {
console.log('路由守卫: 没有找到有效路由')
next()
}
} else { } else {
console.log('路由守卫: 正常访问', to.path)
next() next()
} }
} else { } else {
console.log('路由守卫: 未加载路由,开始加载...')
try { try {
if (!systemStore.apps.length) { if (!systemStore.apps.length) {
await systemStore.getInstallAddons() await systemStore.getInstallAddons()
@ -112,6 +128,16 @@ router.beforeEach(async (to: any, from, next) => {
firstRoute = userStore.addonIndexRoute[systemStore.apps[0].key] firstRoute = userStore.addonIndexRoute[systemStore.apps[0].key]
} }
console.log('路由守卫: 首个路由=', firstRoute, '路由列表=', userStore.routers)
// 如果没有找到有效路由,使用默认的登录页面或404页面
if (!firstRoute) {
console.warn('未找到有效的菜单路由,请检查用户权限配置')
// 清除token并重新登录
userStore.logout()
return
}
ROOT_ROUTER.redirect = { name: firstRoute } ROOT_ROUTER.redirect = { name: firstRoute }
router.addRoute(ROOT_ROUTER) router.addRoute(ROOT_ROUTER)
@ -129,9 +155,19 @@ router.beforeEach(async (to: any, from, next) => {
// 动态添加可访问路由表 // 动态添加可访问路由表
router.addRoute(ADMIN_ROUTE.name, route) router.addRoute(ADMIN_ROUTE.name, route)
}) })
console.log('路由守卫: 动态路由已添加,目标路径=', to.path)
// 如果访问的是登录页或应用根路径,跳转到第一个有效路由
if (to.path === loginPath || to.path === `/${getAppType()}`) {
console.log('路由守卫: 跳转到首个路由', { name: firstRoute })
next({ name: firstRoute })
} else {
console.log('路由守卫: 继续访问目标页面', to.path)
next(to) next(to)
}
} catch (err) { } catch (err) {
console.log(err) console.error('路由守卫: 加载路由失败', err)
next({ path: loginPath, query: { redirect: to.fullPath } }) next({ path: loginPath, query: { redirect: to.fullPath } })
} }
} }

14
admin/src/router/routers.ts

@ -34,8 +34,7 @@ export const ADMIN_ROUTE: RouteRecordRaw = {
children: [ children: [
{ {
path: '', path: '',
name: Symbol('adminRoot'), redirect: '/admin/login', // 默认重定向到登录页,登录后会由路由守卫动态修改
component: Default,
}, },
{ {
path: 'login', path: 'login',
@ -142,15 +141,26 @@ export function formatRouters(
export function findFirstValidRoute( export function findFirstValidRoute(
routes: RouteRecordRaw[] routes: RouteRecordRaw[]
): string | undefined { ): string | undefined {
console.log('findFirstValidRoute: 开始查找,路由数量=', routes.length)
for (const route of routes) { for (const route of routes) {
console.log('findFirstValidRoute: 检查路由', route.name, 'meta=', route.meta)
if (route.meta?.type == 1 && route.meta?.show) { if (route.meta?.type == 1 && route.meta?.show) {
console.log('findFirstValidRoute: 找到有效路由=', route.name)
return route.name as string return route.name as string
} }
if (route.children) { if (route.children) {
console.log('findFirstValidRoute: 检查子路由,数量=', route.children.length)
const name = findFirstValidRoute(route.children) const name = findFirstValidRoute(route.children)
if (name) { if (name) {
console.log('findFirstValidRoute: 在子路由中找到有效路由=', name)
return name return name
} }
} }
} }
console.log('findFirstValidRoute: 没有找到有效路由')
return undefined
} }

5
admin/src/stores/modules/user.ts

@ -60,7 +60,10 @@ const useUserStore = defineStore('user', {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getAuthMenus({}) getAuthMenus({})
.then((res) => { .then((res) => {
console.log('获取菜单数据:', res.data)
this.routers = formatRouters(res.data) this.routers = formatRouters(res.data)
console.log('格式化后的路由:', this.routers)
// 获取插件的首个菜单 // 获取插件的首个菜单
this.routers.forEach((item, index) => { this.routers.forEach((item, index) => {
if (item.meta.app !== '') { if (item.meta.app !== '') {
@ -73,9 +76,11 @@ const useUserStore = defineStore('user', {
} }
} }
}) })
console.log('插件首页路由:', this.addonIndexRoute)
resolve(res) resolve(res)
}) })
.catch((error) => { .catch((error) => {
console.error('获取菜单失败:', error)
reject(error) reject(error)
}) })
}) })

9
docker-compose.yml

@ -9,9 +9,6 @@ services:
- ./niucloud:/var/www/html - ./niucloud:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/php.ini - ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf - ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
# 时区同步
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
working_dir: /var/www/html working_dir: /var/www/html
depends_on: depends_on:
- mysql - mysql
@ -35,9 +32,6 @@ services:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./docker/nginx/conf.d:/etc/nginx/conf.d - ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./docker/logs/nginx:/var/log/nginx - ./docker/logs/nginx:/var/log/nginx
# 时区同步
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
depends_on: depends_on:
@ -62,9 +56,6 @@ services:
- ./docker/data/mysql:/var/lib/mysql - ./docker/data/mysql:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./docker/logs/mysql:/var/log/mysql - ./docker/logs/mysql:/var/log/mysql
# 时区同步
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
command: --default-authentication-plugin=mysql_native_password command: --default-authentication-plugin=mysql_native_password
networks: networks:
- niucloud_network - niucloud_network

15
niucloud/app/adminapi/controller/auth/Auth.php

@ -57,6 +57,21 @@ class Auth extends BaseAdminController
return success('MODIFY_SUCCESS'); return success('MODIFY_SUCCESS');
} }
/**
* 测试接口 - 用于调试
* @return Response
*/
public function test()
{
$data = [
'uid' => $this->request->uid(),
'username' => $this->request->username(),
'token' => $this->request->adminToken(),
'message' => 'Token验证成功'
];
return success($data);
}
/** /**
* 更新用户 * 更新用户
*/ */

11
niucloud/app/adminapi/middleware/AdminCheckToken.php

@ -26,9 +26,20 @@ class AdminCheckToken
{ {
//通过配置来设置系统header参数 //通过配置来设置系统header参数
$token = $request->adminToken(); $token = $request->adminToken();
if (empty($token)) {
throw new \core\exception\AuthException('MUST_LOGIN', 401);
}
try {
$token_info = (new LoginService())->parseToken($token); $token_info = (new LoginService())->parseToken($token);
$request->uid($token_info['uid']); $request->uid($token_info['uid']);
$request->username($token_info['username']); $request->username($token_info['username']);
return $next($request); return $next($request);
} catch (\Throwable $e) {
// 记录错误日志以便调试
\think\facade\Log::error('Token验证失败: ' . $e->getMessage());
throw new \core\exception\AuthException('LOGIN_EXPIRE', 401);
}
} }
} }

3
niucloud/app/adminapi/route/auth.php

@ -30,6 +30,9 @@ Route::group('auth', function () {
Route::put('edit', 'auth.Auth/edit'); Route::put('edit', 'auth.Auth/edit');
//授权用户信息 //授权用户信息
Route::put('modify/:field', 'auth.Auth/modify'); Route::put('modify/:field', 'auth.Auth/modify');
// 测试接口 - 仅用于调试token验证问题
Route::get('test', 'auth.Auth/test');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,
AdminCheckRole::class, AdminCheckRole::class,

8
niucloud/app/model/document/DocumentGenerateLog.php

@ -13,14 +13,6 @@ class DocumentGenerateLog extends BaseModel
protected $pk = 'id'; protected $pk = 'id';
protected $name = 'document_generate_log'; protected $name = 'document_generate_log';
// 字段类型定义
protected $type = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'process_start_time' => 'datetime',
'process_end_time' => 'datetime'
];
/** /**
* 关联合同表 * 关联合同表
*/ */

1500
niucloud/app/service/admin/document/DocumentTemplateService.php

File diff suppressed because it is too large

16
niucloud/app/service/admin/upgrade/UpgradeService.php

@ -317,14 +317,14 @@ class UpgradeService extends BaseAdminService
if ($addon == AddonDict::FRAMEWORK_KEY) { if ($addon == AddonDict::FRAMEWORK_KEY) {
$composer = '/niucloud/composer.json'; $composer = '/niucloud/composer.json';
$admin_package = '/admin/mysql-mcp-config.json'; $admin_package = '/admin/package.json';
$web_package = '/web/mysql-mcp-config.json'; $web_package = '/web/package.json';
$uniapp_package = '/uni-app/mysql-mcp-config.json'; $uniapp_package = '/uniapp/package.json';
} else { } else {
$composer = "/niucloud/addon/{$addon}/package/composer.json"; $composer = "/niucloud/addon/{$addon}/package/composer.json";
$admin_package = "/niucloud/addon/{$addon}/package/admin-mysql-mcp-config.json"; $admin_package = "/niucloud/addon/{$addon}/package/admin-package.json";
$web_package = "/niucloud/addon/{$addon}/package/web-mysql-mcp-config.json"; $web_package = "/niucloud/addon/{$addon}/package/web-package.json";
$uniapp_package = "/niucloud/addon/{$addon}/package/uni-app-mysql-mcp-config.json"; $uniapp_package = "/niucloud/addon/{$addon}/package/uni-app-package.json";
} }
if (in_array($composer, $change_files)) { if (in_array($composer, $change_files)) {
@ -392,7 +392,7 @@ class UpgradeService extends BaseAdminService
$this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon); $this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon);
// 合并插件依赖 // 合并插件依赖
$addon_uniapp_package = str_replace('/', DIRECTORY_SEPARATOR, project_path() . "niucloud/addon/{$addon}/package/uni-app-mysql-mcp-config.json"); $addon_uniapp_package = str_replace('/', DIRECTORY_SEPARATOR, project_path() . "niucloud/addon/{$addon}/package/uni-app-package.json");
if (file_exists($addon_uniapp_package)) { if (file_exists($addon_uniapp_package)) {
$original = $depend_service->getNpmContent('uni-app'); $original = $depend_service->getNpmContent('uni-app');
@ -402,7 +402,7 @@ class UpgradeService extends BaseAdminService
$original[$name] = isset($original[$name]) && is_array($original[$name]) ? array_merge($original[$name], $new[$name]) : $new[$name]; $original[$name] = isset($original[$name]) && is_array($original[$name]) ? array_merge($original[$name], $new[$name]) : $new[$name];
} }
$uniapp_package = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json'; $uniapp_package = $this->root_path . 'uniapp' . DIRECTORY_SEPARATOR . 'package.json';
$depend_service->writeArrayToJsonFile($original, $uniapp_package); $depend_service->writeArrayToJsonFile($original, $uniapp_package);
} }
} }

6
niucloud/app/service/core/addon/CoreAddonInstallService.php

@ -220,10 +220,10 @@ class CoreAddonInstallService extends CoreAddonBaseService
}, $package_file); }, $package_file);
$tips = [get_lang('dict_addon.install_after_update')]; $tips = [get_lang('dict_addon.install_after_update')];
if (in_array('admin-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_admin_update'); if (in_array('admin-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_admin_update');
if (in_array('composer.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_composer_update'); if (in_array('composer.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_composer_update');
if (in_array('uni-app-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_wap_update'); if (in_array('uni-app-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_wap_update');
if (in_array('web-mysql-mcp-config.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_web_update'); if (in_array('web-package.json', $package_file)) $tips[] = get_lang('dict_addon.install_after_web_update');
return $tips; return $tips;
} }
return true; return true;

12
niucloud/app/service/core/addon/CoreDependService.php

@ -25,9 +25,9 @@ class CoreDependService extends CoreAddonBaseService
{ {
parent::__construct(); parent::__construct();
$this->server_composer_file = $this->root_path . 'niucloud' . DIRECTORY_SEPARATOR . 'composer.json'; $this->server_composer_file = $this->root_path . 'niucloud' . DIRECTORY_SEPARATOR . 'composer.json';
$this->admin_npm_file = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json'; $this->admin_npm_file = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'package.json';
$this->web_npm_file = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json'; $this->web_npm_file = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'package.json';
$this->wap_npm_file = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'mysql-mcp-config.json'; $this->wap_npm_file = $this->root_path . 'uniapp' . DIRECTORY_SEPARATOR . 'package.json';
} }
/** /**
@ -141,11 +141,11 @@ class CoreDependService extends CoreAddonBaseService
public function getAddonNpmContent(string $addon, string $type) public function getAddonNpmContent(string $addon, string $type)
{ {
if ($type == 'admin') { if ($type == 'admin') {
$file_path = $this->geAddonPackagePath($addon) . 'admin-mysql-mcp-config.json'; $file_path = $this->geAddonPackagePath($addon) . 'admin-package.json';
} elseif ($type == 'web') { } elseif ($type == 'web') {
$file_path = $this->geAddonPackagePath($addon) . 'web-mysql-mcp-config.json'; $file_path = $this->geAddonPackagePath($addon) . 'web-package.json';
} else { } else {
$file_path = $this->geAddonPackagePath($addon) . 'uni-app-mysql-mcp-config.json'; $file_path = $this->geAddonPackagePath($addon) . 'uni-app-package.json';
} }
return $this->jsonFileToArray($file_path); return $this->jsonFileToArray($file_path);
} }

0
niucloud/bug.md

84
question.md

@ -0,0 +1,84 @@
# 智慧教务系统 - 问题记录
> 记录浏览代码过程中遇到的业务问题,等待老王解答
## 1. 课程预约流程
**问题**:学员预约课程的完整流程是什么?
- 学员如何查看可预约的课程?
- 预约是否需要教练确认?
- 预约失败的原因有哪些?
**涉及文件**:
- `niucloud/app/api/controller/` (预约相关控制器)
- `uniapp/pages/student/` (学员端页面)
---
## 2. 消课机制
**问题**:消课是如何触发的?
- 教练手动消课还是系统自动消课?
- 消课前是否需要学员确认?
- 消课金额如何计算?
**涉及文件**:
- `niucloud/app/service/api/` (消课相关 Service)
- `uniapp/pages/` (消课页面)
---
## 3. 学员卡类型
**问题**:学员卡的类型和有效期规则是什么?
- 看到学员卡相关的数据库表
- 支持哪些类型的卡(次卡、月卡、季卡等)?
- 过期后如何处理?
---
## 4. 教练排班
**问题**:教练的排班是如何管理的?
- 排班是教练自己设置还是管理员设置?
- 排班冲突如何检测?
- 临时调课如何处理?
---
## 5. 多端权限
**问题**:学员端和教练端的权限如何区分?
- 看到 `pages/student/``pages/coach/` 目录
- 权限是如何控制的?
- 是否支持管理员角色?
---
## 6. 数据同步
**问题**:管理后台和移动端的数据如何同步?
- 看到有 `adminapi``api` 两套接口
- 数据是否实时同步?
- 是否有离线支持?
---
## 待确认的业务规则
1. **预约规则**:学员可以提前多久预约?是否可以取消预约?
2. **消课规则**:消课后是否立即扣减学员卡余额?
3. **教练提成**:教练的提成是如何计算的?
4. **退款规则**:学员如何申请退款?退款流程是什么?
5. **课时管理**:总课时和已消课时的关系是什么?
---
> **记录时间**:2026-02-01
> **状态**:待回答

0
uniapp/bug.md

Loading…
Cancel
Save