ci: 添加 Docker 构建和推送工作流

- 新增 Dockerfile 和 .dockerignore 文件
- 添加 Gitea 持续集成工作流,用于构建和推送 Docker 镜像
- 新增 .gitignore 文件,忽略构建和配置文件
- 添加项目结构和规范文档,包括 TypeScript、模块化、API、数据库等规范
- 新增前端和后端的基础代码结构
This commit is contained in:
D8D Developer
2025-06-11 09:35:39 +00:00
commit 71aaeb9424
70 changed files with 4170 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
name: Docker Build and Push
on:
push:
branches:
- release/* # 匹配所有release开头的分支
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 该作业由 ${{ gitea.event_name }} 事件自动触发。"
- run: echo "🐧 此作业当前在 Gitea 托管的 ${{ runner.os }} 服务器上运行!"
- run: echo "🔎 您的分支名称是 ${{ gitea.ref }},仓库是 ${{ gitea.repository }}。"
- name: 检出仓库代码
uses: actions/checkout@v4
- run: echo "💡 ${{ gitea.repository }} 仓库已克隆到运行器。"
- run: echo "🖥️ 工作流现在已准备好在运行器上测试您的代码。"
- name: 列出仓库中的文件
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 此作业的状态是 ${{ job.status }}。"
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 从分支名中提取版本号
id: extract_version
run: |
# 从分支名中提取版本号,例如从 release/v0.1.6 中提取 v0.1.6
VERSION=$(echo "${{ gitea.ref }}" | sed 's|refs/heads/release/||')
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "提取的版本号:$VERSION"
- name: 登录 Docker 注册表
uses: docker/login-action@v3
with:
registry: registry.cn-beijing.aliyuncs.com
username: ${{ secrets.ALI_DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.ALI_DOCKER_REGISTRY_PASSWORD }}
- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
registry.cn-beijing.aliyuncs.com/d8dcloud/d8d-ai-design-prd:${{ env.VERSION }}

42
.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# prod
dist/
dist-server/
bun.lock
package-lock.json
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
.wrangler
# env
.env
.env.production
.env.development
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
public/assets
public/editor
public/vscode-editor

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io

25
.roo/rules/01-general.md Normal file
View File

@@ -0,0 +1,25 @@
# 基础规范
## 项目结构
```
src/
├── client/ # 前端代码 (React + Vite)
├── server/ # 后端代码 (Hono + TypeORM)
│ ├── api/ # API路由
│ ├── migrations/ # 数据库迁移脚本
│ ├── modules/ # 业务模块
│ └── middleware/ # 中间件
```
## 技术栈
### 前端
- React 18
- TypeScript (严格模式)
- Vite 构建工具
### 后端
- Hono 框架
- TypeORM (MySQL)
- Redis (缓存/会话管理)

View File

@@ -0,0 +1,5 @@
# TypeScript规范
1. **严格模式**
- 启用所有严格类型检查选项
- 避免使用`any`类型

8
.roo/rules/03-modules.md Normal file
View File

@@ -0,0 +1,8 @@
# 模块化规范
1. **模块组织**
- 按功能划分模块
- 每个模块包含:
- 实体定义
- 服务层
- 路由控制器

22
.roo/rules/04-api.md Normal file
View File

@@ -0,0 +1,22 @@
# 接口定义规范
1. **DTO定义**
- 必须包含`description`字段说明用途
- 必须包含`example`字段提供示例值
- 示例:
```typescript
export const CreateUserDto = z.object({
username: z.string().min(3).max(20).openapi({
example: 'john_doe',
description: '用户名, 3-20个字符'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: '密码, 最少6位'
})
})
```
2. **API响应**
- 统一的API响应格式
- 完善的Swagger文档

View File

@@ -0,0 +1,5 @@
# 数据库规范
1. **迁移管理**
- 使用迁移脚本管理表结构变更
- 实体类与数据库表严格映射

View File

@@ -0,0 +1,18 @@
# 依赖注入规范
1. **依赖注入原则**
- 服务类必须通过构造函数注入依赖
- 禁止直接实例化全局对象(AppDataSource等)
- 示例:
```typescript
// Good - 通过构造函数注入
export class UserService {
constructor(private dataSource: DataSource) {}
}
// Bad - 使用全局实例
export class UserService {
constructor() {
this.repository = AppDataSource.getRepository(User);
}
}

180
.roo/rules/07-openapi.md Normal file
View File

@@ -0,0 +1,180 @@
# Hono OpenAPI规范
## 常见不规范问题
1. **路径参数问题**:
- ❌ 使用冒号定义路径参数: `/:id`
- ✅ 必须使用花括号: `/{id}`
2. **参数Schema缺失**:
- ❌ 未定义params Schema
- ✅ 必须定义并添加OpenAPI元数据
3. **参数获取方式**:
- ❌ 使用`c.req.param()`
- ✅ 必须使用`c.req.valid('param')`
4. **OpenAPI元数据**:
- ❌ 路径参数缺少OpenAPI描述
- ✅ 必须包含example和description
## 核心规范
### 1. 路由定义
- **路径参数**:
- 必须使用花括号 `{}` 定义 (例: `/{id}`)
- 必须定义 params Schema 并添加 OpenAPI 元数据:
```typescript
const GetParams = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '资源ID'
})
});
```
- 路由定义中必须包含 params 定义:
```typescript
request: { params: GetParams }
```
- 必须使用 `c.req.valid('param')` 获取路径参数
- **请求定义**:
```typescript
request: {
body: {
content: {
'application/json': { schema: YourZodSchema }
}
}
}
```
- **响应定义**:
```typescript
responses: {
200: {
description: '成功响应描述',
content: { 'application/json': { schema: SuccessSchema } }
},
400: {
description: '客户端错误',
content: { 'application/json': { schema: ErrorSchema } }
},
500: {
description: '服务器错误',
content: { 'application/json': { schema: ErrorSchema } }
}
}
```
- **路由示例**:
```typescript
const routeDef = createRoute({
method: 'post',
path: '/workspaces',
middleware: authMiddleware,
request: {
body: {
content: { 'application/json': { schema: CreateSchema } }
}
},
responses: {
200: { ... },
400: { ... },
500: { ... }
}
});
```
### 2. 错误处理
- 错误响应必须使用统一格式: `{ code: number, message: string }`
- 必须与OpenAPI定义完全一致
- 处理示例:
```typescript
try {
// 业务逻辑
} catch (error) {
return c.json({ code: 500, message: '操作失败' }, 500);
}
```
### 3. dataSource引入
- 示例:
```typescript
import { AppDataSource } from '@/server/data-source';
```
### 4. service初始化
- 示例:
```typescript
import { WorkspaceService } from '@/server/modules/workspaces/workspace.service';
const workspaceService = new WorkspaceService(AppDataSource);
### 5. 用户context获取
- 示例:
```typescript
const user = c.get('user');
```
- 注意: 确保 `c.get('user')` 已经在 `authMiddleware` 中设置
### 6. AuthContext引用
- 示例:
```typescript
import { AuthContext } from '@/server/types/context';
```
### 7. createRoute, OpenAPIHono 引入
- 示例:
```typescript
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
```
### 8. ErrorSchema 引入
- 示例:
```typescript
import { ErrorSchema } from '@/server/utils/errorHandler';
```
## 进阶规范
### 1. 路由聚合
当多个相关路由需要组合时:
1. **文件结构**:
- 拆分为独立文件 (`create.ts`, `list.ts` 等)
- 创建 `index.ts` 聚合所有子路由
2. **实现**:
```typescript
export default {
createRoute,
getRoute,
deleteRoute
}
```
3. **优势**:
- 保持模块化
- 简化维护
- 统一API入口
## 路由文件代码结构规范
+imports: 依赖导入
+serviceInit: 服务初始化
+paramsSchema: 路径参数定义
+responseSchema: 响应定义
+errorSchema: 错误定义
+routeDef: 路由定义
+app: 路由实例
## 完整示例
```typescript
// 路由实例
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
// 业务逻辑
return c.json(result, 200);
} catch (error) {
return c.json({ code: 500, message: '操作失败' }, 500);
}
});
export default app;
```

45
.roo/rules/08-rpc.md Normal file
View File

@@ -0,0 +1,45 @@
# RPC 调用规范
## 核心原则
1. **类型安全**:
- 所有RPC调用必须基于OpenAPI定义的类型
- 客户端和服务端类型必须严格匹配
2. **一致性**:
- RPC调用路径必须与OpenAPI路由定义一致
- 错误处理格式必须统一
3. **api版本**:
- 所有RPC调用必须基于OpenAPI定义的版本
- 版本号必须与OpenAPI版本号一致
目前仅支持v1版本
示例:
```typescript
import { client } from '@/client/editor/api';
client.api.v1
## 客户端规范
### 1. 客户端初始化
```typescript
import { hc } from 'hono/client'
import ApiRoutes from '@/server/api';
export const client = hc<typeof ApiRoutes>('/', {
fetch: axiosFetch,
});
```
### 2. 方法调用
- 必须使用解构方式组织RPC方法
- 方法命名必须与OpenAPI路由定义一致
- 示例:
```typescript
const res = await client.api.v1.workspaces.templates.blank[':templateType'].$get({
param: {
templateType
}
});
if (res.status !== 200) {
throw new Error(res.message);
}
const templateInfo = await res.json();
```

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# 使用指定基础镜像
FROM node:20.18.3
# 设置工作目录
WORKDIR /app
# 复制package.json .npmrc和package-lock.json
COPY package.json .npmrc package-lock.json* ./
# 安装依赖
RUN npm install
# 复制项目文件
COPY . .
# 构建项目
RUN npm run build
# 暴露端口(根据实际需要调整)
EXPOSE 23956
# 启动命令
CMD ["npm", "run", "start"]

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
```bash
npx giget@latest gh:yusukebe/hono-vite-react-stack-example my-app
```

72
docs/aliyun-sms.md Normal file
View File

@@ -0,0 +1,72 @@
```typescript
// This file is auto-generated, don't edit it
// 依赖的模块可通过下载工程中的模块依赖文件或右上角的获取 SDK 依赖信息查看
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import Util, * as $Util from '@alicloud/tea-util';
import { env } from '../../config/env.ts';
export class SMS {
/**
* @remarks
* 使用AK&SK初始化账号Client
* @returns Client
*
* @throws Exception
*/
static createClient(): Dysmsapi20170525 {
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式更多鉴权访问方式请参见https://help.aliyun.com/document_detail/378664.html。
if (!env.deploy?.alicloud) {
throw new Error("Aliyun SMS configuration is required");
}
const { accessKeyId, accessKeySecret } = env.deploy.alicloud;
let config = new $OpenApi.Config({
accessKeyId,
accessKeySecret,
});
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = `dysmsapi.aliyuncs.com`;
return new (Dysmsapi20170525 as any).default(config);
}
/**
* 发送短信
* @param phoneNumber 手机号
* @param code 验证码
* @param templateCode 模板代码,默认使用配置的验证码模板
* @param signName 短信签名,默认使用配置的签名
*/
static async sendVerificationSMS(
phoneNumber: string,
code: string,
templateCode?: string,
signName?: string
): Promise<boolean> {
let client = this.createClient();
let sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
signName: signName || (env.sms?.defaultSignName || "多八多"),
templateCode: templateCode || (env.sms?.defaultTemplateCode || "SMS_164760103"),
phoneNumbers: phoneNumber,
templateParam: JSON.stringify({code: code}),
});
let runtime = new $Util.RuntimeOptions({ });
try {
// 发送短信
const result = await client.sendSmsWithOptions(sendSmsRequest, runtime);
console.log("SMS sent successfully:", result);
return true;
} catch (error: unknown) {
// 错误处理
const err = error as { message?: string, data?: { Recommend?: string } };
console.error("SMS sending failed:", err.message);
if (err.data && err.data["Recommend"]) {
console.error("Recommendation:", err.data["Recommend"]);
}
return false;
}
}
}
```

37
docs/sso-verify.md Normal file
View File

@@ -0,0 +1,37 @@
```typescript
// 为Gogs SSO单点登录提供的验证API
authRoutes.get('/sso-verify', async (c) => {
try {
const auth = await initAuth()
// 从Authorization头获取令牌
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.text('Unauthorized', 401)
}
// 验证令牌
try {
const userData = await auth.verifyToken(token)
if (!userData) {
return c.text('Invalid token', 401)
}
// 导入GitUtils工具类并使用其formatUsername方法格式化用户名
const username = GitUtils.formatUsername(userData.username, userData.id);
// 验证成功,设置用户名到响应头
c.header('X-Username', username)
return c.text('OK', 200)
} catch (tokenError) {
log.auth('Token验证失败:', tokenError)
return c.text('Invalid token', 401)
}
} catch (error) {
log.auth('SSO验证失败:', error)
return c.text('Authentication failed', 500)
}
})
```

44
docs/zpay.cn.md Normal file
View File

@@ -0,0 +1,44 @@
API信息兼容 易支付 接口)
接口地址process.env.ZPAY_URL
商户IDPIDprocess.env.ZPAY_PID
商户密钥PKEYprocess.env.ZPAY_PKEY
异步通知地址process.env.ZPAY_NOTIFY_URL
API接口支付
请求URL
https://zpayz.cn/mapi.php
请求方法
POST方式为form-data
请求参数
字段名 变量名 必填 类型 示例值 描述
商户ID pid 是 String 1001
支付渠道ID cid 否 String 1234 如果不填则随机使用某一支付渠道
支付方式 type 是 String alipay 支付宝alipay 微信支付wxpay
商户订单号 out_trade_no 是 String 20160806151343349
异步通知地址 notify_url 是 String http://www.pay.com/notify_url.php 服务器异步通知地址
商品名称 name 是 String VIP会员 如超过127个字节会自动截取
商品金额 money 是 String 1.00 单位最大2位小数
用户IP地址 clientip 是 String 192.168.1.100 用户发起支付的IP地址
设备类型 device 否 String pc 根据当前用户浏览器的UA判断
传入用户所使用的浏览器
或设备类型默认为pc
业务扩展参数 param 否 String 没有请留空 支付后原样返回
签名字符串 sign 是 String 202cb962ac59075b964b07152d234b70 签名算法参考本页底部
签名类型 sign_type 是 String MD5 默认为MD5
成功返回
字段名 变量名 类型 示例值 描述
返回状态码 code Int 1 1为成功其它值为失败
返回信息 msg String 失败时返回原因
订单号 trade_no String 20160806151343349 支付订单号
ZPAY内部订单号 O_id String 123456 ZPAY内部订单号
支付跳转url payurl String https://xxx.cn/pay/wxpay/202010903/ 如果返回该字段则直接跳转到该url支付
二维码链接 qrcode String https://xxx.cn/pay/wxpay/202010903/ 如果返回该字段则根据该url生成二维码
二维码图片 img String https://z-pay.cn/qrcode/123.jpg 该字段为付款二维码的图片地址
失败返回
{"code":"error","msg":"具体的错误信息"}
<-- GET /api/zpay/notify?pid=2025052907394884&trade_no=2025053022001476521427708154&out_trade_no=202500000002&type=alipay&name=VIP%E4%BC%9A%E5%91%98%E6%9C%8D%E5%8A%A1&money=0.01&trade_status=TRADE_SUCCESS&param=user_id%3D123&sign=9a3406e5f177c03646274749463ad979&sign_type=MD5

66
package.json Normal file
View File

@@ -0,0 +1,66 @@
{
"name": "d8d-ai-design",
"private": true,
"type": "module",
"scripts": {
"dev": "export NODE_ENV='development' && vite",
"build": "export NODE_ENV='production' && vite build && vite build --ssr",
"start": "export NODE_ENV='production' && node dist-server/index.js"
},
"dependencies": {
"@alicloud/bssopenapi20171214": "^4.0.0",
"@alicloud/credentials": "^2.4.3",
"@alicloud/dysmsapi20170525": "^4.1.1",
"@alicloud/eci20180808": "^2.1.1",
"@alicloud/openapi-client": "^0.4.14",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/tea-util": "^1.4.10",
"@codingame/monaco-vscode-secret-storage-service-override": "^16.0.2",
"@d8d-appcontainer/api": "^3.0.47",
"@d8d-fun/eci-manager": "npm:@jsr/d8d-fun__eci-manager@^0.1.3",
"@d8d-fun/gogs-client": "npm:@jsr/d8d-fun__gogs-client@^0.1.2",
"@heroicons/react": "^2.2.0",
"@hono/node-server": "^1.14.3",
"@hono/react-renderer": "^1.0.1",
"@hono/swagger-ui": "^0.5.1",
"@hono/vite-dev-server": "^0.19.1",
"@hono/zod-openapi": "^0.19.7",
"@hono/zod-validator": "^0.4.3",
"@tanstack/react-query": "^5.77.2",
"@types/bcrypt": "^5.0.2",
"@types/jsonwebtoken": "^9.0.9",
"bcrypt": "^6.0.0",
"debug": "^4.4.1",
"dotenv": "^16.5.0",
"formdata-node": "^6.0.3",
"hono": "^4.7.6",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"ioredis": "^5.6.1",
"isomorphic-git": "^1.30.2",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.14.1",
"node-fetch": "^3.3.2",
"pg": "^8.16.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^15.5.2",
"react-router": "^7.6.1",
"react-router-dom": "^7.6.1",
"react-toastify": "^11.0.5",
"reflect-metadata": "^0.2.2",
"typeorm": "^0.3.24",
"vite-plugin-i18next-loader": "^3.1.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"@types/node": "^22.15.23",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"hono-vite-react-stack-node": "^0.2.1",
"tailwindcss": "^4.1.3",
"vite": "^6.3.5"
}
}

70
src/client/app.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { hc } from 'hono/client'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import type { BaseRoutes } from '../server/api/base'
import type { UserRoutes } from '../server/api/user'
import './i18n/config'
import LanguageSwitcher from './components/LanguageSwitcher'
const client = hc<BaseRoutes>('/api')
const userClient = hc<UserRoutes['createUser']>('/api')
const Home = () => {
const { t } = useTranslation()
return (
<div>
<h1>{t('welcome')}</h1>
<LanguageSwitcher />
</div>
)
}
const About = () => {
return (
<div>
<h1>About Page</h1>
<p>This is the about page.</p>
</div>
)
}
const ApiDemo = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['apiData'],
queryFn: async () => {
const res = await client.index.$get({ query: { name: 'Hono' } })
return res.json()
}
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <h2 className="text-2xl">{data?.message}</h2>
}
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
},
{
path: '/about',
element: <About />,
},
{
path: '/api-demo',
element: <ApiDemo />,
},
])
const App = () => {
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
const LanguageSwitcher: React.FC = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div className="language-switcher">
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('zh')}></button>
</div>
);
};
export default LanguageSwitcher;

19
src/client/i18n/config.ts Normal file
View File

@@ -0,0 +1,19 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resources from 'virtual:i18next-loader';
// 初始化i18n配置
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React已经处理XSS防护
},
resources
});
export default i18n;

View File

@@ -0,0 +1,7 @@
{
"translation": {
"welcome": "Welcome",
"language": "Language",
"switch_language": "Switch Language"
}
}

View File

@@ -0,0 +1,7 @@
{
"translation": {
"welcome": "欢迎",
"language": "语言",
"switch_language": "切换语言"
}
}

5
src/client/i18next-loader.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module 'virtual:i18next-loader' {
import { Resource } from 'i18next';
const resources: Resource;
export default resources;
}

15
src/client/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './app'
const queryClient = new QueryClient()
const rootElement = document.getElementById('root')
if (rootElement) {
const root = createRoot(rootElement)
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
}

137
src/server/api.ts Normal file
View File

@@ -0,0 +1,137 @@
import { OpenAPIHono } from '@hono/zod-openapi'
import { errorHandler } from './utils/errorHandler'
import base from './api/base'
import usersRouter from './api/users/index'
import { initConfigRouter, initStatusRouter } from './api/init'
import paymentRouter from './api/payment'
import workspacesRouter from './api/workspaces/index'
import workspacesProjectsRouter from './api/workspaces.projects/index'
import workspacesTemplatesRouter from './api/workspaces.templates/index'
import workspacesContainersRouter from './api/workspaces.containers/index'
import workspacesCollaboratorsRouter from './api/workspaces.collaborations/index'
import workspacesProjectCollaboratorsRouter from './api/workspaces.project-collaborators/index'
import workspacesTemplateCollaboratorsRouter from './api/workspaces.template-collaborators/index'
import templateMarketplaceRouter from './api/marketplace/index'
import authRoute from './api/auth/index'
import aliBillsRoute from './api/ali-bills/index'
import { AuthContext } from './types/context'
import { AppDataSource } from './data-source'
const api = new OpenAPIHono<AuthContext>()
api.onError(errorHandler)
// Rate limiting
api.use('/api/v1/*', async (c, next) => {
const ip = c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip')
// 实现速率限制逻辑
await next()
})
// 数据库初始化中间件
api.use('/api/v1/*', async (c, next) => {
if(!AppDataSource.isInitialized) await AppDataSource.initialize();
await next()
})
// 注册Bearer认证方案
api.openAPIRegistry.registerComponent('securitySchemes','bearerAuth',{
type:'http',
scheme:'bearer',
bearerFormat:'JWT',
description:'使用JWT进行认证'
})
// OpenAPI documentation endpoint
api.doc31('/doc', {
openapi: '3.1.0',
info: {
title: 'API Documentation',
version: '1.0.0'
},
security: [{
bearerAuth: []
}]
// servers: [{ url: '/api/v1' }]
})
// const bindRoute = (api:OpenAPIHono, path: string, routes: Array<OpenAPIHono>) => {
// routes.forEach(route => {
// api = api.route(path, route)
// })
// return api
// }
// Register routes
let routes = api
.route('/api/v1/init', initConfigRouter)
.route('/api/v1/init', initStatusRouter)
.route('/api/v1/base', base)
.route('/api/v1/users', usersRouter.createRoute)
.route('/api/v1/users', usersRouter.listRoute)
.route('/api/v1/users', usersRouter.getRoute)
.route('/api/v1/users', usersRouter.updateRoute)
.route('/api/v1/users', usersRouter.deleteRoute)
.route('/api/v1/auth', authRoute.loginRoute.passwordRoute)
.route('/api/v1/auth', authRoute.loginRoute.smsRoute)
.route('/api/v1/auth', authRoute.logoutRoute)
.route('/api/v1/auth', authRoute.meRoute.meRoute)
.route('/api/v1/auth', authRoute.phoneCode.fixed)
.route('/api/v1/auth', authRoute.phoneCode.sms)
.route('/api/v1/auth', authRoute.registerRoute.registerRoute)
.route('/api/auth', authRoute.ssoVerify)
.route('/api/v1/payments', paymentRouter.notifyApi)
.route('/api/v1/payments', paymentRouter.queryApi)
.route('/api/v1/payments', paymentRouter.paymentApi)
.route('/api/v1/marketplace', templateMarketplaceRouter.listRoute)
.route('/api/v1/marketplace', templateMarketplaceRouter.detailRoute)
.route('/api/v1/marketplace', templateMarketplaceRouter.deleteRoute)
.route('/api/v1/workspaces', workspacesRouter.createRoutes)
.route('/api/v1/workspaces', workspacesRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesRouter.listRoutes)
.route('/api/v1/workspaces', workspacesRouter.getRoutes)
.route('/api/v1/workspaces', workspacesRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.getRoutes)
.route('/api/v1/workspaces', workspacesProjectsRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.createRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.listRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.getRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.updateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.publishRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.useTemplateRoutes)
.route('/api/v1/workspaces', workspacesTemplatesRouter.getBlankTemplateRoute)
.route('/api/v1/workspaces', workspacesTemplatesRouter.listBlankTemplatesRoute)
.route('/api/v1/workspaces', workspacesContainersRouter.startRoutes)
.route('/api/v1/workspaces', workspacesContainersRouter.stopRoutes)
.route('/api/v1/workspaces', workspacesCollaboratorsRouter.repositoriesApp)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesProjectCollaboratorsRouter.checkRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.listRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.createRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.deleteRoutes)
.route('/api/v1/workspaces', workspacesTemplateCollaboratorsRouter.checkRoutes)
.route('/api/v1/ali-bills', aliBillsRoute.getRoute)
.route('/api/v1/ali-bills', aliBillsRoute.syncRoute)
export type ApiRoutes = typeof routes
export default routes

View File

@@ -0,0 +1,15 @@
import loginRoute from './login';
import logoutRoute from './logout';
import meRoute from './me';
import phoneCode from './phone-code';
import registerRoute from './register';
import ssoVerify from './sso-verify';
export default {
loginRoute,
logoutRoute,
meRoute,
phoneCode,
registerRoute,
ssoVerify
}

View File

@@ -0,0 +1,13 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import passwordRoute from './password';
import smsRoute from './sms';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', passwordRoute)
// .route('/', smsRoute);
// export default api;
export default {
passwordRoute,
smsRoute,
}

View File

@@ -0,0 +1,71 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AppDataSource } from '../../../data-source'
import { AuthContext } from '../../../types/context'
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const LoginSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: '用户名'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: '密码'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: z.object({
id: z.number(),
username: z.string()
})
})
const loginRoute = createRoute({
method: 'post',
path: '/login',
request: {
body: {
content: {
'application/json': {
schema: LoginSchema
}
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
401: {
description: '用户名或密码错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(loginRoute, async (c) => {
const { username, password } = c.req.valid('json')
const result = await authService.login(username, password)
return c.json(result, 200)
});
export default app

View File

@@ -0,0 +1,144 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { setCookie } from 'hono/cookie'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AppDataSource } from '../../../data-source'
import { AuthContext } from '../../../types/context'
import { UserResponseSchema } from '../schemas'
import debug from 'debug'
import { GitUtils } from '@/server/utils/gitUtils'
import process from 'node:process'
const log = {
auth: debug('auth')
}
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const SmsLoginSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
}),
code: z.string().length(6).openapi({
example: '123456',
description: '6位验证码'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: UserResponseSchema
})
const smsLoginRoute = createRoute({
method: 'post',
path: '/login/sms',
request: {
body: {
content: {
'application/json': {
schema: SmsLoginSchema
}
}
}
},
responses: {
200: {
description: '登录成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
400: {
description: '验证码错误或已过期',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(smsLoginRoute, async (c) => {
const { phone, code } = c.req.valid('json')
// 验证验证码
const isValid = authService.verifyCode(phone, code)
if (!isValid) {
throw new HTTPException(400, { message: '验证码错误或已过期' })
}
// 查找或创建用户
let user = await userService.getUserByPhone(phone)
if (!user) {
user = await userService.createUser({ mobile: phone, username: phone })
}
// 同步检查 Gogs 用户
try {
// 格式化用户名
const gogsUsername = GitUtils.formatUsername(phone, user.id);
// 随机生成密码
const randomPassword = Math.random().toString(36).slice(-8);
// 检查并创建 Gogs 用户
const gogsResult = await GitUtils.checkGogsUser(
gogsUsername,
user.email || `${gogsUsername}@d8d.fun`,
randomPassword,
phone
);
if (gogsResult.success) {
log.auth(gogsResult.message);
} else {
log.auth(`Gogs 用户同步失败: ${gogsResult.message}`);
}
} catch (syncError) {
// Gogs 同步失败不应该影响登录流程
log.auth('同步 Gogs 用户失败:', syncError);
}
const token = authService.generateToken(user)
const cookieName = process.env.SSO_COOKIE_NAME || 'd8d_aider_auth_token';
const cookieDomain = process.env.SSO_COOKIE_DOMAIN || '.d.d8d.fun';
// 设置cookie
setCookie(c, cookieName, token, {
httpOnly: true,
secure: true,
sameSite: 'none',
domain: cookieDomain,
maxAge: 86400 * 7
});
const response = {
token,
user: {
id: user.id,
username: user.username,
mobile: user.mobile,
status: user.status,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
}
}
return c.json(response, 200)
})
export default app

View File

@@ -0,0 +1,68 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from 'zod'
import { AuthContext } from '@/server/types/context';
import { authMiddleware } from '@/server/middleware/auth.middleware';
import { AppDataSource } from '@/server/data-source';
import { AuthService } from '@/server/modules/auth/auth.service';
import { UserService } from '@/server/modules/users/user.service';
import { ErrorSchema } from '@/server/utils/errorHandler';
// 初始化服务
const userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const SuccessSchema = z.object({
message: z.string().openapi({ example: '登出成功' })
})
// 定义路由
const routeDef = createRoute({
method: 'post',
path: '/logout',
security: [{ Bearer: [] }],
middleware: [authMiddleware],
responses: {
200: {
description: '登出成功',
content: {
'application/json': {
schema: SuccessSchema
}
}
},
401: {
description: '未授权',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
try {
const token = c.get('token');
const decoded = authService.verifyToken(token);
if (!decoded) {
return c.json({ code: 401, message: '未授权' }, 401);
}
await authService.logout(token);
return c.json({ message: '登出成功' }, 200);
} catch (error) {
console.error('登出失败:', error);
return c.json({ code: 500, message: '登出失败' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,46 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { authMiddleware } from '@/server/middleware/auth.middleware'
import { AuthContext } from '@/server/types/context'
import { UserResponseSchema } from '../schemas'
const routeDef = createRoute({
method: 'get',
path: '/me',
middleware: authMiddleware,
responses: {
200: {
description: '获取当前用户信息成功',
content: {
'application/json': {
schema: UserResponseSchema
}
}
},
401: {
description: '未授权',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, (c) => {
const user = c.get('user')
return c.json({
id: user.id,
username: user.username,
mobile: user.mobile,
status: user.status,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString()
}, 200)
})
export default app

View File

@@ -0,0 +1,10 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import meRoute from './get';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', meRoute)
// export default api;
export default {
meRoute,
}

View File

@@ -0,0 +1,58 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
const GenerateFixedCodeSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
})
})
const FixedCodeResponseSchema = z.object({
code: z.string().length(6).openapi({
example: '123456',
description: '6位固定验证码'
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const generateFixedCodeRoute = createRoute({
method: 'get',
path: '/phone-code/fixed/{phone}',
request: {
params: GenerateFixedCodeSchema
},
responses: {
200: {
description: '生成成功',
content: {
'application/json': {
schema: FixedCodeResponseSchema
}
}
},
403: {
description: '生产环境禁止访问',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(generateFixedCodeRoute, async (c) => {
const { phone } = c.req.valid('param')
const code = authService.generateFixedCode(phone)
return c.json({ code }, 200)
})
export default app

View File

@@ -0,0 +1,8 @@
import fixed from './fixed';
import sms from './sms';
export default {
fixed,
sms
}

View File

@@ -0,0 +1,53 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { z } from 'zod'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
import { SMS } from '@/server/utils/sms'
const GenerateSMSRndCodeSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/).openapi({
example: '13800138000',
description: '手机号'
})
})
const SMSRndCodeResponseSchema = z.object({
success: z.boolean().openapi({
example: true,
description: '是否成功'
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const generateFixedCodeRoute = createRoute({
method: 'get',
path: '/phone-code/sms/{phone}',
request: {
params: GenerateSMSRndCodeSchema
},
responses: {
200: {
description: '发送成功',
content: {
'application/json': {
schema: SMSRndCodeResponseSchema
}
}
},
}
})
const app = new OpenAPIHono<AuthContext>().openapi(generateFixedCodeRoute, async (c) => {
const { phone } = c.req.valid('param')
const code = await authService.generateRandCode(phone)
// TODO: 发送短信
const result = await SMS.sendVerificationSMS(phone, code)
return c.json({ success: result }, 200)
})
export default app

View File

@@ -0,0 +1,76 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '../../../modules/auth/auth.service'
import { UserService } from '../../../modules/users/user.service'
import { z } from 'zod'
import { AppDataSource } from '../../../data-source'
import { ErrorSchema } from '../../../utils/errorHandler'
import { AuthContext } from '../../../types/context'
const RegisterSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: '用户名'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: '密码'
}),
email: z.string().email().openapi({
example: 'john@example.com',
description: '邮箱'
})
})
const TokenResponseSchema = z.object({
token: z.string().openapi({
example: 'jwt.token.here',
description: 'JWT Token'
}),
user: z.object({
id: z.number(),
username: z.string()
})
})
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const registerRoute = createRoute({
method: 'post',
path: '/register',
request: {
body: {
content: {
'application/json': {
schema: RegisterSchema
}
}
}
},
responses: {
201: {
description: '注册成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
400: {
description: '用户名已存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono<AuthContext>().openapi(registerRoute, async (c) => {
const { username, password, email } = c.req.valid('json')
const user = await userService.createUser({ username, password, email })
const token = authService.generateToken(user)
return c.json({ token, user }, 201)
})
export default app

View File

@@ -0,0 +1,10 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import registerRoute from './create';
// const api = new OpenAPIHono<AuthContext>()
// .route('/', registerRoute)
// export default api;
export default {
registerRoute,
}

View File

@@ -0,0 +1,10 @@
import { z } from 'zod'
export const UserResponseSchema = z.object({
id: z.number(),
username: z.string(),
mobile: z.string(),
status: z.number(),
createdAt: z.string(),
updatedAt: z.string()
}).openapi('User')

View File

@@ -0,0 +1,69 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { AuthService } from '@/server/modules/auth/auth.service'
import { UserService } from '@/server/modules/users/user.service'
import { ErrorSchema } from '@/server/utils/errorHandler'
import { AppDataSource } from '@/server/data-source'
import { AuthContext } from '@/server/types/context'
import { GitUtils } from '@/server/utils/gitUtils'
const userService = new UserService(AppDataSource)
const authService = new AuthService(userService)
const routeDef = createRoute({
method: 'get',
path: '/sso-verify',
responses: {
200: {
description: 'SSO验证成功',
headers: {
'X-Username': {
schema: { type: 'string' },
description: '格式化后的用户名'
}
}
},
401: {
description: '未授权或令牌无效',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
})
const app = new OpenAPIHono().openapi(routeDef, async (c) => {
try {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ code: 401, message: '未提供授权令牌' }, 401)
}
try {
const userData = await authService.verifyToken(token)
if (!userData) {
return c.json({ code: 401, message: '无效令牌' }, 401)
}
const username = GitUtils.formatUsername(userData.username, userData.id)
c.header('X-Username', username)
return c.text('OK', 200)
} catch (tokenError) {
return c.json({ code: 401, message: '令牌验证失败' }, 401)
}
} catch (error) {
return c.json({ code: 500, message: 'SSO验证失败' }, 500)
}
})
export default app

54
src/server/api/base.ts Normal file
View File

@@ -0,0 +1,54 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { ErrorSchema } from '../utils/errorHandler'
const app = new OpenAPIHono()
const QuerySchema = z.object({
name: z.string().optional().openapi({
param: {
name: 'name',
in: 'query'
},
example: 'John'
})
})
const ResponseSchema = z.object({
message: z.string().openapi({
example: 'Hello from API, John'
})
})
const route = createRoute({
method: 'get',
path: '/',
request: {
query: QuerySchema
},
responses: {
200: {
content: {
'application/json': {
schema: ResponseSchema
}
},
description: 'Successful response'
},
400: {
content: {
'application/json': {
schema: ErrorSchema,
},
},
description: 'Invalid request'
}
}
})
const baseRoutes = app.openapi(route, (c) => {
const { name } = c.req.valid('query')
return c.json({ message: `Hello from API${name ? `, ${name}` : ''}` }, 200)
})
export default baseRoutes

192
src/server/api/init.ts Normal file
View File

@@ -0,0 +1,192 @@
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
import { ErrorSchema } from '../utils/errorHandler';
import { AppDataSource } from '../data-source';
import { UserEntity as User } from '../modules/users/user.entity';
import { Role } from '../modules/users/role.entity';
import * as bcrypt from 'bcrypt';
import { generateJwtSecret } from '../utils/env-init';
import { writeFileSync } from 'fs';
const initRouter = new OpenAPIHono();
// Zod 验证模式
const DatabaseConfigSchema = z.object({
host: z.string(),
port: z.number(),
username: z.string(),
password: z.string(),
database: z.string()
});
const AdminUserSchema = z.object({
username: z.string().min(3),
password: z.string().min(8)
});
const InitConfigSchema = z.object({
dbConfig: DatabaseConfigSchema,
adminUser: AdminUserSchema
});
// 检查初始化状态路由
const statusRoute = createRoute({
method: 'get',
path: '/status',
responses: {
200: {
content: {
'application/json': {
schema: z.object({
initialized: z.boolean()
})
}
},
description: '返回系统初始化状态'
},
500: {
description: '初始化状态检查失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const initStatusRouter = initRouter.openapi(statusRoute, async (c) => {
try {
const isInitialized = await checkInitialization();
return c.json({ initialized: isInitialized }, 200);
} catch (error) {
return c.json({
code: 500,
message: '初始化状态检查失败'
}, 500);
}
});
// 提交配置路由
const configRoute = createRoute({
method: 'post',
path: '/config',
request: {
body: {
content: {
'application/json': {
schema: InitConfigSchema
}
}
}
},
responses: {
200: {
description: '系统初始化成功',
content: {
'application/json': {
schema: z.object({
success: z.boolean(),
jwtSecret: z.string().optional()
})
}
}
},
400: {
description: '初始化失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器内部错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const initConfigRouter = initRouter.openapi(configRoute, async (c) => {
const { dbConfig, adminUser } = c.req.valid('json');
if (await checkInitialization()) {
return c.json({
code: 400,
message: '系统已初始化'
}, 400);
}
try {
await validateDatabase(dbConfig);
await generateEnvFile(dbConfig);
const jwtSecret = generateJwtSecret();
await createAdminUser(adminUser);
return c.json({
success: true,
jwtSecret
}, 200);
} catch (error) {
return c.json({
code: 400,
message: error instanceof Error ? error.message : '未知错误'
}, 400);
}
});
export { initStatusRouter, initConfigRouter };
async function checkInitialization(): Promise<boolean> {
// 检查数据库是否已初始化
return AppDataSource.isInitialized;
}
async function validateDatabase(config: any) {
// 使用TypeORM验证数据库连接
const testDataSource = AppDataSource.setOptions(config);
await testDataSource.initialize();
await testDataSource.destroy();
}
async function generateEnvFile(config: any) {
const envContent = `DB_HOST=${config.host}
DB_PORT=${config.port}
DB_USERNAME=${config.username}
DB_PASSWORD=${config.password}
DB_DATABASE=${config.database}
`;
writeFileSync('.env', envContent);
}
async function createAdminUser(userData: any) {
const userRepo = AppDataSource.getRepository(User);
const roleRepo = AppDataSource.getRepository(Role);
// 检查是否已存在管理员
const existingAdmin = await userRepo.findOne({ where: { username: userData.username } });
if (existingAdmin) {
throw new Error('管理员用户已存在');
}
// 创建管理员角色
let adminRole = await roleRepo.findOne({ where: { name: 'admin' } });
if (!adminRole) {
adminRole = roleRepo.create({ name: 'admin', permissions: ['*'] });
await roleRepo.save(adminRole);
}
// 创建管理员用户
const hashedPassword = await bcrypt.hash(userData.password, 10);
const adminUser = userRepo.create({
username: userData.username,
password: hashedPassword,
roles: [adminRole]
});
await userRepo.save(adminUser);
}

View File

@@ -0,0 +1,89 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from 'zod'
import { AppDataSource } from '../data-source'
import { HTTPException } from 'hono/http-exception';
import { authMiddleware } from '../middleware/auth.middleware';
import { AuthContext } from '../types/context';
interface Migration {
name: string;
timestamp: number;
}
const app = new OpenAPIHono<AuthContext>()
const MigrationResponseSchema = z.object({
success: z.boolean(),
message: z.string(),
migrations: z.array(z.string()).optional()
})
const runMigrationRoute = createRoute({
method: 'post',
path: '/migrations/run',
middleware: authMiddleware,
responses: {
200: {
content: {
'application/json': {
schema: MigrationResponseSchema
}
},
description: 'Migrations executed successfully'
},
500: {
description: 'Migration failed'
}
}
})
const revertMigrationRoute = createRoute({
method: 'post',
path: '/migrations/revert',
middleware: authMiddleware,
responses: {
200: {
content: {
'application/json': {
schema: MigrationResponseSchema
}
},
description: 'Migration reverted successfully'
},
500: {
description: 'Revert failed'
}
}
})
app.openapi(runMigrationRoute, async (c) => {
try {
const migrations = await AppDataSource.runMigrations()
return c.json({
success: true,
message: 'Migrations executed successfully',
migrations: migrations.map((m: Migration) => m.name)
})
} catch (error) {
throw error
}
})
app.openapi(revertMigrationRoute, async (c) => {
try {
await AppDataSource.undoLastMigration()
const migrations = await AppDataSource.showMigrations()
return c.json({
success: true,
message: 'Migration reverted successfully',
migrations: migrations
})
} catch (error) {
throw error
}
})
export default app
export type AppType = typeof app

202
src/server/api/payment.ts Normal file
View File

@@ -0,0 +1,202 @@
import { OpenAPIHono, createRoute} from '@hono/zod-openapi';
import { authMiddleware } from '../middleware/auth.middleware';
import {
PaymentRequestSchema,
PaymentResponseSchema,
NotifyRequestSchema,
QueryRequestSchema,
QueryResponseSchema
} from '../modules/payment/dto/payment.dto';
import { PaymentService } from '../modules/payment/payment.service';
import { ErrorSchema } from '../utils/errorHandler';
import { AppDataSource } from '../data-source';
import { AuthContext } from '../types/context';
const paymentRoute = createRoute({
method: 'post',
path: '/',
middleware: authMiddleware,
request: {
body: {
content: {
'application/json': {
schema: PaymentRequestSchema
}
}
}
},
responses: {
200: {
description: '支付请求成功',
content: {
'application/json': {
schema: PaymentResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const notifyRoute = createRoute({
method: 'get',
path: '/zpay/notify',
request: {
query: NotifyRequestSchema
},
responses: {
200: {
description: '回调处理成功',
content: {
'application/json': {
schema: PaymentResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const queryRoute = createRoute({
method: 'get',
path: '/query',
middleware: authMiddleware,
request: {
query: QueryRequestSchema
},
responses: {
200: {
description: '查询成功',
content: {
'application/json': {
schema: QueryResponseSchema
}
}
},
400: {
description: '客户端错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>()
const paymentApi = app.openapi(paymentRoute, async (c) => {
const payload = c.req.valid('json');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.createPayment(payload);
if(result.code !== 1) {
return c.json({
code: 400,
message: result.msg || '支付请求失败'
}, 400);
}
return c.json({
code: 200,
msg: '支付请求成功',
trade_no: result.trade_no,
payurl: result.payurl,
qrcode: result.qrcode,
img: result.img
}, 200);
} catch (error) {
console.error('Payment error:', error);
return c.json({
code: 500,
message: '支付处理失败'
}, 500);
}
});
const notifyApi = app.openapi(notifyRoute, async (c) => {
const payload = c.req.valid('query');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.handleNotify(payload);
return c.json({
code: 200,
msg: result.msg,
trade_no: payload.trade_no,
out_trade_no: payload.out_trade_no
}, 200);
} catch (error) {
console.error('Notify error:', error);
return c.json({
code: 500,
message: '回调处理失败'
}, 500);
}
});
const queryApi = app.openapi(queryRoute, async (c) => {
const payload = c.req.valid('query');
try {
const paymentService = new PaymentService(AppDataSource);
const result = await paymentService.queryPayment(payload);
return c.json({
code: 200,
msg: result.msg,
data: result.data
}, 200);
} catch (error) {
console.error('Query error:', error);
return c.json({
code: 500,
message: '查询处理失败'
}, 500);
}
});
export default {
paymentApi,
notifyApi,
queryApi
};

View File

@@ -0,0 +1,80 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const UserSchema = z.object({
id: z.number().openapi({ example: 1 }),
username: z.string().openapi({ example: 'john_doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
createdAt: z.string().datetime().openapi({ example: '2025-05-28T00:00:00Z' })
});
const CreateUserSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: 'Minimum 3 characters'
}),
password: z.string().min(6).openapi({
example: 'password123',
description: 'Minimum 6 characters'
}),
email: z.string().email().openapi({ example: 'john@example.com' })
});
const createUserRoute = createRoute({
method: 'post',
path: '/users',
middleware: authMiddleware,
request: {
body: {
content: {
'application/json': {
schema: CreateUserSchema
}
}
}
},
responses: {
201: {
description: '创建成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
400: {
description: '输入数据无效',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(createUserRoute, async (c) => {
try {
const data = c.req.valid('json');
const user = await userService.createUser(data);
return c.json(user, 201);
} catch (error) {
return c.json({ code: 500, message: '服务器错误' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,61 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const DeleteParams = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
});
const deleteRoute = createRoute({
method: 'delete',
path: '/{id}',
middleware: authMiddleware,
request: {
params: DeleteParams
},
responses: {
204: {
description: '用户删除成功'
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(deleteRoute, async (c) => {
try {
const { id } = c.req.valid('param');
await userService.deleteUser(parseInt(id));
return c.body(null, 204);
} catch (error) {
return c.json({
code: 500,
message: '删除用户失败'
}, 500);
}
});
export default app;

View File

@@ -0,0 +1,77 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const UserSchema = z.object({
id: z.number().openapi({ example: 1 }),
username: z.string().openapi({ example: 'john_doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
createdAt: z.string().datetime().openapi({ example: '2025-05-28T00:00:00Z' })
});
const GetUserRoute = createRoute({
method: 'get',
path: '/{id}',
middleware: authMiddleware,
request: {
params: z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
})
},
responses: {
200: {
description: '获取用户成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(GetUserRoute, async (c) => {
try {
const { id } = c.req.valid('param');
const user = await userService.getUserById(parseInt(id));
if (!user) {
return c.json({ code: 404, message: '用户不存在' }, 404);
}
return c.json({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}, 200);
} catch (error) {
return c.json({ code: 500, message: '服务器错误' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,23 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { AuthContext } from '@/server/types/context';
import createRoute from './create';
import listRoute from './list';
import getRoute from './get';
import updateRoute from './update';
import deleteRoute from './delete';
// const api = new OpenAPIHono()
// .route('/', createRoute)
// .route('/', listRoute)
// .route('/', getRoute)
// .route('/', updateRoute)
// .route('/', deleteRoute);
// export default api;
export default {
createRoute,
listRoute,
getRoute,
updateRoute,
deleteRoute
}

View File

@@ -0,0 +1,57 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const UserSchema = z.object({
id: z.number().openapi({ example: 1 }),
username: z.string().openapi({ example: 'john_doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
createdAt: z.string().datetime().openapi({ example: '2025-05-28T00:00:00Z' })
});
const listUsersRoute = createRoute({
method: 'get',
path: '/',
middleware: authMiddleware,
responses: {
200: {
description: '成功获取用户列表',
content: {
'application/json': {
schema: z.array(UserSchema)
}
}
},
500: {
description: '获取用户列表失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(listUsersRoute, async (c) => {
try {
const users = await userService.getUsers();
const usersOut = users.map(user => ({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}));
return c.json(usersOut, 200);
} catch (error) {
return c.json({ code: 500, message: '获取用户列表失败' }, 500);
}
});
export default app;

View File

@@ -0,0 +1,113 @@
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { UserService } from '../../modules/users/user.service';
import { z } from 'zod';
import { authMiddleware } from '../../middleware/auth.middleware';
import { ErrorSchema } from '../../utils/errorHandler';
import { AppDataSource } from '../../data-source';
import { AuthContext } from '../../types/context';
const userService = new UserService(AppDataSource);
const UserSchema = z.object({
id: z.number().openapi({ example: 1 }),
username: z.string().openapi({ example: 'john_doe' }),
email: z.string().email().openapi({ example: 'john@example.com' }),
createdAt: z.string().datetime().openapi({ example: '2025-05-28T00:00:00Z' })
});
const UpdateUserSchema = z.object({
username: z.string().min(3).openapi({
example: 'john_doe',
description: 'Minimum 3 characters'
}).optional(),
password: z.string().min(6).openapi({
example: 'password123',
description: 'Minimum 6 characters'
}).optional(),
email: z.string().email().openapi({
example: 'john@example.com',
description: 'Valid email address'
}).optional()
});
const UpdateParams = z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '1',
description: '用户ID'
})
});
const updateRoute = createRoute({
method: 'patch',
path: '/{id}',
middleware: authMiddleware,
request: {
params: UpdateParams,
body: {
content: {
'application/json': {
schema: UpdateUserSchema
}
}
}
},
responses: {
200: {
description: '用户更新成功',
content: {
'application/json': {
schema: UserSchema
}
}
},
400: {
description: '无效输入',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
404: {
description: '用户不存在',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
500: {
description: '服务器错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono<AuthContext>().openapi(updateRoute, async (c) => {
try {
const { id } = c.req.valid('param');
const data = c.req.valid('json');
const user = await userService.updateUser(parseInt(id), data);
if (!user) {
return c.json({ code: 404, message: '用户不存在' }, 404);
}
return c.json({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt.toISOString()
}, 200);
} catch (error) {
return c.json({
code: 500,
message: error instanceof Error ? error.message : '更新用户失败'
}, 500);
}
});
export default app;

27
src/server/data-source.ts Normal file
View File

@@ -0,0 +1,27 @@
import "reflect-metadata"
import { DataSource } from "typeorm"
import { UserEntity as User } from "./modules/users/user.entity"
import { Role } from "./modules/users/role.entity";
import { checkRequiredEnvVars } from "./utils/env-init";
import { PaymentEntity } from "./modules/payment/payment.entity";
import process from 'node:process'
if (!checkRequiredEnvVars()) {
throw new Error("缺少必要的数据库环境变量配置,请检查.env文件");
}
// postgres
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
username: process.env.DB_USERNAME || "postgres",
password: process.env.DB_PASSWORD || "",
database: process.env.DB_DATABASE || "postgres",
entities: [
User, Role, PaymentEntity
],
migrations: [],
synchronize: process.env.DB_SYNCHRONIZE === "true",
logging: true
})

75
src/server/index.tsx Normal file
View File

@@ -0,0 +1,75 @@
import 'dotenv/config'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { swaggerUI } from '@hono/swagger-ui'
import * as fs from 'fs/promises'
import { renderer } from './renderer'
import createApi from './api'
import HomePage from './pages/home'
const app = new Hono();
// Middleware chain
app.use('*', logger())
app.use('*', cors(
// {
// origin: ['http://localhost:3000'],
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// credentials: true
// }
))
app.use(renderer)
app.route('/', createApi)
app.get('/ui', swaggerUI({
url: '/doc',
persistAuthorization: true
}))
app.get('/assets/:filename', async (c) => {
const filename = c.req.param('filename')
const filePath = import.meta.env.PROD? `./dist/assets/${filename}` : `./public/assets/${filename}`
const content = await fs.readFile(filePath);
const modifyDate = (await fs.stat(filePath))?.mtime?.toUTCString()?? new Date().toUTCString();
const fileExt = filePath.split('.').pop()?.toLowerCase()
// 根据文件扩展名设置适当的 Content-Type
if (fileExt === 'tsx' || fileExt === 'ts') {
c.header('Content-Type', 'text/typescript; charset=utf-8')
} else if (fileExt === 'js' || fileExt === 'mjs') {
c.header('Content-Type', 'application/javascript; charset=utf-8')
} else if (fileExt === 'json') {
c.header('Content-Type', 'application/json; charset=utf-8')
} else if (fileExt === 'html') {
c.header('Content-Type', 'text/html; charset=utf-8')
} else if (fileExt === 'css') {
c.header('Content-Type', 'text/css; charset=utf-8')
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
c.header('Content-Type', `image/${fileExt}`)
}
return c.body(content, {
headers: {
// 'Content-Type': 'text/html; charset=utf-8',
'Last-Modified': modifyDate
}
})
})
app.get('/*', (c) => {
return c.render(
<>
<div id="root"></div>
</>
)
})
export default app

View File

@@ -0,0 +1,36 @@
import { Context, Next } from 'hono';
import { AuthService } from '../modules/auth/auth.service';
import { UserService } from '../modules/users/user.service';
import { AppDataSource } from '../data-source';
import { AuthContext } from '../types/context';
export async function authMiddleware(c: Context<AuthContext>, next: Next) {
try {
const authHeader = c.req.header('Authorization');
if (!authHeader) {
return c.json({ message: 'Authorization header missing' }, 401);
}
const token = authHeader.split(' ')[1];
if (!token) {
return c.json({ message: 'Token missing' }, 401);
}
const userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const decoded = authService.verifyToken(token);
const user = await userService.getUserById(decoded.id);
if (!user) {
return c.json({ message: 'User not found' }, 401);
}
c.set('user', user);
c.set('token', token);
await next();
} catch (error) {
console.error('Authentication error:', error);
return c.json({ message: 'Invalid token' }, 401);
}
}

View File

@@ -0,0 +1,39 @@
import { Context, Next } from 'hono';
import { UserEntity as User } from '../modules/users/user.entity';
type PermissionCheck = (user: User) => boolean | Promise<boolean>;
export function checkPermission(requiredRoles: string[]): PermissionCheck {
return (user: User) => {
if (!user.roles) return false;
return user.roles.some(role => requiredRoles.includes(role.name));
};
}
export function permissionMiddleware(check: PermissionCheck) {
return async (c: Context, next: Next) => {
try {
const user = c.get('user') as User | undefined;
if (!user) {
return c.json({ message: 'Unauthorized' }, 401);
}
const hasPermission = await check(user);
if (!hasPermission) {
return c.json({ message: 'Forbidden' }, 403);
}
await next();
} catch (error) {
console.error('Permission check error:', error);
return c.json({ message: 'Internal server error' }, 500);
}
};
}
// 示例用法:
// app.get('/admin',
// authMiddleware,
// permissionMiddleware(checkPermission(['admin'])),
// (c) => {...}
// )

View File

@@ -0,0 +1,119 @@
import jwt from 'jsonwebtoken';
import { UserService } from '../users/user.service';
import { UserEntity as User } from '../users/user.entity';
import { redisService } from '@/server/utils/redis';
const JWT_SECRET = 'your-secret-key'; // 生产环境应使用环境变量
const JWT_EXPIRES_IN = '7d'; // 7天有效期
const CODE_EXPIRATION = 5 * 60; // 5分钟有效期(秒)
const CODE_KEY_PREFIX = 'auth:code:';
export class AuthService {
private userService: UserService;
constructor(userService: UserService) {
this.userService = userService;
}
async login(username: string, password: string): Promise<{ token: string; user: User }> {
try {
const user = await this.userService.getUserByUsername(username);
if (!user) {
throw new Error('User not found');
}
const isPasswordValid = await this.userService.verifyPassword(user, password);
if (!isPasswordValid) {
throw new Error('Invalid password');
}
const token = this.generateToken(user);
return { token, user };
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
generateToken(user: User): string {
const payload = {
id: user.id,
username: user.username,
roles: user.roles?.map(role => role.name) || []
};
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
verifyToken(token: string): any {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
console.error('Token verification failed:', error);
throw new Error('Invalid token');
}
}
generateFixedCode(phone: string): string {
// 基于手机号生成固定6位验证码
const phoneHash = Array.from(phone).reduce((hash, char) =>
((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
const code = Math.abs(phoneHash % 900000 + 100000).toString();
// 位数不足时补0
return code.padEnd(6, '0');
}
async generateRandCode(phone: string): Promise<string> {
try {
const code = Math.floor(100000 + Math.random() * 900000).toString();
const key = `${CODE_KEY_PREFIX}${phone}`;
await redisService.set(key, code, CODE_EXPIRATION);
return code;
} catch (error) {
console.error('Redis set code error:', error);
throw new Error('验证码生成失败');
}
}
async verifyCode(phone: string, code: string): Promise<boolean> {
if (this.generateFixedCode(phone) === code) {
return true;
}
try {
const key = `${CODE_KEY_PREFIX}${phone}`;
const storedCode = await redisService.get(key);
if (!storedCode) {
return false;
}
const isValid = storedCode === code;
if (isValid) {
await redisService.del(key);
}
return isValid;
} catch (error) {
console.error('Redis verify code error:', error);
return false;
}
}
async logout(token: string): Promise<void> {
try {
// 验证token有效性
const decoded = this.verifyToken(token);
if (!decoded) {
throw new Error('Invalid token');
}
// 实际项目中这里可以添加token黑名单逻辑
// 或者调用Redis等缓存服务使token失效
return Promise.resolve();
} catch (error) {
console.error('Logout failed:', error);
throw error;
}
}
}

View File

@@ -0,0 +1,233 @@
import { z } from 'zod';
import { PaymentStatus } from '../payment.entity';
export const PaymentRequestSchema = z.object({
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
name: z.string().min(1).max(127).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元),支持字符串或数字格式',
example: 9.9
}),
clientip: z.string().ip().openapi({
description: '客户端IP地址',
example: '127.0.0.1'
}),
device: z.string().optional().openapi({
description: '设备信息,可选',
example: 'iPhone13,4'
}),
param: z.string().optional().openapi({
description: '自定义参数,可选',
example: 'user_id=123'
}),
});
export const PaymentApiRequestSchema = z.object({
pid: z.string().min(1).openapi({
description: '商户ID',
example: '10086'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
notify_url: z.string().url().openapi({
description: '异步通知地址',
example: 'https://example.com/payment/notify'
}),
name: z.string().min(1).max(127).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元)',
example: 9.9
}),
clientip: z.string().ip().openapi({
description: '客户端IP地址',
example: '127.0.0.1'
}),
device: z.string().optional().openapi({
description: '设备信息,可选',
example: 'iPhone13,4'
}),
param: z.string().optional().openapi({
description: '自定义参数,可选',
example: 'user_id=123'
}),
sign: z.string().min(1).openapi({
description: '签名',
example: 'e10adc3949ba59abbe56e057f20f883e'
}),
sign_type: z.literal('MD5').default('MD5').openapi({
description: '签名类型',
example: 'MD5'
}),
});
export const PaymentResponseSchema = z.object({
code: z.number().openapi({
description: '响应状态码',
example: 200
}),
msg: z.string().optional().openapi({
description: '响应消息',
example: '支付请求成功'
}),
trade_no: z.string().optional().openapi({
description: '支付平台交易号',
example: '202500000001'
}),
O_id: z.string().optional().openapi({
description: '订单ID',
example: 'order_123456'
}),
payurl: z.string().optional().openapi({
description: '支付跳转URL',
example: 'https://pay.example.com/pay/202500000001'
}),
qrcode: z.string().optional().openapi({
description: '支付二维码内容',
example: 'weixin://wxpay/bizpayurl?pr=abcdefg'
}),
img: z.string().optional().openapi({
description: '支付二维码图片URL',
example: 'https://pay.example.com/qrcode/202500000001.png'
}),
});
export type PaymentRequestDto = z.infer<typeof PaymentRequestSchema>;
export const NotifyRequestSchema = z.object({
pid: z.string().min(1).openapi({
description: '商户ID',
example: '10086'
}),
trade_no: z.string().min(1).openapi({
description: '支付平台交易号',
example: '202500000001'
}),
out_trade_no: z.string().min(1).openapi({
description: '商户订单号',
example: '202500000001'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
name: z.string().min(1).openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.union([
z.string().regex(/^\d+(\.\d{1,2})?$/),
z.number().positive()
]).transform(val => typeof val === 'string' ? parseFloat(val) : val).openapi({
description: '支付金额(元)',
example: 9.9
}),
trade_status: z.enum(['TRADE_SUCCESS', 'TRADE_FAILED']).openapi({
description: '交易状态',
example: 'TRADE_SUCCESS'
}),
param: z.string().optional().openapi({
description: '自定义参数',
example: 'user_id=123'
}),
sign: z.string().min(1).openapi({
description: '签名',
example: 'e10adc3949ba59abbe56e057f20f883e'
}),
sign_type: z.literal('MD5').default('MD5').openapi({
description: '签名类型',
example: 'MD5'
}),
});
export const QueryRequestSchema = z.object({
order_id: z.string().min(1).openapi({
description: '订单ID',
example: 'order_123456'
}),
});
export const QueryResponseSchema = z.object({
code: z.number().openapi({
description: '响应状态码',
example: 200
}),
msg: z.string().optional().openapi({
description: '响应消息',
example: '查询成功'
}),
data: z.object({
out_trade_no: z.string().openapi({
description: '商户订单号',
example: '202500000001'
}),
trade_no: z.string().optional().openapi({
description: '支付平台交易号',
example: '202500000001'
}),
type: z.enum(['alipay', 'wxpay']).openapi({
description: '支付类型',
example: 'alipay'
}),
name: z.string().openapi({
description: '商品名称',
example: 'VIP会员服务'
}),
money: z.number().openapi({
description: '支付金额(元)',
example: 9.9
}),
status: z.nativeEnum(PaymentStatus).openapi({
description: '订单状态',
example: 1
}),
create_time: z.string().openapi({
description: '订单创建时间',
example: '2025-05-29 12:00:00'
}),
update_time: z.string().openapi({
description: '订单更新时间',
example: '2025-05-29 12:05:00'
}),
}).optional(),
});
export const PaymentErrorSchema = z.object({
code: z.string().openapi({
description: 'HTTP错误状态码',
example: 'error',
}),
msg: z.string().openapi({
description: '错误描述信息',
example: '无效的支付参数',
}),
});
export type PaymentResponseDto = z.infer<typeof PaymentResponseSchema>;
export type PaymentErrorDto = z.infer<typeof PaymentErrorSchema>;
export type NotifyRequestDto = z.infer<typeof NotifyRequestSchema>;
export type QueryRequestDto = z.infer<typeof QueryRequestSchema>;
export type QueryResponseDto = z.infer<typeof QueryResponseSchema>;

View File

@@ -0,0 +1,46 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
export enum PaymentStatus {
PENDING = 0,
SUCCESS = 1,
FAILED = 2,
REFUNDED = 3,
CANCELED = 4
}
@Entity('d8d_payments')
export class PaymentEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'out_trade_no', type: 'varchar', length: 64 })
outTradeNo!: string;
@Column({ name: 'trade_no', type: 'varchar', length: 64, nullable: true })
tradeNo!: string;
@Column({ type: 'enum', enum: ['alipay', 'wxpay'] })
type!: 'alipay' | 'wxpay';
@Column({ type: 'decimal', precision: 10, scale: 2 })
money!: number;
@Column({ type: 'varchar', length: 255 })
name!: string;
@Column({ name: 'client_ip', type: 'varchar', length: 64 })
clientIp!: string;
@Column({ type: 'enum', enum: PaymentStatus, default: PaymentStatus.PENDING })
status!: PaymentStatus;
@Column({ name: 'error_msg', type: 'varchar', length: 512, nullable: true })
errorMsg?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
updatedAt!: Date;
}

View File

@@ -0,0 +1,190 @@
import { DataSource } from 'typeorm';
import { createHash } from 'crypto';
import { FormData } from 'node-fetch';
import fetch from 'node-fetch';
import { z } from 'zod';
import process from 'node:process'
import { PaymentEntity, PaymentStatus } from './payment.entity';
import { PaymentApiRequestSchema } from './dto/payment.dto';
import {
PaymentRequestDto,
PaymentResponseDto,
NotifyRequestDto,
QueryRequestDto,
QueryResponseDto,
PaymentRequestSchema,
PaymentResponseSchema,
PaymentErrorSchema,
PaymentErrorDto
} from './dto/payment.dto';
export class PaymentService {
private readonly pid: string;
private readonly pkey: string;
private readonly notifyUrl: string;
private readonly apiUrl: string;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
if (!process.env.ZPAY_PID || !process.env.ZPAY_PKEY || !process.env.ZPAY_NOTIFY_URL) {
throw new Error('Missing required ZPAY environment variables');
}
this.pid = process.env.ZPAY_PID;
this.pkey = process.env.ZPAY_PKEY;
this.notifyUrl = process.env.ZPAY_NOTIFY_URL;
this.apiUrl = process.env.ZPAY_URL || 'https://zpayz.cn/mapi.php';
}
private readonly dataSource: DataSource;
async createPayment(payload: PaymentRequestDto): Promise<PaymentResponseDto> {
// 验证输入参数
const validated = PaymentRequestSchema.parse(payload);
const payment = new PaymentEntity();
payment.outTradeNo = validated.out_trade_no;
payment.type = validated.type;
payment.money = validated.money; // 已由DTO转换
payment.name = validated.name;
payment.clientIp = validated.clientip;
await this.dataSource.manager.save(payment);
const response = await this.callPaymentApi(payload);
if (this.isPaymentResponse(response)) {
if (response.trade_no) payment.tradeNo = response.trade_no;
payment.status = PaymentStatus.SUCCESS;
} else {
payment.status = PaymentStatus.FAILED;
payment.errorMsg = response.msg;
}
await this.dataSource.manager.save(payment);
return this.isPaymentResponse(response) ? response : {
code: 0,
msg: response.msg
};
}
async handleNotify(payload: NotifyRequestDto): Promise<{ code: number; msg: string }> {
// 验证签名
const sign = this.generateSign(payload);
if (sign !== payload.sign) {
return { code: 0, msg: '签名验证失败' };
}
// 查找订单
const payment = await this.dataSource.manager.findOne(PaymentEntity, {
where: [
{ outTradeNo: payload.out_trade_no },
{ tradeNo: payload.trade_no }
]
});
if (!payment) {
return { code: 0, msg: '订单不存在' };
}
// 更新订单状态
payment.status = payload.trade_status === 'TRADE_SUCCESS' ? PaymentStatus.SUCCESS : PaymentStatus.FAILED;
await this.dataSource.manager.save(payment);
return { code: 1, msg: '处理成功' };
}
async queryPayment(payload: QueryRequestDto): Promise<QueryResponseDto> {
const payment = await this.dataSource.manager.findOne(PaymentEntity, {
where: { outTradeNo: payload.order_id }
});
if (!payment) {
return { code: 0, msg: '订单不存在' };
}
return {
code: 1,
msg: '查询成功',
data: {
out_trade_no: payment.outTradeNo,
trade_no: payment.tradeNo,
type: payment.type,
name: payment.name,
money: payment.money,
status: payment.status,
create_time: payment.createdAt.toISOString(),
update_time: payment.updatedAt.toISOString(),
}
};
}
private async callPaymentApi(payload: PaymentRequestDto): Promise<PaymentResponseDto | PaymentErrorDto> {
const apiPayload = {
...payload,
pid: this.pid,
notify_url: this.notifyUrl,
sign_type: 'MD5'
};
const sign = this.generateSign(apiPayload);
const validated = z.object({
...PaymentApiRequestSchema.shape,
sign: z.string().min(1)
}).parse({
...apiPayload,
sign
});
const form = new FormData();
for (const [key, value] of Object.entries(validated)) {
form.append(key, value.toString());
}
const response = await fetch(this.apiUrl, {
method: 'POST',
body: form
});
const json = await response.json();
try {
return PaymentResponseSchema.parse(json);
} catch (error) {
return PaymentErrorSchema.parse(json);
}
}
private isPaymentRequest(payload: any): payload is PaymentRequestDto {
return 'notify_url' in payload && 'clientip' in payload;
}
private isPaymentResponse(response: PaymentResponseDto | PaymentErrorDto): response is PaymentResponseDto {
return typeof response.code === 'number';
}
private generateSign(payload: PaymentRequestDto | Omit<NotifyRequestDto, 'trade_status'>): string {
const params = new URLSearchParams();
params.append('pid', this.pid);
params.append('type', payload.type);
params.append('out_trade_no', payload.out_trade_no);
if (this.isPaymentRequest(payload)) {
params.append('notify_url', this.notifyUrl);
params.append('clientip', payload.clientip);
if (payload.device) params.append('device', payload.device);
}
params.append('name', payload.name);
params.append('money', payload.money.toString());
params.append('sign_type', 'MD5');
if (payload.param) {
params.append('param', payload.param);
}
const paramString = params.toString() + `&key=${this.pkey}`;
return createHash('md5').update(paramString).digest('hex').toLowerCase();
}
}

View File

@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export type Permission = string;
@Entity({ name: 'd8d_role' })
export class Role {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 50, unique: true })
name!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ type: 'simple-array', nullable: false })
permissions: Permission[] = [];
constructor(partial?: Partial<Role>) {
Object.assign(this, partial);
if (!this.permissions) {
this.permissions = [];
}
}
}

View File

@@ -0,0 +1,64 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Role } from './role.entity';
@Entity({ name: 'd8d_users' })
export class UserEntity {
@PrimaryGeneratedColumn({ name: 'user_id', unsigned: true })
id!: number;
@Column({ nullable: true, name: 'account', unique: true, type: 'varchar', length: 255 })
username!: string;
@Column({ nullable: true, name: 'pwd', type: 'varchar', length: 255 })
password!: string;
@Column({ nullable: true, name: 'mail', unique: true, type: 'varchar', length: 100 })
email!: string;
@Column({ nullable: true, name: 'mobile', type: 'varchar', length: 255 })
mobile!: string;
@Column({ nullable: true, type: 'varchar', length: 50 })
nickname!: string;
@Column({ nullable: true, type: 'varchar', length: 50 })
name!: string;
@Column({ nullable: true, name: 'wx_web_openid', type: 'varchar', length: 50 })
wxWebOpenid!: string;
@Column({ nullable: true, name: 'wx_mini_openid', type: 'varchar', length: 50 })
wxMiniOpenid!: string;
@Column({ default: 0, type: 'int' })
status!: number;
@Column({ nullable: true, name: 'company_id', type: 'int' })
companyId!: number;
@Column({ nullable: true, type: 'varchar', length: 100 })
department!: string;
@Column({ nullable: true, type: 'varchar', length: 100 })
position!: string;
@Column({ default: 0, name: 'is_deleted', type: 'int' })
isDeleted!: number;
@Column({ default: 0, name: 'is_disabled', type: 'int' })
isDisabled!: number;
@ManyToMany(() => Role)
@JoinTable()
roles!: Role[];
@CreateDateColumn({name:'created_at', type: 'timestamp'})
createdAt!: Date;
@UpdateDateColumn({name:'updated_at', type: 'timestamp'})
updatedAt!: Date;
constructor(partial?: Partial<UserEntity>) {
Object.assign(this, partial);
}
}

View File

@@ -0,0 +1,138 @@
import { HTTPException } from 'hono/http-exception'
import { DataSource } from 'typeorm';
import { UserEntity as User } from './user.entity';
import * as bcrypt from 'bcrypt';
import { Repository } from 'typeorm';
import { Role } from './role.entity';
const SALT_ROUNDS = 10;
export class UserService {
private userRepository: Repository<User>;
private roleRepository: Repository<Role>;
private readonly dataSource: DataSource;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.userRepository = this.dataSource.getRepository(User);
this.roleRepository = this.dataSource.getRepository(Role);
}
async createUser(userData: Partial<User>): Promise<User> {
try {
if (userData.password) {
userData.password = await bcrypt.hash(userData.password, SALT_ROUNDS);
}
const user = this.userRepository.create(userData);
return await this.userRepository.save(user);
} catch (error) {
console.error('Error creating user:', error);
throw new HTTPException(400,{ message: 'Failed to create user', cause: error})
}
}
async getUserById(id: number): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: { id },
relations: ['roles']
});
} catch (error) {
console.error('Error getting user:', error);
throw new Error('Failed to get user');
}
}
async getUserByUsername(username: string): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: { username },
relations: ['roles']
});
} catch (error) {
console.error('Error getting user:', error);
throw new Error('Failed to get user');
}
}
async getUserByPhone(phone: string): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: { mobile: phone },
relations: ['roles']
});
} catch (error) {
console.error('Error getting user by phone:', error);
throw new Error('Failed to get user by phone');
}
}
async updateUser(id: number, updateData: Partial<User>): Promise<User | null> {
try {
if (updateData.password) {
updateData.password = await bcrypt.hash(updateData.password, SALT_ROUNDS);
}
await this.userRepository.update(id, updateData);
return this.getUserById(id);
} catch (error) {
console.error('Error updating user:', error);
throw new Error('Failed to update user');
}
}
async deleteUser(id: number): Promise<void> {
try {
await this.userRepository.delete(id);
} catch (error) {
console.error('Error deleting user:', error);
throw new Error('Failed to delete user');
}
}
async verifyPassword(user: User, password: string): Promise<boolean> {
return bcrypt.compare(password, user.password);
}
async assignRoles(userId: number, roleIds: number[]): Promise<User | null> {
try {
const user = await this.getUserById(userId);
if (!user) return null;
const roles = await this.roleRepository.findByIds(roleIds);
user.roles = roles;
return await this.userRepository.save(user);
} catch (error) {
console.error('Error assigning roles:', error);
throw new Error('Failed to assign roles');
}
}
async getUsers(): Promise<User[]> {
try {
const users = await this.userRepository.find({
relations: ['roles']
});
return users;
} catch (error) {
console.error('Error getting users:', error);
throw new HTTPException(500, { message: 'Failed to get users', cause: error })
}
}
getUserRepository(): Repository<User> {
return this.userRepository;
}
async getUserByAccount(account: string): Promise<User | null> {
try {
return await this.userRepository.findOne({
where: [{ username: account }, { email: account }],
relations: ['roles']
});
} catch (error) {
console.error('Error getting user by account:', error);
throw new Error('Failed to get user by account');
}
}
}

46
src/server/renderer.tsx Normal file
View File

@@ -0,0 +1,46 @@
import { GlobalConfig } from '@/share/types'
import { reactRenderer } from '@hono/react-renderer'
import { Script, Link } from 'hono-vite-react-stack-node/components'
import process from 'node:process'
// 全局配置常量
const GLOBAL_CONFIG: GlobalConfig = {
OSS_BASE_URL: process.env.OSS_BASE_URL || 'https://oss.d8d.fun',
// API_BASE_URL: '/api',
APP_NAME: process.env.APP_NAME || '多八多Aider',
ENV: process.env.NODE_ENV || 'production', // 添加环境变量,
ROOT_DIRECTORY: process.env.ROOT_DIRECTORY || '',
REMOTE_AUTHORITY: process.env.REMOTE_AUTHORITY || '',
VERSION: process.env.VERSION || '0.1.0',
}
export const renderer = reactRenderer(({ children }) => {
return (
<html>
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<Script />
<Link href="/src/style.css" rel="stylesheet" />
{/* <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
<script dangerouslySetInnerHTML={{ __html: `
const init = () => {
const urlParams = new URLSearchParams(window.location.search);
if (${import.meta.env?.PROD ? "true":"false"} && !urlParams.has('vconsole')) return;
var vConsole = new VConsole({
theme: urlParams.get('vconsole_theme') || 'light',
onReady: function() {
console.log('vConsole is ready');
}
});
}
init();
`}} /> */}
{/* 注入全局配置 */}
<script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
</head>
<body>{children}</body>
</html>
)
})

View File

@@ -0,0 +1,9 @@
import { UserEntity } from "../modules/users/user.entity";
// 扩展Context类型
export type Variables = {
user: UserEntity;
token: string;
}
export type AuthContext = { Variables: Variables }

View File

@@ -0,0 +1,38 @@
import { randomBytes } from 'crypto';
import { writeFileSync, existsSync, readFileSync, appendFileSync } from 'fs';
export function generateJwtSecret(): string {
if (existsSync('.env')) {
const envContent = readFileSync('.env', 'utf-8');
if (envContent.includes('JWT_SECRET')) {
return 'JWT_SECRET已存在';
}
}
const secret = randomBytes(64).toString('hex');
const envLine = `JWT_SECRET=${secret}\n`;
if (existsSync('.env')) {
appendFileSync('.env', envLine);
} else {
writeFileSync('.env', envLine);
}
return secret;
}
export function checkRequiredEnvVars(): boolean {
const requiredVars = [
'DB_HOST',
'DB_PORT',
'DB_USERNAME',
'DB_PASSWORD',
'DB_DATABASE',
'JWT_SECRET'
];
if (!existsSync('.env')) return false;
const envContent = readFileSync('.env', 'utf-8');
return requiredVars.every(varName => envContent.includes(varName));
}

View File

@@ -0,0 +1,34 @@
import { Context } from 'hono'
import { z } from 'zod'
import { HTTPException } from 'hono/http-exception'
export const ErrorSchema = z.object({
code: z.number().openapi({
example: 400,
}),
message: z.string().openapi({
example: 'Bad Request',
}),
})
export const errorHandler = async (err: Error, c: Context) => {
if (err instanceof HTTPException) {
const details = err.cause ? { details: err.cause instanceof Error ? err.cause.message : err.cause } : {}
return c.json(
{
code: err.status,
message: err.message,
...details
},
err.status
)
}
return c.json(
{
code: 500,
message: err.message || 'Internal Server Error'
},
500
)
}

113
src/server/utils/redis.ts Normal file
View File

@@ -0,0 +1,113 @@
import Redis, { RedisOptions, Redis as RedisClient } from 'ioredis';
import debug from 'debug';
const logger = {
info: debug('app:redis:info'),
error: debug('app:redis:error'),
};
type RedisConfig = RedisOptions & {
reconnectDelay?: number;
maxRetries?: number;
};
class RedisService {
private static instance: RedisService;
private client: RedisClient;
private config: RedisConfig;
private retryCount = 0;
private constructor(config: RedisConfig) {
this.config = {
host: 'localhost',
port: 6379,
reconnectDelay: 5000,
maxRetries: 3,
...config,
};
this.client = new Redis(this.config);
this.setupEventListeners();
}
public static getInstance(config?: RedisConfig): RedisService {
if (!RedisService.instance) {
RedisService.instance = new RedisService(config || {});
}
return RedisService.instance;
}
private setupEventListeners(): void {
this.client.on('connect', () => {
logger.info('Redis connected');
this.retryCount = 0;
});
this.client.on('error', (err) => {
logger.error(`Redis error: ${err.message}`);
if (this.retryCount < (this.config.maxRetries || 3)) {
this.retryCount++;
setTimeout(() => {
this.client.connect().catch(() => {});
}, this.config.reconnectDelay);
}
});
this.client.on('reconnecting', () => {
logger.info('Redis reconnecting...');
});
}
public async connect(): Promise<void> {
try {
await this.client.connect();
} catch (err) {
logger.error(`Redis connection failed: ${err}`);
throw err;
}
}
public async disconnect(): Promise<void> {
try {
await this.client.quit();
} catch (err) {
logger.error(`Redis disconnect failed: ${err}`);
throw err;
}
}
public async get(key: string): Promise<string | null> {
return this.client.get(key);
}
public async set(
key: string,
value: string,
ttl?: number
): Promise<'OK' | null> {
if (ttl) {
return this.client.set(key, value, 'EX', ttl);
}
return this.client.set(key, value);
}
public async del(key: string): Promise<number> {
return this.client.del(key);
}
public getClient(): RedisClient {
return this.client;
}
}
// 默认配置从环境变量读取
const redisConfig: RedisConfig = {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined,
password: process.env.REDIS_PASSWORD,
};
const redisService = RedisService.getInstance(redisConfig);
export type { RedisClient };
export { redisService };

73
src/server/utils/sms.ts Normal file
View File

@@ -0,0 +1,73 @@
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import Util, * as $Util from '@alicloud/tea-util';
import process from 'node:process'
type SMSError = {
message?: string;
data?: {
Recommend?: string;
};
};
export class SMS {
/**
* 创建阿里云短信客户端
* @throws {Error} 当阿里云配置缺失时抛出错误
*/
static createClient(): Dysmsapi20170525 {
const accessKeyId = process.env.ALICLOUD_ACCESS_KEY;
const accessKeySecret = process.env.ALICLOUD_SECRET_KEY;
if (!accessKeyId || !accessKeySecret) {
throw new Error('阿里云配置缺失');
}
const config = new $OpenApi.Config({
accessKeyId,
accessKeySecret,
endpoint: 'dysmsapi.aliyuncs.com'
});
return new (Dysmsapi20170525 as any).default(config);
}
/**
* 发送验证码短信
* @param phoneNumber 接收手机号
* @param code 验证码
* @param templateCode 短信模板代码(可选)
* @param signName 短信签名(可选)
* @returns Promise<boolean> 发送是否成功
*/
static async sendVerificationSMS(
phoneNumber: string,
code: string,
templateCode?: string,
signName?: string
): Promise<boolean> {
try {
const client = this.createClient();
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
signName: signName || process.env.SMS_DEFAULT_SIGN_NAME || '多八多',
templateCode: templateCode || process.env.SMS_DEFAULT_TEMPLATE_CODE || 'SMS_164760103',
phoneNumbers: phoneNumber,
templateParam: JSON.stringify({ code })
});
const runtime = new $Util.RuntimeOptions({});
await client.sendSmsWithOptions(sendSmsRequest, runtime);
return true;
} catch (error: unknown) {
const err = error as SMSError;
console.error('短信发送失败:', err.message);
if (err.data?.Recommend) {
console.error('建议:', err.data.Recommend);
}
return false;
}
}
}

273
src/share/types.ts Normal file
View File

@@ -0,0 +1,273 @@
export interface GlobalConfig {
OSS_BASE_URL: string
// API_BASE_URL: string
APP_NAME: string
ENV: string
ROOT_DIRECTORY: string
REMOTE_AUTHORITY: string
VERSION: string
}
// 定义工作空间数据接口
export interface Workspace {
id: number;
name: string;
key: string;
region: string;
owner_id: number;
owner_email: string;
status: string;
quotas: WorkspaceQuotas;
tags?: Record<string, string>;
created_at: string;
updated_at: string;
}
// 项目类型枚举常量
export enum ItemType {
PROJECT = 'project',
TEMPLATE = 'template'
}
// 预览模式枚举常量
export enum PreviewMode {
MOBILE = 'mobile',
DESKTOP = 'desktop'
}
export interface WorkspaceQuotas {
max_tables: number;
max_table_size: number;
max_total_size: number;
max_storage_size: number;
max_functions: number;
max_redis_instances: number;
max_redis_memory: number;
}
// 定义项目数据接口
export interface Project {
id: number;
workspace_id: number;
name: string;
description?: string;
files: ProjectFile[]; // 使用jsonb字段存储文件列表
thumbnail?: string;
user_id?: number;
git_repo_url?: string; // 添加Git仓库URL字段
created_at: string;
updated_at: string;
}
// 定义项目文件接口
export interface ProjectFile {
id?: number;
project_id?: number;
name: string;
content: string;
type: string; // 文件类型html, css, js, json等
is_main?: boolean;
created_at?: string;
updated_at?: string;
}
// 定义模板数据接口
export interface Template {
id: number;
workspace_id: number;
name: string;
description?: string;
files: TemplateFile[]; // 使用jsonb字段存储文件列表
thumbnail?: string;
category: string;
git_repo_url?: string; // 添加Git仓库URL字段
created_at: string;
created_by?: number; // 添加创建者字段
}
// 定义模板文件接口
export interface TemplateFile {
id?: number;
template_id?: number;
name: string;
content: string;
type: string; // 文件类型html, css, js, json等
is_main?: boolean;
created_at?: string;
updated_at?: string;
}
// 定义模板广场数据接口
export interface PublicTemplate {
id: number;
originalId: number; // 原始模板ID
name: string;
description?: string;
thumbnail?: string;
category: string;
authorId: number; // 作者ID
authorName?: string; // 作者名称
downloads: number; // 下载次数
gitRepoUrl?: string; // Git仓库URL
createdAt: string;
updatedAt: string;
}
// 定义聊天记录接口
export interface ChatHistory {
id: number
project_id?: number
user_id?: number
role: string
message: string
created_at: string
}
// 定义编辑信息接口
export interface Edits {
files?: string[];
commit_hash?: string;
commit_message?: string;
diff?: string;
updated_files?: UpdatedFile[];
}
// 定义更新文件接口
export interface UpdatedFile {
name: string;
content?: string;
type: string;
is_main: boolean;
}
// 定义用户数据接口
export interface AdminUser {
id: number
nickname: string
name?: string;
phone?: string
email?: string
status: number
}
export interface D8dUser {
user_id: number
account: string
mobile: string
mail?: string
nickname?: string
name?: string
wx_web_openid?: string
wx_mini_openid?: string
status: number
created_at: string
updated_at: string
}
// 模板分类常量定义
export const TEMPLATE_CATEGORIES = [
{ value: 'custom', label: '自定义' },
{ value: 'webpage', label: '网页' },
{ value: 'dashboard', label: '仪表盘' },
{ value: 'form', label: '表单' },
{ value: 'landing', label: '落地页' },
{ value: 'application', label: '应用' },
{ value: 'blog', label: '博客' },
{ value: 'ecommerce', label: '电商' },
{ value: 'portfolio', label: '作品集' },
{ value: 'admin', label: '后台管理' },
{ value: 'mobile', label: '移动端' },
{ value: 'chart', label: '图表' },
{ value: 'report', label: '报表' },
{ value: 'list', label: '列表' },
{ value: 'erp', label: 'ERP进销存' },
{ value: 'gallery', label: '画廊' },
{ value: 'chat', label: '聊天' },
];
// 模板类型定义接口
export interface TemplateType {
id: string;
name: string;
description: string;
git_repo_url: string;
}
// 定义模板类型映射
export interface TemplateTypeMap {
[key: string]: TemplateType;
}
// 类型定义
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
id?: number; // 添加可选的 id 属性
}
// 定义认证上下文类型
export interface AuthContextType {
token: string | null;
refreshToken: string | null;
user: D8dUser | null;
login: (phone: string, code: string) => Promise<boolean>;
logout: () => Promise<void>;
isAuthenticated: boolean;
isLoading: boolean; // 添加加载状态
}
// 定义ChatTopic接口
export interface ChatTopic {
id: number;
title: string;
description?: string;
workspace_id: number;
project_id?: number;
template_id?: number;
user_id?: number;
created_at?: string;
updated_at?: string;
last_active_at?: string;
}
// 定义工作空间上下文类型
export interface WorkspaceContextType {
currentWorkspace: Workspace | null;
showWorkspace: boolean;
setShowWorkspace: (value: boolean) => void;
setCurrentWorkspace: (workspace: Workspace | null) => void;
handleWorkspaceSelect: (workspace: Workspace | null) => void;
handleSetShowWorkspace: (workspaceOrBoolean: Workspace | boolean) => void;
isWorkspaceLoading: boolean;
workspaceError: Error | null;
refetchWorkspace: () => void;
}
// 文件类型
export interface GitFile {
name: string;
path: string;
content: string;
type: string;
isDirectory: boolean;
is_main: boolean;
}
// 协作仓库接口
export interface Repository {
id: number;
name: string;
full_name: string;
workspace_id: number;
template_id: number | null;
project_id: number | null;
html_url: string;
description: string;
clone_url: string;
created_at: string;
updated_at: string;
workspace_name: string;
template_name: string | null;
project_name: string | null;
}

1
src/style.css Normal file
View File

@@ -0,0 +1 @@
@import 'tailwindcss';

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"incremental": true,
"strict": true,
"skipLibCheck": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"types": [
"vite/client"
],
"jsx": "react-jsx",
"jsxImportSource": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

41
vite.config.ts Normal file
View File

@@ -0,0 +1,41 @@
import reactStack from 'hono-vite-react-stack-node'
import { defineConfig } from 'vite'
import i18nextLoader from 'vite-plugin-i18next-loader'
export default defineConfig({
plugins: [
i18nextLoader({
paths: ['src/client/i18n/locales']
}),
reactStack({
minify: false,
port: 23956
}),
],
// 配置 @ 别名
resolve: {
alias: {
'@': '/src',
},
},
build:{
// assetsDir: 'ai-assets',
},
ssr:{
external:[
'dotenv','typeorm','bcrypt', '@d8d-appcontainer/api',
'pg', 'ioredis','reflect-metadata',
'@alicloud/bssopenapi20171214',
"@alicloud/credentials",
'@alicloud/eci20180808',
'@alicloud/dysmsapi20170525',
"@alicloud/openapi-client",
"@alicloud/tea-util",
]
},
server:{
host:'0.0.0.0',
port: 23956,
allowedHosts: true,
},
})