forked from wangziqi/gongxue-base
feat: scaffold taro h5 frontends
This commit is contained in:
39
README.md
39
README.md
@@ -33,7 +33,7 @@
|
|||||||
- Excel/CSV 导入解析已完成并复用 `content_import_jobs/items/issues` 管线;大批量异步导入 worker 基础已接入,支持 queued job 消费、重试和审计;导入后复检、模板下载和字段映射 API 已完成,前端 UI 待接。
|
- Excel/CSV 导入解析已完成并复用 `content_import_jobs/items/issues` 管线;大批量异步导入 worker 基础已接入,支持 queued job 消费、重试和审计;导入后复检、模板下载和字段映射 API 已完成,前端 UI 待接。
|
||||||
- 题库导出目前完成服务端结构化 payload;PDF/Word 二进制生成、导出水印、发布到资料下载和导出 worker 还没完成。
|
- 题库导出目前完成服务端结构化 payload;PDF/Word 二进制生成、导出水印、发布到资料下载和导出 worker 还没完成。
|
||||||
- 勋章管理/手动发放已可联调;自动发放规则、积分活动联动、分佣真实打款、结算导出、发票/凭证、CRM 轮询/定向分配、富卡片模板、失败告警、销售转化看板、公共题库版本通知和冲突处理操作台还没完成。
|
- 勋章管理/手动发放已可联调;自动发放规则、积分活动联动、分佣真实打款、结算导出、发票/凭证、CRM 轮询/定向分配、富卡片模板、失败告警、销售转化看板、公共题库版本通知和冲突处理操作台还没完成。
|
||||||
- Taro 跨端前端还没开始 scaffold。
|
- `apps/taro` 已建立 Taro 4 React 跨端前端地基,包含 H5 学生端、租户后台、平台后台三套构建入口、租户解析、统一 API client 和 Supabase Auth client 初始化;完整页面和端到端联调仍需继续补。
|
||||||
- 根目录已清理为新 Supabase SaaS monorepo 编排层;旧 PocketBase/React 项目和旧构建产物仅保留在 `参考/` 目录作为迁移参考,不进入 Git 提交。
|
- 根目录已清理为新 Supabase SaaS monorepo 编排层;旧 PocketBase/React 项目和旧构建产物仅保留在 `参考/` 目录作为迁移参考,不进入 Git 提交。
|
||||||
|
|
||||||
更完整的进度看这些文档:
|
更完整的进度看这些文档:
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
apps/api/ Node.js 业务 API
|
apps/api/ Node.js 业务 API
|
||||||
|
apps/taro/ Taro 4 React 跨端前端,H5 三入口,后续扩展小程序
|
||||||
apps/worker/ 后台异步任务:CRM webhook、支付/退款补偿、资源复检、导入执行、公共题库同步等
|
apps/worker/ 后台异步任务:CRM webhook、支付/退款补偿、资源复检、导入执行、公共题库同步等
|
||||||
packages/config/ 共享配置
|
packages/config/ 共享配置
|
||||||
packages/db/ PostgreSQL 连接池和查询封装
|
packages/db/ PostgreSQL 连接池和查询封装
|
||||||
@@ -92,6 +93,30 @@ npm run db:smoke-seed
|
|||||||
npm run dev:api
|
npm run dev:api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Taro H5 本地开发:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:taro:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
三套 H5 构建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:taro:h5:student
|
||||||
|
npm run build:taro:h5:tenant
|
||||||
|
npm run build:taro:h5:platform
|
||||||
|
```
|
||||||
|
|
||||||
|
对应产物:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/taro/dist/h5-student
|
||||||
|
apps/taro/dist/h5-tenant-admin
|
||||||
|
apps/taro/dist/h5-platform-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐分别部署到学生端域名、租户后台域名、平台后台域名;三者共用 `apps/taro/src/services/api.ts` 请求层,业务数据默认调用 `apps/api`,不要在页面里直写 Supabase 表。
|
||||||
|
|
||||||
单次运行 CRM worker:
|
单次运行 CRM worker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -157,6 +182,8 @@ npm run check:refactor
|
|||||||
npm run check:api
|
npm run check:api
|
||||||
npm run check:worker
|
npm run check:worker
|
||||||
npm run check:importer
|
npm run check:importer
|
||||||
|
npm run check:taro
|
||||||
|
npm run audit:runtime
|
||||||
npm run pb:import:dry-run
|
npm run pb:import:dry-run
|
||||||
npm run pb:import:validate
|
npm run pb:import:validate
|
||||||
npm run test:readiness
|
npm run test:readiness
|
||||||
@@ -250,16 +277,22 @@ API 身份上下文:
|
|||||||
npx supabase db reset
|
npx supabase db reset
|
||||||
npm run check:refactor
|
npm run check:refactor
|
||||||
npm run test:readiness
|
npm run test:readiness
|
||||||
|
npm run audit:runtime
|
||||||
npm run test:worker:imports
|
npm run test:worker:imports
|
||||||
npm run test:worker:crm
|
npm run test:worker:crm
|
||||||
npm run test:worker:commerce
|
npm run test:worker:commerce
|
||||||
npm run test:worker:assets
|
npm run test:worker:assets
|
||||||
npm run test:worker:public-banks
|
npm run test:worker:public-banks
|
||||||
npm audit --audit-level=high
|
npm run check:taro
|
||||||
|
npm run build:taro:h5:student
|
||||||
|
npm run build:taro:h5:tenant
|
||||||
|
npm run build:taro:h5:platform
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
结果:通过。`npm audit --audit-level=high` 无高危漏洞;当前依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
|
结果:通过。`npm run audit:runtime` 无 high/critical 漏洞;当前运行时依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
|
||||||
|
|
||||||
|
注意:`apps/taro` 是静态构建工程,线上发布 `apps/taro/dist/**`,不发布 `node_modules`。Taro 4.2.0 当前构建工具链仍会触发 `npm run audit:taro:toolchain` 的上游 high/critical 提示,不能用 `npm audit fix --force` 降级到 Taro 3 破坏构建;上线验收时以 `audit:runtime`、构建产物、前端密钥检查和静态服务器配置为准,并持续跟进 Taro 官方修复。
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
2
apps/taro/.gitignore
vendored
Normal file
2
apps/taro/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
.swc/
|
||||||
44
apps/taro/README.md
Normal file
44
apps/taro/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Taro 前端工程
|
||||||
|
|
||||||
|
这是 SaaS 题库的新跨端前端地基。当前阶段先提供 H5 多入口壳、租户解析、统一 API client、Supabase Auth client 初始化和安全环境变量边界。
|
||||||
|
|
||||||
|
## 三个 H5 入口
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:taro:h5
|
||||||
|
npm run build:taro:h5:student
|
||||||
|
npm run build:taro:h5:tenant
|
||||||
|
npm run build:taro:h5:platform
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/taro/dist/h5-student
|
||||||
|
apps/taro/dist/h5-tenant-admin
|
||||||
|
apps/taro/dist/h5-platform-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
可以分别部署到学生端域名、租户后台域名、平台后台域名。三个入口共用 `src/services/api.ts`,不得在页面中散写 `Taro.request`。
|
||||||
|
|
||||||
|
## 前端环境变量
|
||||||
|
|
||||||
|
只允许使用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TARO_APP_PORTAL=student | tenant-admin | platform-admin
|
||||||
|
TARO_APP_API_BASE_URL=https://api.example.com
|
||||||
|
TARO_APP_SUPABASE_URL=https://<supabase-host>
|
||||||
|
TARO_APP_SUPABASE_PUBLISHABLE_KEY=<publishable-key>
|
||||||
|
TARO_APP_TENANT_CODE=<可选,小程序/预览环境使用>
|
||||||
|
```
|
||||||
|
|
||||||
|
禁止把 service role、数据库连接串、支付私钥、对象存储密钥放进 Taro 构建环境。
|
||||||
|
|
||||||
|
## 接入原则
|
||||||
|
|
||||||
|
- H5 可用 Supabase client 管理 Auth session。
|
||||||
|
- 业务数据默认走 `apps/api`。
|
||||||
|
- `x-tenant-id` 只是租户上下文,不是身份凭证。
|
||||||
|
- 登录后不要传 `x-user-id` 或 body/query `userId` 伪造当前用户。
|
||||||
|
- 题库练习、订单支付、内容导入、CRM、资料签名、后台配置必须走后端命令层。
|
||||||
11
apps/taro/babel.config.cjs
Normal file
11
apps/taro/babel.config.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'taro',
|
||||||
|
{
|
||||||
|
framework: 'react',
|
||||||
|
ts: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
6
apps/taro/config/dev.ts
Normal file
6
apps/taro/config/dev.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: '"development"',
|
||||||
|
},
|
||||||
|
defineConstants: {},
|
||||||
|
};
|
||||||
62
apps/taro/config/index.ts
Normal file
62
apps/taro/config/index.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { UserConfigExport } from '@tarojs/cli';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const portal = process.env.TARO_APP_PORTAL || 'student';
|
||||||
|
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const distDirByPortal: Record<string, string> = {
|
||||||
|
student: 'dist/h5-student',
|
||||||
|
'tenant-admin': 'dist/h5-tenant-admin',
|
||||||
|
'platform-admin': 'dist/h5-platform-admin',
|
||||||
|
};
|
||||||
|
const outputRoot = distDirByPortal[portal] || distDirByPortal.student;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
projectName: 'tiku-saas-taro',
|
||||||
|
date: '2026-06-29',
|
||||||
|
designWidth: 750,
|
||||||
|
deviceRatio: {
|
||||||
|
640: 2.34 / 2,
|
||||||
|
750: 1,
|
||||||
|
828: 1.81 / 2,
|
||||||
|
},
|
||||||
|
sourceRoot: 'src',
|
||||||
|
outputRoot,
|
||||||
|
framework: 'react',
|
||||||
|
compiler: {
|
||||||
|
type: 'webpack5',
|
||||||
|
prebundle: {
|
||||||
|
enable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(configDir, '..', 'src'),
|
||||||
|
},
|
||||||
|
defineConstants: {},
|
||||||
|
copy: {
|
||||||
|
patterns: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
publicPath: './',
|
||||||
|
staticDirectory: 'static',
|
||||||
|
output: {
|
||||||
|
filename: 'js/[name].[contenthash:8].js',
|
||||||
|
chunkFilename: 'js/[name].[contenthash:8].js',
|
||||||
|
},
|
||||||
|
router: {
|
||||||
|
mode: 'browser',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mini: {
|
||||||
|
postcss: {
|
||||||
|
pxtransform: {
|
||||||
|
enable: true,
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
cssModules: {
|
||||||
|
enable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies UserConfigExport;
|
||||||
6
apps/taro/config/prod.ts
Normal file
6
apps/taro/config/prod.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: '"production"',
|
||||||
|
},
|
||||||
|
defineConstants: {},
|
||||||
|
};
|
||||||
40
apps/taro/package.json
Normal file
40
apps/taro/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@tiku-saas/taro",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build:h5": "taro build --type h5",
|
||||||
|
"build:h5:student": "cross-env TARO_APP_PORTAL=student taro build --type h5",
|
||||||
|
"build:h5:tenant": "cross-env TARO_APP_PORTAL=tenant-admin taro build --type h5",
|
||||||
|
"build:h5:platform": "cross-env TARO_APP_PORTAL=platform-admin taro build --type h5",
|
||||||
|
"dev:h5": "taro build --type h5 --watch",
|
||||||
|
"check": "tsc -p tsconfig.json --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.28.3",
|
||||||
|
"@babel/preset-react": "^7.29.7",
|
||||||
|
"@supabase/supabase-js": "^2.108.2",
|
||||||
|
"@tarojs/cli": "4.2.0",
|
||||||
|
"@tarojs/components": "4.2.0",
|
||||||
|
"@tarojs/helper": "4.2.0",
|
||||||
|
"@tarojs/plugin-framework-react": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-h5": "4.2.0",
|
||||||
|
"@tarojs/plugin-platform-weapp": "4.2.0",
|
||||||
|
"@tarojs/react": "4.2.0",
|
||||||
|
"@tarojs/runtime": "4.2.0",
|
||||||
|
"@tarojs/shared": "4.2.0",
|
||||||
|
"@tarojs/taro": "4.2.0",
|
||||||
|
"@tarojs/webpack5-runner": "4.2.0",
|
||||||
|
"@types/react": "^18.3.24",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"babel-preset-taro": "^4.2.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-refresh": "^0.14.2",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"webpack": "5.91.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
apps/taro/project.config.json
Normal file
14
apps/taro/project.config.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"miniprogramRoot": "dist/weapp/",
|
||||||
|
"projectname": "tiku-saas-taro",
|
||||||
|
"description": "工学教育 SaaS 题库 Taro 多端前端",
|
||||||
|
"appid": "touristappid",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false,
|
||||||
|
"es6": true,
|
||||||
|
"enhance": true,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": true
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram"
|
||||||
|
}
|
||||||
14
apps/taro/src/app.config.ts
Normal file
14
apps/taro/src/app.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
pages: [
|
||||||
|
'pages/bootstrap/index',
|
||||||
|
'pages/student/home/index',
|
||||||
|
'pages/tenant-admin/workbench/index',
|
||||||
|
'pages/platform-admin/workbench/index',
|
||||||
|
],
|
||||||
|
window: {
|
||||||
|
backgroundTextStyle: 'light',
|
||||||
|
navigationBarBackgroundColor: '#0f172a',
|
||||||
|
navigationBarTitleText: '工学题库',
|
||||||
|
navigationBarTextStyle: 'white',
|
||||||
|
},
|
||||||
|
});
|
||||||
17
apps/taro/src/app.css
Normal file
17
apps/taro/src/app.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
page {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f8fb;
|
||||||
|
color: #172033;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
view,
|
||||||
|
text,
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
6
apps/taro/src/app.tsx
Normal file
6
apps/taro/src/app.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import './app.css';
|
||||||
|
|
||||||
|
export default function App({ children }: PropsWithChildren) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
31
apps/taro/src/env.ts
Normal file
31
apps/taro/src/env.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export type Portal = 'student' | 'tenant-admin' | 'platform-admin';
|
||||||
|
|
||||||
|
declare const process: {
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appEnv = {
|
||||||
|
portal: (process.env.TARO_APP_PORTAL || 'student') as Portal,
|
||||||
|
apiBaseUrl: process.env.TARO_APP_API_BASE_URL || 'http://127.0.0.1:8787',
|
||||||
|
supabaseUrl: process.env.TARO_APP_SUPABASE_URL || '',
|
||||||
|
supabasePublishableKey: process.env.TARO_APP_SUPABASE_PUBLISHABLE_KEY || '',
|
||||||
|
tenantCode: process.env.TARO_APP_TENANT_CODE || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function assertFrontendSecretsAreAbsent() {
|
||||||
|
const forbidden = [
|
||||||
|
'SUPABASE_SERVICE_ROLE_KEY',
|
||||||
|
'SUPABASE_SECRET_KEY',
|
||||||
|
'DATABASE_URL',
|
||||||
|
'ALIYUN_OSS_ACCESS_KEY_SECRET',
|
||||||
|
'TENCENT_COS_SECRET_KEY',
|
||||||
|
'WECHAT_PAY_PRIVATE_KEY',
|
||||||
|
'ALIPAY_APP_PRIVATE_KEY',
|
||||||
|
'AUTH_SESSION_SECRET',
|
||||||
|
'PLATFORM_ADMIN_API_KEY',
|
||||||
|
];
|
||||||
|
const leaked = forbidden.filter(key => process.env[key]);
|
||||||
|
if (leaked.length) {
|
||||||
|
throw new Error(`Forbidden secret env in Taro build: ${leaked.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
apps/taro/src/pages/bootstrap/index.config.ts
Normal file
3
apps/taro/src/pages/bootstrap/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '启动',
|
||||||
|
});
|
||||||
48
apps/taro/src/pages/bootstrap/index.css
Normal file
48
apps/taro/src/pages/bootstrap/index.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.bootstrap-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 56px 28px;
|
||||||
|
background: #101827;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-shell {
|
||||||
|
display: flex;
|
||||||
|
min-height: calc(100vh - 112px);
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: #80b7ff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
max-width: 620px;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #fecaca;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
width: 240px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #2f7cf6;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 76px;
|
||||||
|
}
|
||||||
51
apps/taro/src/pages/bootstrap/index.tsx
Normal file
51
apps/taro/src/pages/bootstrap/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { Button, Text, View } from '@tarojs/components';
|
||||||
|
import { appEnv, assertFrontendSecretsAreAbsent } from '@/env';
|
||||||
|
import { resolveTenant } from '@/services/api';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
function hostFromRuntime() {
|
||||||
|
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') return window.location.host;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function landingPath() {
|
||||||
|
if (appEnv.portal === 'tenant-admin') return '/pages/tenant-admin/workbench/index';
|
||||||
|
if (appEnv.portal === 'platform-admin') return '/pages/platform-admin/workbench/index';
|
||||||
|
return '/pages/student/home/index';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BootstrapPage() {
|
||||||
|
const [status, setStatus] = useState('正在解析租户');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
assertFrontendSecretsAreAbsent();
|
||||||
|
resolveTenant({ host: hostFromRuntime() })
|
||||||
|
.then(() => {
|
||||||
|
setStatus('租户解析完成');
|
||||||
|
Taro.redirectTo({ url: landingPath() });
|
||||||
|
})
|
||||||
|
.catch((nextError: Error) => {
|
||||||
|
setError(nextError.message);
|
||||||
|
setStatus('租户解析失败');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='bootstrap-page'>
|
||||||
|
<View className='bootstrap-shell'>
|
||||||
|
<Text className='eyebrow'>Tiku SaaS</Text>
|
||||||
|
<Text className='title'>正在进入题库系统</Text>
|
||||||
|
<Text className='status'>{status}</Text>
|
||||||
|
{error ? <Text className='error'>{error}</Text> : null}
|
||||||
|
{error ? (
|
||||||
|
<Button className='primary-button' onClick={() => Taro.reLaunch({ url: '/pages/bootstrap/index' })}>
|
||||||
|
重新尝试
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '平台后台',
|
||||||
|
});
|
||||||
53
apps/taro/src/pages/platform-admin/workbench/index.css
Normal file
53
apps/taro/src/pages/platform-admin/workbench/index.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
.platform-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-header {
|
||||||
|
padding-bottom: 28px;
|
||||||
|
border-bottom: 1px solid #d8e0ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-title {
|
||||||
|
display: block;
|
||||||
|
color: #101827;
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-tile {
|
||||||
|
min-height: 128px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-title {
|
||||||
|
display: block;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-status {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
29
apps/taro/src/pages/platform-admin/workbench/index.tsx
Normal file
29
apps/taro/src/pages/platform-admin/workbench/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Text, View } from '@tarojs/components';
|
||||||
|
import { apiRequest } from '@/services/api';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
export default function PlatformWorkbenchPage() {
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiRequest('/api/platform-admin/overview').then(() => setReady(true)).catch(() => setReady(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='platform-page'>
|
||||||
|
<View className='platform-header'>
|
||||||
|
<Text className='platform-title'>SaaS 平台后台</Text>
|
||||||
|
<Text className='platform-subtitle'>租户、套餐、账单、公共题库授权和平台审计入口</Text>
|
||||||
|
</View>
|
||||||
|
<View className='platform-grid'>
|
||||||
|
{['租户管理', 'SaaS 套餐', '服务费账单', '公共题库', '用量记录', '安全审计'].map(name => (
|
||||||
|
<View className='platform-tile' key={name}>
|
||||||
|
<Text className='tile-title'>{name}</Text>
|
||||||
|
<Text className='tile-status'>{ready ? '接口已连接' : '需要平台管理员登录'}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
apps/taro/src/pages/student/home/index.config.ts
Normal file
3
apps/taro/src/pages/student/home/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '学习首页',
|
||||||
|
});
|
||||||
98
apps/taro/src/pages/student/home/index.css
Normal file
98
apps/taro/src/pages/student/home/index.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
.student-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: block;
|
||||||
|
color: #14213d;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subline {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-button {
|
||||||
|
width: 132px;
|
||||||
|
height: 64px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-band {
|
||||||
|
margin-top: 36px;
|
||||||
|
padding: 34px 30px;
|
||||||
|
border-left: 8px solid #2f7cf6;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
display: block;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-copy {
|
||||||
|
display: block;
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-tile {
|
||||||
|
min-height: 126px;
|
||||||
|
padding: 22px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-name {
|
||||||
|
display: block;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-type {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
54
apps/taro/src/pages/student/home/index.tsx
Normal file
54
apps/taro/src/pages/student/home/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Button, Text, View } from '@tarojs/components';
|
||||||
|
import { getTenantContext } from '@/services/api';
|
||||||
|
import { loadStudentDashboard, type DashboardSnapshot } from '@/services/catalog';
|
||||||
|
import { loadCurrentUser } from '@/services/auth';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
export default function StudentHomePage() {
|
||||||
|
const tenant = getTenantContext();
|
||||||
|
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
|
||||||
|
const [userName, setUserName] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStudentDashboard().then(setSnapshot).catch(() => setSnapshot({ entries: [], banners: [], announcements: [], profile: null }));
|
||||||
|
loadCurrentUser().then(payload => setUserName(payload.user?.name || payload.item?.name || '')).catch(() => setUserName(''));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const entryNames = useMemo(() => (snapshot?.entries || []).slice(0, 6), [snapshot]);
|
||||||
|
const brandName = tenant?.branding.brandName || tenant?.branding.shortName || '工学题库';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='student-page'>
|
||||||
|
<View className='topbar'>
|
||||||
|
<View>
|
||||||
|
<Text className='brand'>{brandName}</Text>
|
||||||
|
<Text className='subline'>{userName ? `${userName},继续学习` : '题库、手册、资料和会员统一入口'}</Text>
|
||||||
|
</View>
|
||||||
|
<Button className='ghost-button'>会员</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='hero-band'>
|
||||||
|
<Text className='hero-title'>今日学习</Text>
|
||||||
|
<Text className='hero-copy'>后端已接入多租户题库、练习快照、错题、收藏、单词和知识手册接口。</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='section'>
|
||||||
|
<Text className='section-title'>学习入口</Text>
|
||||||
|
<View className='entry-grid'>
|
||||||
|
{entryNames.length ? entryNames.map(item => (
|
||||||
|
<View className='entry-tile' key={item.id}>
|
||||||
|
<Text className='entry-name'>{item.name}</Text>
|
||||||
|
<Text className='entry-type'>{item.entryType || 'content'}</Text>
|
||||||
|
</View>
|
||||||
|
)) : ['刷题', '背单词', '知识手册', '分数线', '资料下载', '会员中心'].map(name => (
|
||||||
|
<View className='entry-tile' key={name}>
|
||||||
|
<Text className='entry-name'>{name}</Text>
|
||||||
|
<Text className='entry-type'>待联调</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '租户后台',
|
||||||
|
});
|
||||||
72
apps/taro/src/pages/tenant-admin/workbench/index.css
Normal file
72
apps/taro/src/pages/tenant-admin/workbench/index.css
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
.admin-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
padding-bottom: 26px;
|
||||||
|
border-bottom: 1px solid #dbe3ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-title {
|
||||||
|
display: block;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
min-height: 116px;
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
display: block;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-list {
|
||||||
|
margin-top: 28px;
|
||||||
|
border-top: 1px solid #dbe3ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24px 0;
|
||||||
|
border-bottom: 1px solid #dbe3ef;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-status {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
46
apps/taro/src/pages/tenant-admin/workbench/index.tsx
Normal file
46
apps/taro/src/pages/tenant-admin/workbench/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Text, View } from '@tarojs/components';
|
||||||
|
import { apiRequest, getTenantContext } from '@/services/api';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
interface Dashboard {
|
||||||
|
cards?: Record<string, unknown>;
|
||||||
|
trends?: unknown[];
|
||||||
|
recentActivities?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TenantWorkbenchPage() {
|
||||||
|
const tenant = getTenantContext();
|
||||||
|
const [dashboard, setDashboard] = useState<Dashboard | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiRequest<{ item?: Dashboard }>('/api/tenant-admin/dashboard', { query: { timeRange: '30d' } })
|
||||||
|
.then(payload => setDashboard(payload.item || null))
|
||||||
|
.catch(() => setDashboard(null));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='admin-page'>
|
||||||
|
<View className='admin-header'>
|
||||||
|
<Text className='admin-title'>{tenant?.branding.brandName || '租户运营后台'}</Text>
|
||||||
|
<Text className='admin-subtitle'>品牌、题库、学生、订单、营销和数据看板的统一工作台</Text>
|
||||||
|
</View>
|
||||||
|
<View className='metric-row'>
|
||||||
|
{['收益', '注册', '答题', '激活码'].map(label => (
|
||||||
|
<View className='metric' key={label}>
|
||||||
|
<Text className='metric-label'>{label}</Text>
|
||||||
|
<Text className='metric-value'>{dashboard ? '已接入' : '待登录'}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View className='module-list'>
|
||||||
|
{['内容管理', '导入复检', '学生运营', '销售分佣', 'CRM 队列', '品牌域名'].map(name => (
|
||||||
|
<View className='module-row' key={name}>
|
||||||
|
<Text>{name}</Text>
|
||||||
|
<Text className='module-status'>按权限显示</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
apps/taro/src/services/api.ts
Normal file
132
apps/taro/src/services/api.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import { appEnv } from '@/env';
|
||||||
|
import type { ApiErrorPayload, ApiSession, TenantContext } from '@/types';
|
||||||
|
import { getStorage, removeStorage, setStorage } from './storage';
|
||||||
|
|
||||||
|
const TENANT_KEY = 'tiku:tenant';
|
||||||
|
const SESSION_KEY = 'tiku:session';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
code: string;
|
||||||
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor(payload: ApiErrorPayload) {
|
||||||
|
super(payload.message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.status = payload.status;
|
||||||
|
this.code = payload.code;
|
||||||
|
this.details = payload.details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTenantContext() {
|
||||||
|
return getStorage<TenantContext>(TENANT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveTenantContext(tenant: TenantContext) {
|
||||||
|
setStorage(TENANT_KEY, tenant);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearTenantContext() {
|
||||||
|
removeStorage(TENANT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSession() {
|
||||||
|
return getStorage<ApiSession>(SESSION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSession(session: ApiSession) {
|
||||||
|
setStorage(SESSION_KEY, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSession() {
|
||||||
|
removeStorage(SESSION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBaseUrl(baseUrl: string) {
|
||||||
|
return baseUrl.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(path: string, query?: Record<string, string | number | boolean | null | undefined>) {
|
||||||
|
const url = `${normalizeBaseUrl(appEnv.apiBaseUrl)}${path.startsWith('/') ? path : `/${path}`}`;
|
||||||
|
const params = Object.entries(query || {})
|
||||||
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
return params.length ? `${url}?${params.join('&')}` : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiRequest<T>(
|
||||||
|
path: string,
|
||||||
|
options: {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
|
query?: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
body?: unknown;
|
||||||
|
tenantId?: string | null;
|
||||||
|
token?: string | null;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
} = {},
|
||||||
|
): Promise<T> {
|
||||||
|
const tenant = getTenantContext();
|
||||||
|
const session = getSession();
|
||||||
|
const token = options.token ?? session?.token ?? null;
|
||||||
|
const tenantId = options.tenantId ?? tenant?.tenantId ?? null;
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
...(tenantId ? { 'x-tenant-id': tenantId } : {}),
|
||||||
|
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await Taro.request({
|
||||||
|
url: buildUrl(path, options.query),
|
||||||
|
method: options.method || 'GET',
|
||||||
|
data: options.body === undefined ? undefined : options.body,
|
||||||
|
header: headers,
|
||||||
|
});
|
||||||
|
const payload = (response.data || {}) as Record<string, unknown>;
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
if (response.statusCode === 401) clearSession();
|
||||||
|
throw new ApiError({
|
||||||
|
status: response.statusCode,
|
||||||
|
code: String(payload.code || 'API_ERROR'),
|
||||||
|
message: String(payload.message || '请求失败'),
|
||||||
|
details: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return payload as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTenant(input: { host?: string; tenantCode?: string } = {}) {
|
||||||
|
const payload = await apiRequest<{
|
||||||
|
item?: TenantContext;
|
||||||
|
tenant?: {
|
||||||
|
id?: string;
|
||||||
|
tenantId?: string;
|
||||||
|
slug?: string;
|
||||||
|
};
|
||||||
|
branding?: TenantContext['branding'];
|
||||||
|
features?: TenantContext['features'];
|
||||||
|
adminFeatures?: TenantContext['adminFeatures'];
|
||||||
|
publicConfig?: TenantContext['publicConfig'];
|
||||||
|
}>('/api/tenant/resolve', {
|
||||||
|
query: {
|
||||||
|
host: input.host,
|
||||||
|
tenantCode: input.tenantCode || appEnv.tenantCode,
|
||||||
|
},
|
||||||
|
tenantId: null,
|
||||||
|
});
|
||||||
|
const tenantId = payload.item?.tenantId || payload.tenant?.tenantId || payload.tenant?.id;
|
||||||
|
if (!tenantId) throw new ApiError({ status: 500, code: 'TENANT_RESOLVE_INVALID', message: '租户解析结果缺少 tenantId' });
|
||||||
|
const context: TenantContext = {
|
||||||
|
tenantId,
|
||||||
|
tenantSlug: payload.item?.tenantSlug || payload.tenant?.slug,
|
||||||
|
host: input.host,
|
||||||
|
branding: payload.item?.branding || payload.branding || {},
|
||||||
|
features: payload.item?.features || payload.features || {},
|
||||||
|
adminFeatures: payload.item?.adminFeatures || payload.adminFeatures || {},
|
||||||
|
publicConfig: payload.item?.publicConfig || payload.publicConfig || {},
|
||||||
|
};
|
||||||
|
saveTenantContext(context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
31
apps/taro/src/services/auth.ts
Normal file
31
apps/taro/src/services/auth.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { apiRequest, clearSession, saveSession } from './api';
|
||||||
|
import type { ApiEnvelope, CurrentUser } from '@/types';
|
||||||
|
|
||||||
|
export async function sendSmsCode(phone: string, purpose: 'login' | 'bind_phone' = 'login') {
|
||||||
|
return apiRequest<ApiEnvelope<never>>('/api/auth/sms/send', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { phone, purpose },
|
||||||
|
tenantId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifySmsCode(phone: string, code: string, purpose: 'login' | 'bind_phone' = 'login') {
|
||||||
|
const payload = await apiRequest<ApiEnvelope<CurrentUser>>('/api/auth/sms/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { phone, code, purpose },
|
||||||
|
});
|
||||||
|
if (payload.session?.token) saveSession(payload.session);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCurrentUser() {
|
||||||
|
return apiRequest<ApiEnvelope<CurrentUser>>('/api/auth/me');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
try {
|
||||||
|
await apiRequest('/api/auth/logout', { method: 'POST' });
|
||||||
|
} finally {
|
||||||
|
clearSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/taro/src/services/catalog.ts
Normal file
32
apps/taro/src/services/catalog.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { apiRequest } from './api';
|
||||||
|
|
||||||
|
export interface ContentEntry {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
entryType?: string;
|
||||||
|
route?: string;
|
||||||
|
icon?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardSnapshot {
|
||||||
|
entries: ContentEntry[];
|
||||||
|
banners: unknown[];
|
||||||
|
announcements: unknown[];
|
||||||
|
profile: unknown | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadStudentDashboard(regionId?: string): Promise<DashboardSnapshot> {
|
||||||
|
const [entries, banners, announcements, profile] = await Promise.all([
|
||||||
|
apiRequest<{ items?: ContentEntry[] }>('/api/catalog/content-entries', { query: { regionId } }),
|
||||||
|
apiRequest<{ items?: unknown[] }>('/api/catalog/banners'),
|
||||||
|
apiRequest<{ items?: unknown[] }>('/api/catalog/announcements'),
|
||||||
|
apiRequest<{ item?: unknown }>('/api/profile/me').catch(() => ({ item: null })),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
entries: entries.items || [],
|
||||||
|
banners: banners.items || [],
|
||||||
|
announcements: announcements.items || [],
|
||||||
|
profile: profile.item || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
19
apps/taro/src/services/storage.ts
Normal file
19
apps/taro/src/services/storage.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
|
||||||
|
export function getStorage<T>(key: string): T | null {
|
||||||
|
try {
|
||||||
|
const value = Taro.getStorageSync<string>(key);
|
||||||
|
if (!value) return null;
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStorage<T>(key: string, value: T) {
|
||||||
|
Taro.setStorageSync(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeStorage(key: string) {
|
||||||
|
Taro.removeStorageSync(key);
|
||||||
|
}
|
||||||
25
apps/taro/src/services/supabase.ts
Normal file
25
apps/taro/src/services/supabase.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import { appEnv } from '@/env';
|
||||||
|
|
||||||
|
let client: SupabaseClient | null = null;
|
||||||
|
|
||||||
|
export function getSupabaseClient() {
|
||||||
|
if (!appEnv.supabaseUrl || !appEnv.supabasePublishableKey) return null;
|
||||||
|
if (!client) {
|
||||||
|
client = createClient(appEnv.supabaseUrl, appEnv.supabasePublishableKey, {
|
||||||
|
auth: {
|
||||||
|
persistSession: true,
|
||||||
|
autoRefreshToken: true,
|
||||||
|
detectSessionInUrl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSupabaseAccessToken() {
|
||||||
|
const supabase = getSupabaseClient();
|
||||||
|
if (!supabase) return null;
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
return data.session?.access_token || null;
|
||||||
|
}
|
||||||
48
apps/taro/src/types.ts
Normal file
48
apps/taro/src/types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export interface TenantBranding {
|
||||||
|
brandName?: string;
|
||||||
|
shortName?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
slogan?: string;
|
||||||
|
theme?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantContext {
|
||||||
|
tenantId: string;
|
||||||
|
tenantSlug?: string;
|
||||||
|
host?: string;
|
||||||
|
branding: TenantBranding;
|
||||||
|
features: Record<string, unknown>;
|
||||||
|
adminFeatures: Record<string, unknown>;
|
||||||
|
publicConfig: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiSession {
|
||||||
|
token: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentUser {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
roles?: string[];
|
||||||
|
permissions?: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiEnvelope<T> {
|
||||||
|
item?: T;
|
||||||
|
items?: T[];
|
||||||
|
tenant?: unknown;
|
||||||
|
user?: CurrentUser;
|
||||||
|
session?: ApiSession;
|
||||||
|
code?: string;
|
||||||
|
message?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiErrorPayload {
|
||||||
|
status: number;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: unknown;
|
||||||
|
}
|
||||||
19
apps/taro/tsconfig.json
Normal file
19
apps/taro/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"types": ["@tarojs/taro", "@tarojs/taro-h5", "node"]
|
||||||
|
},
|
||||||
|
"include": ["config/**/*.ts", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -177,8 +177,12 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx supabase db reset
|
npx supabase db reset
|
||||||
npm audit --audit-level=high
|
npm run audit:runtime
|
||||||
npm run check:refactor
|
npm run check:refactor
|
||||||
|
npm run check:taro
|
||||||
|
npm run build:taro:h5:student
|
||||||
|
npm run build:taro:h5:tenant
|
||||||
|
npm run build:taro:h5:platform
|
||||||
npm run test:readiness
|
npm run test:readiness
|
||||||
npm run test:pb:dry-run
|
npm run test:pb:dry-run
|
||||||
npm run test:worker:imports
|
npm run test:worker:imports
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
| 内容导入 | 可联调 | 题目、单词、知识手册、分数线、视频 JSON/CSV/Excel preview/import、issue、job、审计、幂等、`executionMode=async`、imports worker、导入后复检、模板下载、字段映射 API、PocketBase JSON dry-run 报告 | 字段映射 UI、真实数据 dry-run 执行验收和导入性能压测 |
|
| 内容导入 | 可联调 | 题目、单词、知识手册、分数线、视频 JSON/CSV/Excel preview/import、issue、job、审计、幂等、`executionMode=async`、imports worker、导入后复检、模板下载、字段映射 API、PocketBase JSON dry-run 报告 | 字段映射 UI、真实数据 dry-run 执行验收和导入性能压测 |
|
||||||
| 数据看板 | 可联调 | 租户 dashboard 聚合接口,收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态 | 预聚合 worker、缓存、慢 SQL 监控和销售转化看板 |
|
| 数据看板 | 可联调 | 租户 dashboard 聚合接口,收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态 | 预聚合 worker、缓存、慢 SQL 监控和销售转化看板 |
|
||||||
| AI 择校推荐 | 未开始 | 暂无 | 数据上下文、AI JSON schema、报告渲染、PDF 生成 |
|
| AI 择校推荐 | 未开始 | 暂无 | 数据上下文、AI JSON schema、报告渲染、PDF 生成 |
|
||||||
| Taro 前端 | 未开始 | 旧 Web 已有新 API 适配雏形 | `apps/taro`、跨端 API client、H5/小程序页面和端到端测试 |
|
| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化和基础工作台壳 | 学生端完整页面、租户后台/平台后台业务页面、小程序兼容验证和端到端测试 |
|
||||||
|
|
||||||
## 前端接入建议
|
## 前端接入建议
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
- 对象存储:上传/下载签名已接入阿里云 OSS、腾讯云 COS、Supabase Storage;上传确认、PDF/图片预览签名和 assets worker 复检已完成,继续补 PDF 渲染、视频播放防盗链、杀毒扫描和水印。
|
- 对象存储:上传/下载签名已接入阿里云 OSS、腾讯云 COS、Supabase Storage;上传确认、PDF/图片预览签名和 assets worker 复检已完成,继续补 PDF 渲染、视频播放防盗链、杀毒扫描和水印。
|
||||||
- 真实数据 dry-run:导出 PocketBase 用户、题库、单词、知识手册、分数线、订单、权益,先跑 `npm run pb:import:dry-run`,再跑迁移和校验报告。
|
- 真实数据 dry-run:导出 PocketBase 用户、题库、单词、知识手册、分数线、订单、权益,先跑 `npm run pb:import:dry-run`,再跑迁移和校验报告。
|
||||||
- 生产环境配置:`.env.example` 和 `npm run readiness:production` / `npm run readiness:production:db` 已补;继续补数据库迁移流程、备份恢复、日志、告警和 API 容器部署说明。
|
- 生产环境配置:`.env.example` 和 `npm run readiness:production` / `npm run readiness:production:db` 已补;继续补数据库迁移流程、备份恢复、日志、告警和 API 容器部署说明。
|
||||||
- Taro scaffold:建立 `apps/taro`,先完成租户解析、首页、题库、背单词、知识手册、个人中心主链路。
|
- Taro scaffold:`apps/taro` 地基已建立;下一步补学生端首页、题库、背单词、知识手册、个人中心主链路和小程序兼容验证。
|
||||||
|
|
||||||
### P1:商用收费和运营能力
|
### P1:商用收费和运营能力
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ provider event id 幂等
|
|||||||
|
|
||||||
## 上线前安全验收清单
|
## 上线前安全验收清单
|
||||||
|
|
||||||
- `npm audit` 为 0 高危/严重漏洞。
|
- `npm run audit:runtime` 为 0 high/critical 漏洞;Taro 构建工具链 audit 单独跟踪,不能用破坏性降级绕过。
|
||||||
- `npm run check:refactor` 通过。
|
- `npm run check:refactor` 通过。
|
||||||
- `npm run readiness:production` 没有 blocker。
|
- `npm run readiness:production` 没有 blocker。
|
||||||
- `npm run readiness:production:db` 没有 blocker。
|
- `npm run readiness:production:db` 没有 blocker。
|
||||||
|
|||||||
@@ -158,10 +158,11 @@
|
|||||||
|
|
||||||
### 架构目标
|
### 架构目标
|
||||||
|
|
||||||
- 建议新建 `apps/taro`,不要继续在旧 React Web 上堆兼容。
|
- 已新建 `apps/taro`,不要继续在旧 React Web 上堆兼容。
|
||||||
- H5 和小程序共用同一套业务 API client。
|
- H5 和小程序共用同一套业务 API client。
|
||||||
- 租户通过域名、小程序配置或启动参数解析。
|
- 租户通过域名、小程序配置或启动参数解析。
|
||||||
- 页面主题、品牌、功能开关都从后端租户配置读取。
|
- 页面主题、品牌、功能开关都从后端租户配置读取。
|
||||||
|
- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client;下一步补完整页面、状态管理和小程序兼容验证。
|
||||||
|
|
||||||
### 第一批页面
|
### 第一批页面
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,17 @@ F:\project
|
|||||||
index.ts worker CLI/常驻循环入口
|
index.ts worker CLI/常驻循环入口
|
||||||
package.json
|
package.json
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
taro/ Taro 4 React 跨端前端
|
||||||
|
src/
|
||||||
|
services/ 统一 API client、Auth、Supabase client、缓存
|
||||||
|
pages/
|
||||||
|
bootstrap/ 租户解析和三入口分流
|
||||||
|
student/ 学生学习端 H5/小程序页面
|
||||||
|
tenant-admin/ 租户后台工作台和运营页面
|
||||||
|
platform-admin/ 平台超级管理员后台页面
|
||||||
|
config/ Taro 构建配置,按 portal 输出三套 H5 产物
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
|
||||||
packages/
|
packages/
|
||||||
config/ 共享配置和 env 工具
|
config/ 共享配置和 env 工具
|
||||||
@@ -127,6 +138,7 @@ git status --short --branch
|
|||||||
|
|
||||||
- `apps/api/**`
|
- `apps/api/**`
|
||||||
- `apps/worker/**`
|
- `apps/worker/**`
|
||||||
|
- `apps/taro/**`
|
||||||
- `packages/**`
|
- `packages/**`
|
||||||
- `supabase/**`
|
- `supabase/**`
|
||||||
- `scripts/import-pocketbase/**`
|
- `scripts/import-pocketbase/**`
|
||||||
|
|||||||
129
docs/refactor/taro-h5-deployment.md
Normal file
129
docs/refactor/taro-h5-deployment.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Taro H5 三入口部署说明
|
||||||
|
|
||||||
|
更新时间:2026-06-29
|
||||||
|
|
||||||
|
当前 `apps/taro` 按一个 Taro 工程、三套 H5 产物组织:
|
||||||
|
|
||||||
|
- 学生学习端:刷题、背单词、知识手册、分数线、资料、会员和个人中心。
|
||||||
|
- 租户后台:品牌、域名、题库、导入、学生、订单、营销、销售和数据看板。
|
||||||
|
- 平台后台:租户、SaaS 套餐、订阅账单、公共题库授权和平台审计。
|
||||||
|
|
||||||
|
## 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:taro:h5:student
|
||||||
|
npm run build:taro:h5:tenant
|
||||||
|
npm run build:taro:h5:platform
|
||||||
|
```
|
||||||
|
|
||||||
|
输出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/taro/dist/h5-student
|
||||||
|
apps/taro/dist/h5-tenant-admin
|
||||||
|
apps/taro/dist/h5-platform-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐部署:
|
||||||
|
|
||||||
|
| 域名 | 目录 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `www.example.com` 或租户自有学生端域名 | `h5-student` | 面向学生和 C 端用户 |
|
||||||
|
| `admin.example.com` | `h5-tenant-admin` | 面向租户公司运营、教师、销售、代理、管理员 |
|
||||||
|
| `console.example.com` | `h5-platform-admin` | 面向平台超级管理员 |
|
||||||
|
|
||||||
|
三个入口可以放在同一台服务器的三个静态目录,也可以放到 CDN/对象存储静态网站。API 推荐独立域名,例如 `api.example.com`。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
构建时只允许注入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TARO_APP_PORTAL=student | tenant-admin | platform-admin
|
||||||
|
TARO_APP_API_BASE_URL=https://api.example.com
|
||||||
|
TARO_APP_SUPABASE_URL=https://<supabase-auth-host>
|
||||||
|
TARO_APP_SUPABASE_PUBLISHABLE_KEY=<publishable-key>
|
||||||
|
TARO_APP_TENANT_CODE=<可选,小程序或预览环境使用>
|
||||||
|
```
|
||||||
|
|
||||||
|
禁止进入前端构建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY
|
||||||
|
SUPABASE_SECRET_KEY
|
||||||
|
DATABASE_URL
|
||||||
|
ALIYUN_OSS_ACCESS_KEY_SECRET
|
||||||
|
TENCENT_COS_SECRET_KEY
|
||||||
|
WECHAT_PAY_PRIVATE_KEY
|
||||||
|
ALIPAY_APP_PRIVATE_KEY
|
||||||
|
AUTH_SESSION_SECRET
|
||||||
|
PLATFORM_ADMIN_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
`apps/taro/src/env.ts` 会在启动时检查这些危险变量,防止误把服务端密钥打包到前端。
|
||||||
|
|
||||||
|
## Nginx 建议
|
||||||
|
|
||||||
|
H5 使用 history 路由时,静态服务器需要把未知路径回退到 `index.html`。
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
server_name www.example.com;
|
||||||
|
root /www/tiku/h5-student;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name admin.example.com;
|
||||||
|
root /www/tiku/h5-tenant-admin;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
server_name console.example.com;
|
||||||
|
root /www/tiku/h5-platform-admin;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
API CORS 必须只允许实际域名,不允许生产环境 `CORS=*`。生产前运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run readiness:production
|
||||||
|
npm run readiness:production:db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端请求边界
|
||||||
|
|
||||||
|
- 所有页面统一通过 `apps/taro/src/services/api.ts` 调用后端。
|
||||||
|
- H5 可以用 Supabase client 管理 Auth session,但业务数据默认走 `apps/api`。
|
||||||
|
- `x-tenant-id` 只是租户上下文,不是身份来源。
|
||||||
|
- 登录后禁止传 `x-user-id` 或 body/query `userId` 表示当前用户。
|
||||||
|
- 订单、支付、权益、内容导入、后台配置、CRM、对象存储签名、视频播放签名必须走后端命令层。
|
||||||
|
|
||||||
|
## 安全审计边界
|
||||||
|
|
||||||
|
H5 线上只发布 `apps/taro/dist/**` 静态文件,不要把 `apps/taro/node_modules` 或源码目录部署到 Web 根目录。后端/API/worker 的生产依赖审计使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run audit:runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Taro 4.2.0 当前构建工具链仍会触发 `npm run audit:taro:toolchain` 的上游 high/critical 告警,主要来自构建期 CLI、webpack、swiper、lodash-es 等传递依赖。不要使用 `npm audit fix --force` 将 Taro 降级到 3.x;应等 Taro 官方升级后再处理,或者后续评估 Vite runner 替代方案。上线时以静态产物、前端密钥检查、CORS 域名白名单和 API runtime audit 作为阻断项。
|
||||||
|
|
||||||
|
## 下一步页面顺序
|
||||||
|
|
||||||
|
1. 学生端:租户启动、登录、首页、题库入口、练习、错题、收藏。
|
||||||
|
2. 学生端:背单词、知识手册、分数线、资料、会员、个人中心。
|
||||||
|
3. 租户后台:数据看板、内容导航、题目录入/导入、学生管理、营销中心。
|
||||||
|
4. 平台后台:租户、套餐、账单、公共题库授权。
|
||||||
|
5. 小程序:验证 storage/fetch/Auth 兼容性,复用同一套 API client。
|
||||||
16824
package-lock.json
generated
16824
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,11 @@
|
|||||||
"build:worker": "npm --workspace @tiku-saas/worker run build",
|
"build:worker": "npm --workspace @tiku-saas/worker run build",
|
||||||
"check:api": "npm --workspace @tiku-saas/api run check",
|
"check:api": "npm --workspace @tiku-saas/api run check",
|
||||||
"check:importer": "npm --workspace @tiku-saas/import-pocketbase run check",
|
"check:importer": "npm --workspace @tiku-saas/import-pocketbase run check",
|
||||||
|
"check:taro": "npm --workspace @tiku-saas/taro run check",
|
||||||
"check:worker": "npm --workspace @tiku-saas/worker run check",
|
"check:worker": "npm --workspace @tiku-saas/worker run check",
|
||||||
"check:refactor": "npm run check:api && npm run check:worker && npm run check:importer && npm run pb:import:validate && npm run test:readiness && npm run test:pb:dry-run && npm run test:api",
|
"check:refactor": "npm run check:api && npm run check:worker && npm run check:importer && npm run pb:import:validate && npm run test:readiness && npm run test:pb:dry-run && npm run test:api",
|
||||||
|
"audit:runtime": "npm audit --omit=dev --audit-level=high",
|
||||||
|
"audit:taro:toolchain": "npm audit --workspace @tiku-saas/taro --audit-level=high",
|
||||||
"docker:api:build": "docker compose -f docker-compose.api.yml build",
|
"docker:api:build": "docker compose -f docker-compose.api.yml build",
|
||||||
"docker:api:up": "docker compose -f docker-compose.api.yml up api",
|
"docker:api:up": "docker compose -f docker-compose.api.yml up api",
|
||||||
"docker:api:down": "docker compose -f docker-compose.api.yml down",
|
"docker:api:down": "docker compose -f docker-compose.api.yml down",
|
||||||
@@ -43,6 +46,11 @@
|
|||||||
"readiness:production": "node scripts/production-readiness-check.js --skip-db",
|
"readiness:production": "node scripts/production-readiness-check.js --skip-db",
|
||||||
"readiness:production:db": "node scripts/production-readiness-check.js --check-db",
|
"readiness:production:db": "node scripts/production-readiness-check.js --check-db",
|
||||||
"test:api:remote": "node scripts/api-integration-test.js",
|
"test:api:remote": "node scripts/api-integration-test.js",
|
||||||
|
"dev:taro:h5": "npm --workspace @tiku-saas/taro run dev:h5",
|
||||||
|
"build:taro:h5": "npm --workspace @tiku-saas/taro run build:h5",
|
||||||
|
"build:taro:h5:student": "npm --workspace @tiku-saas/taro run build:h5:student",
|
||||||
|
"build:taro:h5:tenant": "npm --workspace @tiku-saas/taro run build:h5:tenant",
|
||||||
|
"build:taro:h5:platform": "npm --workspace @tiku-saas/taro run build:h5:platform",
|
||||||
"pb:schema:summary": "npm --workspace @tiku-saas/import-pocketbase run schema:summary",
|
"pb:schema:summary": "npm --workspace @tiku-saas/import-pocketbase run schema:summary",
|
||||||
"pb:schema:risk": "npm --workspace @tiku-saas/import-pocketbase run schema:risk",
|
"pb:schema:risk": "npm --workspace @tiku-saas/import-pocketbase run schema:risk",
|
||||||
"pb:import:dry-run": "npm --workspace @tiku-saas/import-pocketbase run import:dry-run --",
|
"pb:import:dry-run": "npm --workspace @tiku-saas/import-pocketbase run import:dry-run --",
|
||||||
|
|||||||
Reference in New Issue
Block a user