ci: 添加 Docker 构建和推送工作流
- 新增 Dockerfile 和 .dockerignore 文件 - 添加 Gitea 持续集成工作流,用于构建和推送 Docker 镜像 - 新增 .gitignore 文件,忽略构建和配置文件 - 添加项目结构和规范文档,包括 TypeScript、模块化、API、数据库等规范 - 新增前端和后端的基础代码结构
This commit is contained in:
48
.gitea/workflows/release.yaml
Normal file
48
.gitea/workflows/release.yaml
Normal 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
42
.gitignore
vendored
Normal 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
|
||||
25
.roo/rules/01-general.md
Normal file
25
.roo/rules/01-general.md
Normal 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 (缓存/会话管理)
|
||||
5
.roo/rules/02-typescript.md
Normal file
5
.roo/rules/02-typescript.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# TypeScript规范
|
||||
|
||||
1. **严格模式**
|
||||
- 启用所有严格类型检查选项
|
||||
- 避免使用`any`类型
|
||||
8
.roo/rules/03-modules.md
Normal file
8
.roo/rules/03-modules.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 模块化规范
|
||||
|
||||
1. **模块组织**
|
||||
- 按功能划分模块
|
||||
- 每个模块包含:
|
||||
- 实体定义
|
||||
- 服务层
|
||||
- 路由控制器
|
||||
22
.roo/rules/04-api.md
Normal file
22
.roo/rules/04-api.md
Normal 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文档
|
||||
5
.roo/rules/05-database.md
Normal file
5
.roo/rules/05-database.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 数据库规范
|
||||
|
||||
1. **迁移管理**
|
||||
- 使用迁移脚本管理表结构变更
|
||||
- 实体类与数据库表严格映射
|
||||
18
.roo/rules/06-service-di.md
Normal file
18
.roo/rules/06-service-di.md
Normal 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
180
.roo/rules/07-openapi.md
Normal 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
45
.roo/rules/08-rpc.md
Normal 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
23
Dockerfile
Normal 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
3
README.md
Normal 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
72
docs/aliyun-sms.md
Normal 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
37
docs/sso-verify.md
Normal 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
44
docs/zpay.cn.md
Normal file
@@ -0,0 +1,44 @@
|
||||
API信息(兼容 易支付 接口)
|
||||
接口地址:process.env.ZPAY_URL
|
||||
|
||||
商户ID(PID):process.env.ZPAY_PID
|
||||
|
||||
商户密钥(PKEY):process.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¶m=user_id%3D123&sign=9a3406e5f177c03646274749463ad979&sign_type=MD5
|
||||
66
package.json
Normal file
66
package.json
Normal 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
70
src/client/app.tsx
Normal 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
|
||||
19
src/client/components/LanguageSwitcher.tsx
Normal file
19
src/client/components/LanguageSwitcher.tsx
Normal 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
19
src/client/i18n/config.ts
Normal 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;
|
||||
7
src/client/i18n/locales/en/translation.json
Normal file
7
src/client/i18n/locales/en/translation.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"welcome": "Welcome",
|
||||
"language": "Language",
|
||||
"switch_language": "Switch Language"
|
||||
}
|
||||
}
|
||||
7
src/client/i18n/locales/zh/translation.json
Normal file
7
src/client/i18n/locales/zh/translation.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"welcome": "欢迎",
|
||||
"language": "语言",
|
||||
"switch_language": "切换语言"
|
||||
}
|
||||
}
|
||||
5
src/client/i18next-loader.d.ts
vendored
Normal file
5
src/client/i18next-loader.d.ts
vendored
Normal 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
15
src/client/index.tsx
Normal 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
137
src/server/api.ts
Normal 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
|
||||
15
src/server/api/auth/index.ts
Normal file
15
src/server/api/auth/index.ts
Normal 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
|
||||
}
|
||||
13
src/server/api/auth/login/index.ts
Normal file
13
src/server/api/auth/login/index.ts
Normal 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,
|
||||
}
|
||||
71
src/server/api/auth/login/password.ts
Normal file
71
src/server/api/auth/login/password.ts
Normal 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
|
||||
144
src/server/api/auth/login/sms.ts
Normal file
144
src/server/api/auth/login/sms.ts
Normal 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
|
||||
68
src/server/api/auth/logout/index.ts
Normal file
68
src/server/api/auth/logout/index.ts
Normal 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;
|
||||
46
src/server/api/auth/me/get.ts
Normal file
46
src/server/api/auth/me/get.ts
Normal 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
|
||||
10
src/server/api/auth/me/index.ts
Normal file
10
src/server/api/auth/me/index.ts
Normal 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,
|
||||
}
|
||||
58
src/server/api/auth/phone-code/fixed.ts
Normal file
58
src/server/api/auth/phone-code/fixed.ts
Normal 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
|
||||
8
src/server/api/auth/phone-code/index.ts
Normal file
8
src/server/api/auth/phone-code/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
import fixed from './fixed';
|
||||
import sms from './sms';
|
||||
|
||||
export default {
|
||||
fixed,
|
||||
sms
|
||||
}
|
||||
53
src/server/api/auth/phone-code/sms.ts
Normal file
53
src/server/api/auth/phone-code/sms.ts
Normal 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
|
||||
76
src/server/api/auth/register/create.ts
Normal file
76
src/server/api/auth/register/create.ts
Normal 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
|
||||
10
src/server/api/auth/register/index.ts
Normal file
10
src/server/api/auth/register/index.ts
Normal 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,
|
||||
}
|
||||
10
src/server/api/auth/schemas.ts
Normal file
10
src/server/api/auth/schemas.ts
Normal 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')
|
||||
69
src/server/api/auth/sso-verify.ts
Normal file
69
src/server/api/auth/sso-verify.ts
Normal 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
54
src/server/api/base.ts
Normal 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
192
src/server/api/init.ts
Normal 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);
|
||||
}
|
||||
89
src/server/api/migration.ts
Normal file
89
src/server/api/migration.ts
Normal 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
202
src/server/api/payment.ts
Normal 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
|
||||
};
|
||||
80
src/server/api/users/create.ts
Normal file
80
src/server/api/users/create.ts
Normal 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;
|
||||
61
src/server/api/users/delete.ts
Normal file
61
src/server/api/users/delete.ts
Normal 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;
|
||||
77
src/server/api/users/get.ts
Normal file
77
src/server/api/users/get.ts
Normal 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;
|
||||
23
src/server/api/users/index.ts
Normal file
23
src/server/api/users/index.ts
Normal 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
|
||||
}
|
||||
57
src/server/api/users/list.ts
Normal file
57
src/server/api/users/list.ts
Normal 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;
|
||||
113
src/server/api/users/update.ts
Normal file
113
src/server/api/users/update.ts
Normal 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
27
src/server/data-source.ts
Normal 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
75
src/server/index.tsx
Normal 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
|
||||
36
src/server/middleware/auth.middleware.ts
Normal file
36
src/server/middleware/auth.middleware.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
39
src/server/middleware/permission.middleware.ts
Normal file
39
src/server/middleware/permission.middleware.ts
Normal 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) => {...}
|
||||
// )
|
||||
119
src/server/modules/auth/auth.service.ts
Normal file
119
src/server/modules/auth/auth.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
233
src/server/modules/payment/dto/payment.dto.ts
Normal file
233
src/server/modules/payment/dto/payment.dto.ts
Normal 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>;
|
||||
46
src/server/modules/payment/payment.entity.ts
Normal file
46
src/server/modules/payment/payment.entity.ts
Normal 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;
|
||||
|
||||
}
|
||||
190
src/server/modules/payment/payment.service.ts
Normal file
190
src/server/modules/payment/payment.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
25
src/server/modules/users/role.entity.ts
Normal file
25
src/server/modules/users/role.entity.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/server/modules/users/user.entity.ts
Normal file
64
src/server/modules/users/user.entity.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
138
src/server/modules/users/user.service.ts
Normal file
138
src/server/modules/users/user.service.ts
Normal 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
46
src/server/renderer.tsx
Normal 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>
|
||||
)
|
||||
})
|
||||
9
src/server/types/context.ts
Normal file
9
src/server/types/context.ts
Normal 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 }
|
||||
38
src/server/utils/env-init.ts
Normal file
38
src/server/utils/env-init.ts
Normal 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));
|
||||
}
|
||||
34
src/server/utils/errorHandler.ts
Normal file
34
src/server/utils/errorHandler.ts
Normal 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
113
src/server/utils/redis.ts
Normal 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
73
src/server/utils/sms.ts
Normal 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
273
src/share/types.ts
Normal 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
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
||||
@import 'tailwindcss';
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal 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
41
vite.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user