web
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
# 开发环境变量配置
|
||||
GO_ENV=development
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=JKjk20011115
|
||||
DB_NAME=ai_dianshang_dev
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=dev-jwt-secret-key-not-for-production
|
||||
JWT_EXPIRE=7200
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 微信小程序配置
|
||||
WECHAT_APP_ID=wx430b70d696b4dbd7
|
||||
WECHAT_APP_SECRET=147751f8789272e56a43f748bec4b56b
|
||||
|
||||
# 微信支付配置(开发环境)
|
||||
WECHAT_PAY_APP_ID=wx430b70d696b4dbd7
|
||||
WECHAT_PAY_MCH_ID=
|
||||
WECHAT_PAY_API_V3_KEY=
|
||||
WECHAT_PAY_CERT_PATH=
|
||||
WECHAT_PAY_KEY_PATH=
|
||||
WECHAT_PAY_SERIAL_NO=
|
||||
WECHAT_PAY_NOTIFY_URL=http://localhost:8080/api/payment/notify
|
||||
WECHAT_PAY_REFUND_NOTIFY_URL=http://localhost:8080/api/refunds/callback
|
||||
WECHAT_PAY_ENVIRONMENT=sandbox
|
||||
@@ -1,36 +0,0 @@
|
||||
# 环境变量配置示例文件
|
||||
# 复制此文件为 .env 并根据实际情况修改配置
|
||||
|
||||
# 应用环境 (development/dev, test/testing, production/prod)
|
||||
GO_ENV=development
|
||||
# 或者使用以下任一变量名
|
||||
# APP_ENV=development
|
||||
# ENVIRONMENT=development
|
||||
|
||||
# 数据库配置 (生产环境使用)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password_here
|
||||
DB_NAME=ai_dianshang
|
||||
|
||||
# Redis配置 (生产环境使用)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# JWT配置 (生产环境使用)
|
||||
JWT_SECRET=your-super-secret-jwt-key-for-production
|
||||
JWT_EXPIRE=7200
|
||||
|
||||
# 日志配置 (生产环境使用)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 微信小程序配置 (生产环境使用)
|
||||
WECHAT_APP_ID=wx430b70d696b4dbd7
|
||||
WECHAT_APP_SECRET=your_wechat_app_secret_here
|
||||
|
||||
# 服务器配置
|
||||
SERVER_PORT=8080
|
||||
SERVER_MODE=release
|
||||
@@ -1,26 +0,0 @@
|
||||
# 测试环境变量配置
|
||||
GO_ENV=test
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=test_password_123
|
||||
DB_NAME=ai_dianshang_test
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=1
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=test-jwt-secret-key-for-testing-only
|
||||
JWT_EXPIRE=3600
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 微信小程序配置
|
||||
WECHAT_APP_ID=wx_test_app_id
|
||||
WECHAT_APP_SECRET=test_app_secret_for_testing
|
||||
6
server/.idea/vcs.xml
generated
Normal file
6
server/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
84
server/.idea/workspace.xml
generated
84
server/.idea/workspace.xml
generated
@@ -4,13 +4,93 @@
|
||||
<option name="autoReloadType" value="ALL" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="7ea8f074-aff1-4978-8a35-3734e098b780" name="更改" comment="" />
|
||||
<list default="true" id="7ea8f074-aff1-4978-8a35-3734e098b780" name="更改" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/../Makefile" beforeDir="false" afterPath="$PROJECT_DIR$/../Makefile" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/Dashboard-c6aa87b9.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/Login-369dc207.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/Permissions-4b3b2679.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/ProductList-e03d6e50.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/ProductList-ea349e38.css" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/Roles-a22d3b28.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/categories-80fde5d4.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/categories-fb0f0b81.css" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-0a06735a.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-12c2b32b.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-20ceadcc.css" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-3cdcc6af.css" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-60dd7d88.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-92073a1b.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-d81478ed.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-eacf7336.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/index-fa7a0ed8.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/orders-9608cfb6.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/assets/products-abc7ab38.js" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/dist/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/dist/index.html" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/package.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/api/products.js" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/api/products.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/layout/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/layout/index.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/router/index.js" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/router/index.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/banners/components/BannerDetail.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/banners/components/BannerDetail.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/banners/components/BannerForm.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/banners/components/BannerForm.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/banners/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/banners/index.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/products/categories.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/products/categories.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/products/components/CategoryForm.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/products/components/CategoryForm.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/products/components/ProductForm.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/products/components/ProductForm.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../admin/src/views/products/index.vue" beforeDir="false" afterPath="$PROJECT_DIR$/../admin/src/views/products/index.vue" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../miniprogram/config/index.js" beforeDir="false" afterPath="$PROJECT_DIR$/../miniprogram/config/index.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../miniprogram/services/good/fetchCategoryList.js" beforeDir="false" afterPath="$PROJECT_DIR$/../miniprogram/services/good/fetchCategoryList.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../miniprogram/utils/env-switcher.js" beforeDir="false" afterPath="$PROJECT_DIR$/../miniprogram/utils/env-switcher.js" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.env.dev" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.env.example" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.env.test" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Dockerfile" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/configs/config.dev.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/configs/config.dev.yaml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/configs/config.oss.example.yaml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/configs/config.prod.example.yaml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/configs/config.prod.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/configs/config.prod.yaml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/config/config.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/config/config.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/admin_product.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/admin_product.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/cart.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/cart.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/comment.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/comment.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/frontend.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/frontend.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/order.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/order.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/payment.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/payment.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/product.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/product.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/handler/user.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/handler/user.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/middleware/cors.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/middleware/cors.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/model/product.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/model/product.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/model/user.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/model/user.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/repository/comment.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/repository/comment.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/repository/coupon.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/repository/coupon.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/repository/order.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/repository/order.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/repository/product.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/repository/product.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/repository/user.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/repository/user.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/router/router.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/router/router.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/cart.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/cart.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/comment.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/comment.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/coupon.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/coupon.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/product.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/product.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/user.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/user.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/internal/service/wechat_pay.go" beforeDir="false" afterPath="$PROJECT_DIR$/internal/service/wechat_pay.go" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/logs/app.dev.log" beforeDir="false" afterPath="$PROJECT_DIR$/logs/app.dev.log" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/001_create_refund_table.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/001_create_refund_table_fixed.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/002_fix_ai_products_id.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/003_add_refunded_at_to_orders.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/add_comment_tables.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/migrations/add_wechat_fields_to_users.sql" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/start.sh" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/../start.txt" beforeDir="false" afterPath="$PROJECT_DIR$/../start.txt" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="GOROOT" url="file://$PROJECT_DIR$/../../../environment/go" />
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 2
|
||||
}</component>
|
||||
@@ -26,9 +106,11 @@
|
||||
"RunOnceActivity.GoLinterPluginOnboarding": "true",
|
||||
"RunOnceActivity.GoLinterPluginStorageMigration": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.go.formatter.settings.were.checked": "true",
|
||||
"RunOnceActivity.go.migrated.go.modules.settings": "true",
|
||||
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"go.import.settings.migrated": "true",
|
||||
"go.sdk.automatically.set": "true",
|
||||
"last_opened_file_path": "D:/project/Work/dianshang/server",
|
||||
|
||||
949
server/API_DOCUMENTATION.md
Normal file
949
server/API_DOCUMENTATION.md
Normal file
@@ -0,0 +1,949 @@
|
||||
# 后端API接口文档
|
||||
|
||||
## 1. 接口概述
|
||||
|
||||
### 1.1 基本信息
|
||||
- **项目名称**: vizee电商后端服务
|
||||
- **技术栈**: Go + Gin + GORM + MySQL
|
||||
- **API版本**: v1
|
||||
- **Base URL**:
|
||||
- 开发环境: `http://localhost:8080/api/v1`
|
||||
- 测试环境: `配置中`
|
||||
- 生产环境: `配置中`
|
||||
|
||||
### 1.2 通用说明
|
||||
- **数据格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
- **认证方式**: JWT Token
|
||||
- **时间格式**: ISO 8601 (YYYY-MM-DDTHH:mm:ss.SSSZ)
|
||||
|
||||
### 1.3 通用响应格式
|
||||
|
||||
**成功响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "错误信息描述",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 状态码说明
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权,需要登录 |
|
||||
| 403 | 禁止访问 |
|
||||
| 404 | 资源不存在 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证接口
|
||||
|
||||
### 2.1 用户登录
|
||||
|
||||
**接口地址**: `POST /auth/login`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 用户注册
|
||||
|
||||
**接口地址**: `POST /auth/register`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123",
|
||||
"nickname": "用户昵称"
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "注册成功",
|
||||
"data": {
|
||||
"user_id": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 微信登录
|
||||
|
||||
**接口地址**: `POST /auth/wechat/login`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"code": "微信登录code"
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"openid": "微信openid",
|
||||
"nickname": "微信昵称",
|
||||
"avatar": "微信头像"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户接口
|
||||
|
||||
### 3.1 获取用户信息
|
||||
|
||||
**接口地址**: `GET /users/profile`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL",
|
||||
"phone": "手机号",
|
||||
"gender": 1,
|
||||
"points": 100,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 更新用户信息
|
||||
|
||||
**接口地址**: `PUT /users/profile`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"nickname": "新昵称",
|
||||
"avatar": "新头像URL",
|
||||
"phone": "手机号",
|
||||
"gender": 1
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "更新成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 获取用户地址列表
|
||||
|
||||
**接口地址**: `GET /users/addresses`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "收货人",
|
||||
"phone": "13800138000",
|
||||
"province": "广东省",
|
||||
"city": "深圳市",
|
||||
"district": "南山区",
|
||||
"detail": "详细地址",
|
||||
"is_default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 添加收货地址
|
||||
|
||||
**接口地址**: `POST /users/addresses`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"name": "收货人",
|
||||
"phone": "13800138000",
|
||||
"province": "广东省",
|
||||
"city": "深圳市",
|
||||
"district": "南山区",
|
||||
"detail": "详细地址",
|
||||
"is_default": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 商品接口
|
||||
|
||||
### 4.1 获取商品列表
|
||||
|
||||
**接口地址**: `GET /products`
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 否 | 页码,默认1 |
|
||||
| page_size | int | 否 | 每页数量,默认20 |
|
||||
| category_id | int | 否 | 分类ID |
|
||||
| keyword | string | 否 | 搜索关键词 |
|
||||
| sort | string | 否 | 排序方式:price_asc/price_desc/sales/new |
|
||||
| min_price | float | 否 | 最低价格 |
|
||||
| max_price | float | 否 | 最高价格 |
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "商品名称",
|
||||
"cover": "封面图URL",
|
||||
"price": 99.00,
|
||||
"original_price": 199.00,
|
||||
"sales": 1000,
|
||||
"stock": 500,
|
||||
"rating": 4.8,
|
||||
"comment_count": 100
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 获取商品详情
|
||||
|
||||
**接口地址**: `GET /products/:id`
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "商品名称",
|
||||
"cover": "封面图URL",
|
||||
"images": ["图片1", "图片2"],
|
||||
"price": 99.00,
|
||||
"original_price": 199.00,
|
||||
"sales": 1000,
|
||||
"stock": 500,
|
||||
"rating": 4.8,
|
||||
"comment_count": 100,
|
||||
"description": "商品描述",
|
||||
"detail": "商品详情HTML",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "分类名称"
|
||||
},
|
||||
"skus": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "规格名称",
|
||||
"price": 99.00,
|
||||
"stock": 100,
|
||||
"image": "规格图片"
|
||||
}
|
||||
],
|
||||
"specs": [
|
||||
{
|
||||
"name": "颜色",
|
||||
"values": ["红色", "蓝色"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 获取商品评价
|
||||
|
||||
**接口地址**: `GET /products/:id/comments`
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 否 | 页码 |
|
||||
| page_size | int | 否 | 每页数量 |
|
||||
| rating | int | 否 | 评分筛选:1-5 |
|
||||
| has_image | bool | 否 | 是否有图 |
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"stats": {
|
||||
"total": 100,
|
||||
"rating_5": 80,
|
||||
"rating_4": 15,
|
||||
"rating_3": 3,
|
||||
"rating_2": 1,
|
||||
"rating_1": 1,
|
||||
"avg_rating": 4.8
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"user": {
|
||||
"nickname": "用户昵称",
|
||||
"avatar": "头像URL"
|
||||
},
|
||||
"rating": 5,
|
||||
"content": "评价内容",
|
||||
"images": ["图片1", "图片2"],
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 分类接口
|
||||
|
||||
### 5.1 获取分类列表
|
||||
|
||||
**接口地址**: `GET /categories`
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "艺术手工",
|
||||
"icon": "图标URL",
|
||||
"sort": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 11,
|
||||
"name": "陶艺",
|
||||
"parent_id": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 购物车接口
|
||||
|
||||
### 6.1 获取购物车
|
||||
|
||||
**接口地址**: `GET /cart`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"product_id": 1,
|
||||
"sku_id": 1,
|
||||
"product_name": "商品名称",
|
||||
"sku_name": "规格名称",
|
||||
"image": "图片URL",
|
||||
"price": 99.00,
|
||||
"quantity": 2,
|
||||
"stock": 100,
|
||||
"selected": true
|
||||
}
|
||||
],
|
||||
"total_price": 198.00,
|
||||
"total_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 添加到购物车
|
||||
|
||||
**接口地址**: `POST /cart`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"product_id": 1,
|
||||
"sku_id": 1,
|
||||
"quantity": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 更新购物车数量
|
||||
|
||||
**接口地址**: `PUT /cart/:id`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"quantity": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 删除购物车商品
|
||||
|
||||
**接口地址**: `DELETE /cart/:id`
|
||||
|
||||
### 6.5 清空购物车
|
||||
|
||||
**接口地址**: `DELETE /cart/clear`
|
||||
|
||||
---
|
||||
|
||||
## 7. 订单接口
|
||||
|
||||
### 7.1 创建订单
|
||||
|
||||
**接口地址**: `POST /orders`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"address_id": 1,
|
||||
"items": [
|
||||
{
|
||||
"product_id": 1,
|
||||
"sku_id": 1,
|
||||
"quantity": 2
|
||||
}
|
||||
],
|
||||
"coupon_id": 1,
|
||||
"remark": "备注信息"
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "下单成功",
|
||||
"data": {
|
||||
"order_id": "ORD20240101123456",
|
||||
"total_amount": 198.00,
|
||||
"pay_amount": 188.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 获取订单列表
|
||||
|
||||
**接口地址**: `GET /orders`
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | int | 否 | 页码 |
|
||||
| page_size | int | 否 | 每页数量 |
|
||||
| status | string | 否 | 订单状态:pending/paid/shipped/completed/cancelled |
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": "ORD20240101123456",
|
||||
"status": "paid",
|
||||
"status_text": "已支付",
|
||||
"total_amount": 198.00,
|
||||
"pay_amount": 188.00,
|
||||
"items": [
|
||||
{
|
||||
"product_name": "商品名称",
|
||||
"sku_name": "规格名称",
|
||||
"image": "图片URL",
|
||||
"price": 99.00,
|
||||
"quantity": 2
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 获取订单详情
|
||||
|
||||
**接口地址**: `GET /orders/:id`
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": "ORD20240101123456",
|
||||
"status": "paid",
|
||||
"status_text": "已支付",
|
||||
"total_amount": 198.00,
|
||||
"pay_amount": 188.00,
|
||||
"shipping_fee": 10.00,
|
||||
"discount_amount": 20.00,
|
||||
"address": {
|
||||
"name": "收货人",
|
||||
"phone": "13800138000",
|
||||
"province": "广东省",
|
||||
"city": "深圳市",
|
||||
"district": "南山区",
|
||||
"detail": "详细地址"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"product_name": "商品名称",
|
||||
"sku_name": "规格名称",
|
||||
"image": "图片URL",
|
||||
"price": 99.00,
|
||||
"quantity": 2
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"paid_at": "2024-01-01T00:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 取消订单
|
||||
|
||||
**接口地址**: `POST /orders/:id/cancel`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"reason": "取消原因"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 确认收货
|
||||
|
||||
**接口地址**: `POST /orders/:id/confirm`
|
||||
|
||||
---
|
||||
|
||||
## 8. 支付接口
|
||||
|
||||
### 8.1 创建支付
|
||||
|
||||
**接口地址**: `POST /pay/create`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"order_id": "ORD20240101123456",
|
||||
"pay_type": "wechat"
|
||||
}
|
||||
```
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"pay_params": {
|
||||
"timeStamp": "1234567890",
|
||||
"nonceStr": "random_string",
|
||||
"package": "prepay_id=xxx",
|
||||
"signType": "MD5",
|
||||
"paySign": "signature"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 支付回调
|
||||
|
||||
**接口地址**: `POST /pay/callback`
|
||||
|
||||
**说明**: 由支付平台调用,处理支付结果
|
||||
|
||||
---
|
||||
|
||||
## 9. 退款接口
|
||||
|
||||
### 9.1 申请退款
|
||||
|
||||
**接口地址**: `POST /refunds`
|
||||
|
||||
**请求参数**:
|
||||
```json
|
||||
{
|
||||
"order_id": "ORD20240101123456",
|
||||
"reason": "退款原因",
|
||||
"description": "详细说明",
|
||||
"images": ["图片1", "图片2"]
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 获取退款列表
|
||||
|
||||
**接口地址**: `GET /refunds`
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"order_id": "ORD20240101123456",
|
||||
"status": "pending",
|
||||
"status_text": "待审核",
|
||||
"amount": 188.00,
|
||||
"reason": "退款原因",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 取消退款申请
|
||||
|
||||
**接口地址**: `POST /refunds/:id/cancel`
|
||||
|
||||
---
|
||||
|
||||
## 10. Banner接口
|
||||
|
||||
### 10.1 获取Banner列表
|
||||
|
||||
**接口地址**: `GET /banners`
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| position | string | 否 | 位置:home/category |
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Banner标题",
|
||||
"image": "图片URL",
|
||||
"link": "跳转链接",
|
||||
"sort": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 优惠券接口
|
||||
|
||||
### 11.1 获取优惠券列表
|
||||
|
||||
**接口地址**: `GET /coupons`
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "优惠券名称",
|
||||
"type": "discount",
|
||||
"discount": 0.9,
|
||||
"min_amount": 100.00,
|
||||
"max_discount": 50.00,
|
||||
"start_time": "2024-01-01T00:00:00Z",
|
||||
"end_time": "2024-12-31T23:59:59Z",
|
||||
"total_count": 1000,
|
||||
"received_count": 500,
|
||||
"is_received": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 领取优惠券
|
||||
|
||||
**接口地址**: `POST /coupons/:id/receive`
|
||||
|
||||
### 11.3 获取我的优惠券
|
||||
|
||||
**接口地址**: `GET /coupons/my`
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| status | string | 否 | unused/used/expired |
|
||||
|
||||
---
|
||||
|
||||
## 12. 文件上传接口
|
||||
|
||||
### 12.1 上传图片
|
||||
|
||||
**接口地址**: `POST /upload/image`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
- file: 图片文件(支持jpg、png、gif,最大5MB)
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "上传成功",
|
||||
"data": {
|
||||
"url": "https://cdn.example.com/images/xxx.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**说明**:
|
||||
- 图片自动压缩优化
|
||||
- 自动生成缩略图
|
||||
- 设置正确的Content-Type和Content-Disposition
|
||||
- 返回OSS访问URL
|
||||
|
||||
---
|
||||
|
||||
## 13. 统计接口(管理端)
|
||||
|
||||
### 13.1 获取销售统计
|
||||
|
||||
**接口地址**: `GET /admin/stats/sales`
|
||||
|
||||
**请求头**:
|
||||
```
|
||||
Authorization: Bearer {admin_token}
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| start_date | string | 是 | 开始日期 |
|
||||
| end_date | string | 是 | 结束日期 |
|
||||
|
||||
**响应数据**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"total_amount": 100000.00,
|
||||
"total_orders": 1000,
|
||||
"avg_order_amount": 100.00,
|
||||
"daily_stats": [
|
||||
{
|
||||
"date": "2024-01-01",
|
||||
"amount": 1000.00,
|
||||
"orders": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 10001 | 参数错误 |
|
||||
| 10002 | 用户不存在 |
|
||||
| 10003 | 密码错误 |
|
||||
| 10004 | Token无效 |
|
||||
| 10005 | Token过期 |
|
||||
| 20001 | 商品不存在 |
|
||||
| 20002 | 库存不足 |
|
||||
| 20003 | 商品已下架 |
|
||||
| 30001 | 订单不存在 |
|
||||
| 30002 | 订单状态错误 |
|
||||
| 30003 | 订单已取消 |
|
||||
| 40001 | 支付失败 |
|
||||
| 40002 | 退款失败 |
|
||||
| 50001 | 文件上传失败 |
|
||||
| 50002 | 文件类型不支持 |
|
||||
| 50003 | 文件大小超限 |
|
||||
|
||||
---
|
||||
|
||||
## 15. 开发调试
|
||||
|
||||
### 15.1 本地环境配置
|
||||
|
||||
**配置文件**: `server/configs/config.yaml`
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
mode: debug
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: password
|
||||
database: dianshang
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key
|
||||
expire: 7200
|
||||
|
||||
oss:
|
||||
endpoint: oss-cn-shenzhen.aliyuncs.com
|
||||
access_key_id: your-access-key-id
|
||||
access_key_secret: your-access-key-secret
|
||||
bucket: your-bucket-name
|
||||
```
|
||||
|
||||
### 15.2 API调试工具
|
||||
- Postman
|
||||
- Apifox
|
||||
- Swagger UI (开发中)
|
||||
|
||||
### 15.3 测试账号
|
||||
- **普通用户**: test@example.com / 123456
|
||||
- **管理员**: admin@example.com / admin123
|
||||
|
||||
---
|
||||
|
||||
## 16. 注意事项
|
||||
|
||||
### 16.1 请求限制
|
||||
- 同一IP每分钟最多60次请求
|
||||
- 文件上传大小限制5MB
|
||||
- 批量操作最多100条
|
||||
|
||||
### 16.2 数据安全
|
||||
- 敏感信息传输使用HTTPS
|
||||
- 密码使用bcrypt加密
|
||||
- Token有效期2小时
|
||||
- 软删除数据需显式过滤
|
||||
|
||||
### 16.3 性能优化
|
||||
- 列表接口支持分页
|
||||
- 使用Redis缓存热点数据
|
||||
- 数据库连接池优化
|
||||
- 图片CDN加速
|
||||
|
||||
### 16.4 数据库规范
|
||||
- GORM预加载需显式过滤软删除数据
|
||||
- SKU价格优先级:SKU价格 > 商品价格
|
||||
- 订单数据按用户隔离存储
|
||||
- 所有时间字段使用UTC时间
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**创建日期**: 2024-11-19
|
||||
**最后更新**: 2024-11-19
|
||||
**维护团队**: 后端开发组
|
||||
@@ -1,58 +0,0 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# 复制 go mod 文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S appgroup && \
|
||||
adduser -u 1001 -S appuser -G appgroup
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
# 复制配置文件
|
||||
COPY --from=builder /app/configs ./configs
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p logs && chown -R appuser:appgroup /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
CMD ["./main"]
|
||||
BIN
server/bin/server.exe
Normal file
BIN
server/bin/server.exe
Normal file
Binary file not shown.
@@ -58,8 +58,8 @@ wechatPay:
|
||||
certPath: "certs/apiclient_cert.pem" # 商户证书路径
|
||||
keyPath: "certs/apiclient_key.pem" # 商户私钥路径
|
||||
serialNo: "26DA8C2BC03B796222DA3FCFC6825B236A8C7538" # 证书序列号
|
||||
notifyUrl: "http://192.168.10.109:8081/api/v1/payment/notify" # 支付回调地址
|
||||
refundNotifyUrl: "http://192.168.10.109:8081/api/refunds/callback" # 退款回调地址
|
||||
notifyUrl: "https://tral.cc/api/v1/payment/notify" # 支付回调地址
|
||||
refundNotifyUrl: "https://tral.cc/api/refunds/callback" # 退款回调地址
|
||||
|
||||
upload:
|
||||
maxImageSize: 5242880 # 5MB (5 * 1024 * 1024)
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
# 阿里云OSS配置示例
|
||||
# 将此文件复制为 config.yaml 或 config.prod.yaml,并填入你的实际配置信息
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
mode: release
|
||||
|
||||
database:
|
||||
driver: mysql
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
username: root
|
||||
password: "your-password"
|
||||
dbname: ai_dianshang
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
jwt:
|
||||
secret: "your-jwt-secret-key-change-this-in-production"
|
||||
expire: 7200
|
||||
|
||||
log:
|
||||
level: info
|
||||
filename: logs/app.log
|
||||
maxSize: 100
|
||||
maxAge: 30
|
||||
maxBackups: 5
|
||||
enableConsole: true
|
||||
enableFile: true
|
||||
format: json
|
||||
enableCaller: true
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 1000
|
||||
|
||||
wechat:
|
||||
appId: "your-wechat-appid"
|
||||
appSecret: "your-wechat-appsecret"
|
||||
|
||||
wechatPay:
|
||||
environment: "production"
|
||||
appId: "your-wechat-appid"
|
||||
mchId: "your-merchant-id"
|
||||
apiV3Key: "your-api-v3-key-32-characters"
|
||||
certPath: "certs/apiclient_cert.pem"
|
||||
keyPath: "certs/apiclient_key.pem"
|
||||
serialNo: "your-certificate-serial-number"
|
||||
notifyUrl: "https://yourdomain.com/api/v1/payment/notify"
|
||||
refundNotifyUrl: "https://yourdomain.com/api/refunds/callback"
|
||||
|
||||
# ========== 文件上传配置 ==========
|
||||
upload:
|
||||
maxImageSize: 5242880 # 5MB
|
||||
maxFileSize: 10485760 # 10MB
|
||||
imageTypes: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
|
||||
# 存储类型选择:
|
||||
# - local: 本地存储(默认)
|
||||
# - oss: 阿里云OSS
|
||||
storageType: "oss"
|
||||
|
||||
# 本地存储配置(storageType为local时使用)
|
||||
staticPath: "./static"
|
||||
baseUrl: "https://yourdomain.com"
|
||||
|
||||
# 阿里云OSS配置(storageType为oss时使用)
|
||||
oss:
|
||||
# OSS访问域名(Endpoint)
|
||||
# 根据你的Bucket所在地域选择:
|
||||
# - 华东1(杭州):oss-cn-hangzhou.aliyuncs.com
|
||||
# - 华东2(上海):oss-cn-shanghai.aliyuncs.com
|
||||
# - 华北1(青岛):oss-cn-qingdao.aliyuncs.com
|
||||
# - 华北2(北京):oss-cn-beijing.aliyuncs.com
|
||||
# - 华南1(深圳):oss-cn-shenzhen.aliyuncs.com
|
||||
# 更多地域请参考:https://help.aliyun.com/document_detail/31837.html
|
||||
endpoint: "oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
# AccessKey ID(在阿里云控制台获取)
|
||||
# 建议使用RAM子账号的AccessKey,并授予最小权限
|
||||
accessKeyId: "your-access-key-id"
|
||||
|
||||
# AccessKey Secret(在阿里云控制台获取)
|
||||
accessKeySecret: "your-access-key-secret"
|
||||
|
||||
# Bucket名称(需要提前在OSS控制台创建)
|
||||
bucketName: "your-bucket-name"
|
||||
|
||||
# 文件存储基础路径(可选,建议设置以便文件分类管理)
|
||||
# 例如:dianshang/ 表示所有文件都存储在 dianshang/ 目录下
|
||||
basePath: "dianshang/"
|
||||
|
||||
# 自定义域名(可选)
|
||||
# 如果你配置了CDN加速域名或自定义域名,填写这里
|
||||
# 例如:https://cdn.yourdomain.com
|
||||
# 如果不填写,将使用默认的OSS域名:https://bucket-name.endpoint/
|
||||
domain: ""
|
||||
|
||||
# ========== OSS配置说明 ==========
|
||||
#
|
||||
# 1. 创建OSS Bucket:
|
||||
# - 登录阿里云OSS控制台:https://oss.console.aliyun.com/
|
||||
# - 创建Bucket,选择合适的地域和存储类型
|
||||
# - 设置读写权限为"公共读"(如果需要直接访问)或"私有"(使用签名URL访问)
|
||||
#
|
||||
# 2. 获取AccessKey:
|
||||
# - 访问:https://ram.console.aliyun.com/manage/ak
|
||||
# - 建议创建RAM子账号,只授予OSS相关权限(AliyunOSSFullAccess)
|
||||
# - 获取AccessKey ID和AccessKey Secret
|
||||
#
|
||||
# 3. 配置跨域(CORS):
|
||||
# - 在OSS控制台的Bucket设置中配置CORS规则
|
||||
# - 允许的来源:* 或你的域名
|
||||
# - 允许的方法:GET, POST, PUT, DELETE, HEAD
|
||||
# - 允许的Headers:*
|
||||
#
|
||||
# 4. CDN加速(可选但推荐):
|
||||
# - 在OSS控制台绑定自定义域名
|
||||
# - 开启CDN加速以提升访问速度
|
||||
# - 配置HTTPS证书
|
||||
# - 将domain设置为你的CDN域名
|
||||
#
|
||||
# 5. 切换存储方式:
|
||||
# - 修改 storageType 为 "local" 使用本地存储
|
||||
# - 修改 storageType 为 "oss" 使用阿里云OSS
|
||||
# - 系统支持降级:OSS上传失败时自动切换到本地存储
|
||||
80
server/configs/config.prod-cn.yaml
Normal file
80
server/configs/config.prod-cn.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
# 生产环境配置 - 中国区
|
||||
server:
|
||||
port: 8060
|
||||
mode: release # debug, release, test
|
||||
|
||||
# 数据库配置 - 中国区生产环境
|
||||
database:
|
||||
driver: mysql
|
||||
host: 8.149.233.36
|
||||
port: 3306
|
||||
username: ai_dianshang
|
||||
password: "7aK_H2yvokVumr84lLNDt8fDBp6P"
|
||||
dbname: ai_dianshang
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
autoMigrate: false # 生产环境禁用自动迁移
|
||||
logLevel: silent # 生产环境关闭GORM SQL日志
|
||||
|
||||
# Redis配置 - 中国区生产环境
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# JWT配置 - 中国区生产环境
|
||||
jwt:
|
||||
secret: "prod-cn-jwt-secret-key-change-this"
|
||||
expire: 7200
|
||||
|
||||
# 日志配置 - 中国区生产环境
|
||||
log:
|
||||
level: info # debug, info, warn, error
|
||||
filename: logs/app.prod-cn.log
|
||||
maxSize: 200 # MB
|
||||
maxAge: 30 # 天
|
||||
maxBackups: 10 # 保留文件数
|
||||
enableConsole: true # 启用控制台输出
|
||||
enableFile: true
|
||||
format: text # 使用text格式便于查看
|
||||
enableCaller: true
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 2000 # 生产环境更宽松的性能阈值
|
||||
|
||||
# 微信小程序配置 - 中国区生产环境
|
||||
wechat:
|
||||
appId: "wxccc7018b3bfff234"
|
||||
appSecret: "fa5802a24e7dca8a3cf91ad1e2f288e8"
|
||||
|
||||
# 微信支付配置 - 中国区生产环境
|
||||
wechatPay:
|
||||
environment: "production" # sandbox(沙箱) 或 production(生产)
|
||||
appId: "wxccc7018b3bfff234" # 您的真实微信小程序AppID
|
||||
mchId: "1726717114" # 您的真实微信支付商户号
|
||||
apiV3Key: "M2nB4vCxZ7qW8eKrDtA1jHlP5gF3sN9y" # 您的真实APIv3密钥(32位)
|
||||
certPath: "certs/apiclient_cert.pem" # 商户证书路径
|
||||
keyPath: "certs/apiclient_key.pem" # 商户私钥路径
|
||||
serialNo: "26DA8C2BC03B796222DA3FCFC6825B236A8C7538" # 证书序列号
|
||||
notifyUrl: "https://api-cn.your-domain.com/api/v1/payment/notify" # 中国区支付回调地址
|
||||
refundNotifyUrl: "https://api-cn.your-domain.com/api/refunds/callback" # 中国区退款回调地址
|
||||
|
||||
# 文件上传配置 - 中国区生产环境
|
||||
upload:
|
||||
maxImageSize: 5242880 # 5MB (5 * 1024 * 1024)
|
||||
maxFileSize: 10485760 # 10MB (10 * 1024 * 1024)
|
||||
imageTypes: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
staticPath: "./static"
|
||||
baseUrl: "https://api-cn.your-domain.com"
|
||||
storageType: "oss" # local(本地存储) 或 oss(阿里云OSS)
|
||||
|
||||
# 阿里云OSS配置(中国区)
|
||||
oss:
|
||||
endpoint: "oss-cn-beijing.aliyuncs.com" # 中国北京地域
|
||||
accessKeyId: "LTAI5tNesdhDH4ErqEUZmEg2" # 你的AccessKey ID
|
||||
accessKeySecret: "xZn7WUkTW76TqOLTh01zZATnU6p3Tf" # 你的AccessKey Secret
|
||||
bucketName: "bxmkb-beijing" # 你的Bucket名称
|
||||
basePath: "dianshang/" # 文件存储基础路径
|
||||
domain: "" # 自定义域名(可选,如果有CDN加速域名)
|
||||
80
server/configs/config.prod-eu.yaml
Normal file
80
server/configs/config.prod-eu.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
# 生产环境配置 - 欧洲区
|
||||
server:
|
||||
port: 8060
|
||||
mode: release # debug, release, test
|
||||
|
||||
# 数据库配置 - 欧洲区生产环境
|
||||
database:
|
||||
driver: mysql
|
||||
host: eu-db.your-domain.com # 欧洲区数据库地址
|
||||
port: 3306
|
||||
username: ai_dianshang_eu
|
||||
password: "your-eu-db-password" # 请替换为实际密码
|
||||
dbname: ai_dianshang
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
autoMigrate: false # 生产环境禁用自动迁移
|
||||
logLevel: silent # 生产环境关闭GORM SQL日志
|
||||
|
||||
# Redis配置 - 欧洲区生产环境
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# JWT配置 - 欧洲区生产环境
|
||||
jwt:
|
||||
secret: "prod-eu-jwt-secret-key-change-this"
|
||||
expire: 7200
|
||||
|
||||
# 日志配置 - 欧洲区生产环境
|
||||
log:
|
||||
level: info # debug, info, warn, error
|
||||
filename: logs/app.prod-eu.log
|
||||
maxSize: 200 # MB
|
||||
maxAge: 30 # 天
|
||||
maxBackups: 10 # 保留文件数
|
||||
enableConsole: true # 启用控制台输出
|
||||
enableFile: true
|
||||
format: text # 使用text格式便于查看
|
||||
enableCaller: true
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 2000 # 生产环境更宽松的性能阈值
|
||||
|
||||
# 微信小程序配置 - 欧洲区生产环境
|
||||
wechat:
|
||||
appId: "wx_eu_app_id" # 欧洲区小程序AppID
|
||||
appSecret: "eu_app_secret" # 欧洲区小程序AppSecret
|
||||
|
||||
# 微信支付配置 - 欧洲区生产环境
|
||||
wechatPay:
|
||||
environment: "production"
|
||||
appId: "wx_eu_app_id"
|
||||
mchId: "eu_merchant_id"
|
||||
apiV3Key: "eu_api_v3_key"
|
||||
certPath: "certs/eu_apiclient_cert.pem"
|
||||
keyPath: "certs/eu_apiclient_key.pem"
|
||||
serialNo: "eu_serial_no"
|
||||
notifyUrl: "https://api-eu.your-domain.com/api/v1/payment/notify"
|
||||
refundNotifyUrl: "https://api-eu.your-domain.com/api/refunds/callback"
|
||||
|
||||
# 文件上传配置 - 欧洲区生产环境
|
||||
upload:
|
||||
maxImageSize: 5242880 # 5MB
|
||||
maxFileSize: 10485760 # 10MB
|
||||
imageTypes: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
staticPath: "./static"
|
||||
baseUrl: "https://api-eu.your-domain.com"
|
||||
storageType: "oss"
|
||||
|
||||
# 阿里云OSS配置(欧洲区)
|
||||
oss:
|
||||
endpoint: "oss-eu-central-1.aliyuncs.com" # 欧洲中部地域
|
||||
accessKeyId: "your-eu-access-key-id"
|
||||
accessKeySecret: "your-eu-access-key-secret"
|
||||
bucketName: "your-eu-bucket"
|
||||
basePath: "dianshang/"
|
||||
domain: ""
|
||||
80
server/configs/config.prod-us.yaml
Normal file
80
server/configs/config.prod-us.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
# 生产环境配置 - 美国区
|
||||
server:
|
||||
port: 8060
|
||||
mode: release # debug, release, test
|
||||
|
||||
# 数据库配置 - 美国区生产环境
|
||||
database:
|
||||
driver: mysql
|
||||
host: 104.244.91.212 # 美国区数据库地址
|
||||
port: 3306
|
||||
username: ai_dianshang
|
||||
password: "7aK_H2yvokVumr84lLNDt8fDBp6P" # 请替换为实际密码
|
||||
dbname: ai_dianshang
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
autoMigrate: false # 生产环境禁用自动迁移
|
||||
logLevel: silent # 生产环境关闭GORM SQL日志
|
||||
|
||||
# Redis配置 - 美国区生产环境
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# JWT配置 - 美国区生产环境
|
||||
jwt:
|
||||
secret: "prod-us-jwt-secret-key-change-this"
|
||||
expire: 7200
|
||||
|
||||
# 日志配置 - 美国区生产环境
|
||||
log:
|
||||
level: info # debug, info, warn, error
|
||||
filename: logs/app.prod-us.log
|
||||
maxSize: 200 # MB
|
||||
maxAge: 30 # 天
|
||||
maxBackups: 10 # 保留文件数
|
||||
enableConsole: true # 启用控制台输出
|
||||
enableFile: true
|
||||
format: text # 使用text格式便于查看
|
||||
enableCaller: true
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 2000 # 生产环境更宽松的性能阈值
|
||||
|
||||
# 微信小程序配置 - 生产环境
|
||||
wechat:
|
||||
appId: "wxccc7018b3bfff234"
|
||||
appSecret: "fa5802a24e7dca8a3cf91ad1e2f288e8"
|
||||
|
||||
# 微信支付配置 - 开发环境
|
||||
wechatPay:
|
||||
environment: "production" # sandbox(沙箱) 或 production(生产)
|
||||
appId: "wxccc7018b3bfff234" # 您的真实微信小程序AppID
|
||||
mchId: "1726717114" # 您的真实微信支付商户号
|
||||
apiV3Key: "M2nB4vCxZ7qW8eKrDtA1jHlP5gF3sN9y" # 您的真实APIv3密钥(32位)
|
||||
certPath: "certs/apiclient_cert.pem" # 商户证书路径
|
||||
keyPath: "certs/apiclient_key.pem" # 商户私钥路径
|
||||
serialNo: "26DA8C2BC03B796222DA3FCFC6825B236A8C7538" # 证书序列号
|
||||
notifyUrl: "https://vizee.shop/api/v1/payment/notify" # 支付回调地址
|
||||
refundNotifyUrl: "https://vizee.shop/api/refunds/callback" # 退款回调地址
|
||||
|
||||
# 文件上传配置 - 生产环境
|
||||
upload:
|
||||
maxImageSize: 5242880 # 5MB (5 * 1024 * 1024)
|
||||
maxFileSize: 10485760 # 10MB (10 * 1024 * 1024)
|
||||
imageTypes: [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
|
||||
staticPath: "./static"
|
||||
baseUrl: "http://vizee.shop"
|
||||
storageType: "oss" # local(本地存储) 或 oss(阿里云OSS)
|
||||
|
||||
# 阿里云OSS配置(当storageType为oss时生效)
|
||||
oss:
|
||||
endpoint: "oss-cn-beijing.aliyuncs.com" # OSS访问域名,根据你的地域修改
|
||||
accessKeyId: "LTAI5tNesdhDH4ErqEUZmEg2" # 你的AccessKey ID
|
||||
accessKeySecret: "xZn7WUkTW76TqOLTh01zZATnU6p3Tf" # 你的AccessKey Secret
|
||||
bucketName: "bxmkb-beijing" # 你的Bucket名称
|
||||
basePath: "dianshang/" # 文件存储基础路径
|
||||
domain: "" # 自定义域名(可选,如果有CDN加速域名)
|
||||
@@ -1,65 +0,0 @@
|
||||
# 生产环境微信支付配置示例
|
||||
# 请复制此文件为 config.prod.yaml 并填入真实参数
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
mode: release
|
||||
|
||||
# 数据库配置 - 生产环境
|
||||
database:
|
||||
driver: mysql
|
||||
host: your_db_host
|
||||
port: 3306
|
||||
username: your_db_user
|
||||
password: "your_db_password"
|
||||
dbname: ai_dianshang
|
||||
charset: utf8mb4
|
||||
parseTime: true
|
||||
loc: Local
|
||||
|
||||
# Redis配置 - 生产环境
|
||||
redis:
|
||||
host: your_redis_host
|
||||
port: 6379
|
||||
password: "your_redis_password"
|
||||
db: 0
|
||||
|
||||
# JWT配置 - 生产环境
|
||||
jwt:
|
||||
secret: "your-production-jwt-secret-key-must-be-complex"
|
||||
expire: 7200
|
||||
|
||||
# 日志配置 - 生产环境
|
||||
log:
|
||||
level: info
|
||||
filename: logs/app.log
|
||||
maxSize: 100
|
||||
maxAge: 30
|
||||
maxBackups: 10
|
||||
enableConsole: true
|
||||
enableFile: true
|
||||
format: json
|
||||
enableCaller: false
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 1000
|
||||
|
||||
# 微信小程序配置 - 生产环境
|
||||
wechat:
|
||||
appId: "wx1234567890abcdef" # 替换为您的真实AppID
|
||||
appSecret: "your_real_app_secret" # 替换为您的真实AppSecret
|
||||
|
||||
# 微信支付配置 - 生产环境
|
||||
wechatPay:
|
||||
appId: "wx1234567890abcdef" # 您的真实微信小程序AppID
|
||||
mchId: "1600000000" # 您的真实微信支付商户号
|
||||
apiKey: "your_real_32_character_api_v3_key_here" # 您的真实APIv3密钥(32位)
|
||||
certPath: "certs/apiclient_cert.pem" # 商户证书路径
|
||||
keyPath: "certs/apiclient_key.pem" # 商户私钥路径
|
||||
notifyUrl: "https://yourdomain.com/api/v1/payment/notify" # 您的真实支付回调URL(必须HTTPS)
|
||||
|
||||
# 重要提醒:
|
||||
# 1. 所有以 "your_" 开头的值都需要替换为真实值
|
||||
# 2. 证书文件需要从微信支付商户平台下载
|
||||
# 3. 回调URL必须是可公网访问的HTTPS地址
|
||||
# 4. 商户号需要通过微信支付商户资质审核获得
|
||||
@@ -36,10 +36,10 @@ log:
|
||||
maxSize: 200 # MB
|
||||
maxAge: 30 # 天
|
||||
maxBackups: 10 # 保留文件数
|
||||
enableConsole: true # 生产环境不输出到控制台
|
||||
enableConsole: true # 启用控制台输出
|
||||
enableFile: true
|
||||
format: json
|
||||
enableCaller: true # 生产环境关闭调用者信息
|
||||
format: text # 使用text格式便于查看
|
||||
enableCaller: true
|
||||
enableOperation: true
|
||||
enablePerf: true
|
||||
perfThreshold: 2000 # 生产环境更宽松的性能阈值
|
||||
|
||||
@@ -187,6 +187,15 @@ func getConfigName(env string) string {
|
||||
return "config.test"
|
||||
case "production", "prod":
|
||||
return "config.prod"
|
||||
// 生产环境 - 中国区
|
||||
case "prod-cn", "production-cn":
|
||||
return "config.prod-cn"
|
||||
// 生产环境 - 美国区
|
||||
case "prod-us", "production-us":
|
||||
return "config.prod-us"
|
||||
// 生产环境 - 欧洲区
|
||||
case "prod-eu", "production-eu":
|
||||
return "config.prod-eu"
|
||||
default:
|
||||
// 如果环境不匹配,尝试使用默认配置文件
|
||||
if _, err := os.Stat("./configs/config.yaml"); err == nil {
|
||||
|
||||
466
server/internal/handler/admin_coupon.go
Normal file
466
server/internal/handler/admin_coupon.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"dianshang/internal/service"
|
||||
"dianshang/pkg/logger"
|
||||
"dianshang/pkg/response"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AdminCouponHandler struct {
|
||||
couponService *service.CouponService
|
||||
}
|
||||
|
||||
func NewAdminCouponHandler(couponService *service.CouponService) *AdminCouponHandler {
|
||||
return &AdminCouponHandler{
|
||||
couponService: couponService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCouponList 获取优惠券列表
|
||||
// @Summary 获取优惠券列表
|
||||
// @Description 管理员获取所有优惠券(分页)
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Param status query int false "状态筛选(1-启用 0-禁用)"
|
||||
// @Param type query int false "类型筛选(1-满减 2-折扣 3-免邮)"
|
||||
// @Param keyword query string false "搜索关键词"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons [get]
|
||||
func (h *AdminCouponHandler) GetCouponList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
status := c.Query("status")
|
||||
couponType := c.Query("type")
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
coupons, total, err := h.couponService.GetCouponListForAdmin(page, pageSize, status, couponType, keyword)
|
||||
if err != nil {
|
||||
logger.Error("获取优惠券列表失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取优惠券列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Page(c, coupons, total, page, pageSize)
|
||||
}
|
||||
|
||||
// GetCouponDetail 获取优惠券详情
|
||||
// @Summary 获取优惠券详情
|
||||
// @Description 获取指定优惠券的详细信息
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "优惠券ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/{id} [get]
|
||||
func (h *AdminCouponHandler) GetCouponDetail(c *gin.Context) {
|
||||
couponIDStr := c.Param("id")
|
||||
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
|
||||
return
|
||||
}
|
||||
|
||||
coupon, err := h.couponService.GetCouponDetailForAdmin(uint(couponID))
|
||||
if err != nil {
|
||||
logger.Error("获取优惠券详情失败", "error", err, "couponID", couponID)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, coupon)
|
||||
}
|
||||
|
||||
// CreateCoupon 创建优惠券
|
||||
// @Summary 创建优惠券
|
||||
// @Description 创建新的优惠券
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CreateCouponRequest true "创建优惠券请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons [post]
|
||||
func (h *AdminCouponHandler) CreateCoupon(c *gin.Context) {
|
||||
var req CreateCouponRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("绑定创建优惠券参数失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管理员ID
|
||||
adminID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, response.ERROR_UNAUTHORIZED)
|
||||
return
|
||||
}
|
||||
|
||||
coupon, err := h.couponService.CreateCoupon(&req, adminID.(uint))
|
||||
if err != nil {
|
||||
logger.Error("创建优惠券失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("优惠券创建成功", "couponID", coupon.ID, "name", coupon.Name, "adminID", adminID)
|
||||
response.Success(c, coupon)
|
||||
}
|
||||
|
||||
// UpdateCoupon 更新优惠券
|
||||
// @Summary 更新优惠券
|
||||
// @Description 更新优惠券信息
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "优惠券ID"
|
||||
// @Param request body UpdateCouponRequest true "更新优惠券请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/{id} [put]
|
||||
func (h *AdminCouponHandler) UpdateCoupon(c *gin.Context) {
|
||||
couponIDStr := c.Param("id")
|
||||
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCouponRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("绑定更新优惠券参数失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.couponService.UpdateCoupon(uint(couponID), &req)
|
||||
if err != nil {
|
||||
logger.Error("更新优惠券失败", "error", err, "couponID", couponID)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("优惠券更新成功", "couponID", couponID)
|
||||
response.Success(c, "更新成功")
|
||||
}
|
||||
|
||||
// DeleteCoupon 删除优惠券
|
||||
// @Summary 删除优惠券
|
||||
// @Description 删除指定优惠券
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "优惠券ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/{id} [delete]
|
||||
func (h *AdminCouponHandler) DeleteCoupon(c *gin.Context) {
|
||||
couponIDStr := c.Param("id")
|
||||
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.couponService.DeleteCoupon(uint(couponID))
|
||||
if err != nil {
|
||||
logger.Error("删除优惠券失败", "error", err, "couponID", couponID)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("优惠券删除成功", "couponID", couponID)
|
||||
response.Success(c, "删除成功")
|
||||
}
|
||||
|
||||
// UpdateCouponStatus 更新优惠券状态
|
||||
// @Summary 更新优惠券状态
|
||||
// @Description 启用或禁用优惠券
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "优惠券ID"
|
||||
// @Param request body UpdateStatusRequest true "状态更新请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/{id}/status [put]
|
||||
func (h *AdminCouponHandler) UpdateCouponStatus(c *gin.Context) {
|
||||
couponIDStr := c.Param("id")
|
||||
couponID, err := strconv.ParseUint(couponIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的优惠券ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("绑定状态更新参数失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = h.couponService.UpdateCouponStatus(uint(couponID), req.Status)
|
||||
if err != nil {
|
||||
logger.Error("更新优惠券状态失败", "error", err, "couponID", couponID)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("优惠券状态更新成功", "couponID", couponID, "status", req.Status)
|
||||
response.Success(c, "状态更新成功")
|
||||
}
|
||||
|
||||
// BatchDeleteCoupons 批量删除优惠券
|
||||
// @Summary 批量删除优惠券
|
||||
// @Description 批量删除多个优惠券
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body BatchDeleteRequest true "批量删除请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/batch [delete]
|
||||
func (h *AdminCouponHandler) BatchDeleteCoupons(c *gin.Context) {
|
||||
var req BatchDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("绑定批量删除参数失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "请选择要删除的优惠券")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.couponService.BatchDeleteCoupons(req.IDs)
|
||||
if err != nil {
|
||||
logger.Error("批量删除优惠券失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("批量删除优惠券成功", "count", len(req.IDs))
|
||||
response.Success(c, "批量删除成功")
|
||||
}
|
||||
|
||||
// GetCouponStatistics 获取优惠券统计
|
||||
// @Summary 获取优惠券统计
|
||||
// @Description 获取优惠券使用统计数据
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param start_date query string false "开始日期"
|
||||
// @Param end_date query string false "结束日期"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/statistics [get]
|
||||
func (h *AdminCouponHandler) GetCouponStatistics(c *gin.Context) {
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
// 如果没有提供日期,默认查询最近30天
|
||||
if startDate == "" || endDate == "" {
|
||||
now := time.Now()
|
||||
endDate = now.Format("2006-01-02")
|
||||
startDate = now.AddDate(0, 0, -30).Format("2006-01-02")
|
||||
}
|
||||
|
||||
// 解析日期
|
||||
startTime, err := time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "开始日期格式错误")
|
||||
return
|
||||
}
|
||||
endTime, err := time.Parse("2006-01-02", endDate)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "结束日期格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
endTime = endTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
|
||||
stats, err := h.couponService.GetCouponStatistics(startTime, endTime)
|
||||
if err != nil {
|
||||
logger.Error("获取优惠券统计失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取优惠券统计失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, stats)
|
||||
}
|
||||
|
||||
// GetUserCouponList 获取用户优惠券列表
|
||||
// @Summary 获取用户优惠券列表
|
||||
// @Description 查看指定用户的优惠券领取记录
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id query int true "用户ID"
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/user-coupons [get]
|
||||
func (h *AdminCouponHandler) GetUserCouponList(c *gin.Context) {
|
||||
userIDStr := c.Query("user_id")
|
||||
if userIDStr == "" {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的用户ID")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
coupons, total, err := h.couponService.GetUserCouponListForAdmin(uint(userID), page, pageSize)
|
||||
if err != nil {
|
||||
logger.Error("获取用户优惠券列表失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取用户优惠券列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Page(c, coupons, total, page, pageSize)
|
||||
}
|
||||
|
||||
// DistributeCoupon 发放优惠券
|
||||
// @Summary 发放优惠券
|
||||
// @Description 给用户发放优惠券(单个/批量/全员)
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body DistributeCouponRequest true "发放请求"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/distribute [post]
|
||||
func (h *AdminCouponHandler) DistributeCoupon(c *gin.Context) {
|
||||
var req DistributeCouponRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error("绑定发放优惠券参数失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if !req.DistributeAll && len(req.UserIDs) == 0 {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "请选择用户或全员发放")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管理员ID
|
||||
adminID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Error(c, response.ERROR_UNAUTHORIZED)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层发放优惠券
|
||||
result, err := h.couponService.DistributeCoupon(req.CouponID, req.UserIDs, req.DistributeAll, req.Quantity, adminID.(uint))
|
||||
if err != nil {
|
||||
logger.Error("发放优惠券失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("优惠券发放成功", "couponID", req.CouponID, "totalCount", result["total_count"], "successCount", result["success_count"], "adminID", adminID)
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// GetDistributeHistory 获取发放历史
|
||||
// @Summary 获取发放历史
|
||||
// @Description 获取优惠券发放记录
|
||||
// @Tags 管理员-优惠券管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(10)
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.Response
|
||||
// @Router /admin/coupons/distribute/history [get]
|
||||
func (h *AdminCouponHandler) GetDistributeHistory(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
history, total, err := h.couponService.GetDistributeHistory(page, pageSize)
|
||||
if err != nil {
|
||||
logger.Error("获取发放历史失败", "error", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取发放历史失败")
|
||||
return
|
||||
}
|
||||
|
||||
response.Page(c, history, total, page, pageSize)
|
||||
}
|
||||
|
||||
// 请求结构体
|
||||
type CreateCouponRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Type int `json:"type" binding:"required,oneof=1 2 3"`
|
||||
Value int64 `json:"value" binding:"required,min=1"`
|
||||
MinAmount int64 `json:"min_amount" binding:"min=0"`
|
||||
Description string `json:"description" binding:"max=500"`
|
||||
StartTime time.Time `json:"start_time" binding:"required"`
|
||||
EndTime time.Time `json:"end_time" binding:"required"`
|
||||
TotalCount int `json:"total_count" binding:"min=0"`
|
||||
Status int `json:"status" binding:"oneof=0 1"`
|
||||
}
|
||||
|
||||
type UpdateCouponRequest struct {
|
||||
Name string `json:"name" binding:"max=100"`
|
||||
Type int `json:"type" binding:"oneof=1 2 3"`
|
||||
Value int64 `json:"value" binding:"min=1"`
|
||||
MinAmount int64 `json:"min_amount" binding:"min=0"`
|
||||
Description string `json:"description" binding:"max=500"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
TotalCount int `json:"total_count" binding:"min=0"`
|
||||
Status int `json:"status" binding:"oneof=0 1"`
|
||||
}
|
||||
|
||||
type UpdateStatusRequest struct {
|
||||
Status int `json:"status" binding:"required,oneof=0 1"`
|
||||
}
|
||||
|
||||
type BatchDeleteRequest struct {
|
||||
IDs []uint `json:"ids" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type DistributeCouponRequest struct {
|
||||
CouponID uint `json:"coupon_id" binding:"required"`
|
||||
UserIDs []uint `json:"user_ids"`
|
||||
DistributeAll bool `json:"distribute_all"`
|
||||
Quantity int `json:"quantity" binding:"required,min=1,max=100"`
|
||||
}
|
||||
@@ -109,6 +109,25 @@ func (h *AdminProductHandler) UpdateProduct(c *gin.Context) {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 category_id 字段:转换为 JSONUintSlice 类型
|
||||
if categoryIDRaw, ok := updates["category_id"]; ok {
|
||||
switch v := categoryIDRaw.(type) {
|
||||
case []interface{}:
|
||||
var categoryIDs []uint
|
||||
for _, item := range v {
|
||||
switch id := item.(type) {
|
||||
case float64:
|
||||
categoryIDs = append(categoryIDs, uint(id))
|
||||
case int:
|
||||
categoryIDs = append(categoryIDs, uint(id))
|
||||
}
|
||||
}
|
||||
updates["category_id"] = model.JSONUintSlice(categoryIDs)
|
||||
case []uint:
|
||||
updates["category_id"] = model.JSONUintSlice(v)
|
||||
}
|
||||
}
|
||||
|
||||
// 商品更新时间会自动设置
|
||||
|
||||
@@ -212,6 +231,7 @@ func (h *AdminProductHandler) CreateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建分类
|
||||
if err := h.productService.CreateCategory(&category); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
@@ -235,6 +255,40 @@ func (h *AdminProductHandler) UpdateCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 platform 字段:转换为 JSONSlice 类型
|
||||
if platformRaw, ok := updates["platform"]; ok {
|
||||
switch v := platformRaw.(type) {
|
||||
case []interface{}:
|
||||
// 前端传来的是数组
|
||||
var platforms []string
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
platforms = append(platforms, str)
|
||||
}
|
||||
}
|
||||
updates["platform"] = model.JSONSlice(platforms)
|
||||
case string:
|
||||
// 前端传来的是单个字符串,转为数组
|
||||
updates["platform"] = model.JSONSlice([]string{v})
|
||||
default:
|
||||
// 其他类型,删除该字段
|
||||
delete(updates, "platform")
|
||||
}
|
||||
}
|
||||
|
||||
// 删除只读字段,避免更新时出错
|
||||
readonlyFields := []string{"id", "created_at", "updated_at", "children", "hasChildren", "level"}
|
||||
for _, field := range readonlyFields {
|
||||
delete(updates, field)
|
||||
}
|
||||
|
||||
// 删除不存在的字段
|
||||
nonExistFields := []string{"is_show", "keywords"}
|
||||
for _, field := range nonExistFields {
|
||||
delete(updates, field)
|
||||
}
|
||||
|
||||
// 更新分类基本信息
|
||||
if err := h.productService.UpdateCategory(uint(id), updates); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
|
||||
@@ -36,9 +36,16 @@ func (h *CartHandler) GetCart(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算购物车统计信息
|
||||
count, _ := h.cartService.GetCartCount(userID.(uint))
|
||||
total, _ := h.cartService.GetCartTotal(userID.(uint))
|
||||
// 优化: 在一次查询结果基础上计算统计信息,避免重复查询
|
||||
var count int
|
||||
var total float64
|
||||
for _, item := range cart {
|
||||
count += item.Quantity
|
||||
if item.Product.ID != 0 {
|
||||
// 将价格从分转换为元
|
||||
total += (float64(item.Product.Price) / 100) * float64(item.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"items": cart,
|
||||
|
||||
@@ -137,6 +137,22 @@ func (h *CommentHandler) GetCommentStats(c *gin.Context) {
|
||||
response.Success(c, stats)
|
||||
}
|
||||
|
||||
// GetHighRatingComments 获取高分评论(用于首页展示)
|
||||
func (h *CommentHandler) GetHighRatingComments(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "6"))
|
||||
|
||||
comments, err := h.commentService.GetHighRatingComments(limit)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
result := h.convertToResponseList(comments)
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// GetCommentDetail 获取评论详情
|
||||
func (h *CommentHandler) GetCommentDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
@@ -57,14 +57,14 @@ func (h *FrontendHandler) GetHomeData(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取推荐商品
|
||||
recommendProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, "default", "desc")
|
||||
recommendProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, nil, "default", "desc")
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取推荐商品失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取热门商品
|
||||
hotProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, "sales", "desc")
|
||||
hotProducts, _, err := h.productService.GetProductList(1, 10, 0, "", 0, 0, nil, "sales", "desc")
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, "获取热门商品失败: "+err.Error())
|
||||
return
|
||||
@@ -91,7 +91,7 @@ func (h *FrontendHandler) GetProductsRecommend(c *gin.Context) {
|
||||
page := utils.StringToInt(c.DefaultQuery("page", "1"))
|
||||
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, "default", "desc")
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, nil, "default", "desc")
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
@@ -106,7 +106,7 @@ func (h *FrontendHandler) GetProductsHot(c *gin.Context) {
|
||||
page := utils.StringToInt(c.DefaultQuery("page", "1"))
|
||||
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "10"))
|
||||
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, "sales", "desc")
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, 0, "", 0, 0, nil, "sales", "desc")
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
@@ -474,7 +474,13 @@ func (h *FrontendHandler) convertProductToFrontend(product *model.Product) model
|
||||
SpuStockQuantity: product.Stock,
|
||||
SoldNum: product.Sales,
|
||||
IsPutOnSale: 1,
|
||||
CategoryIds: []string{strconv.Itoa(int(product.CategoryID))},
|
||||
CategoryIds: func() []string {
|
||||
ids := make([]string, len(product.CategoryID))
|
||||
for i, id := range product.CategoryID {
|
||||
ids[i] = strconv.Itoa(int(id))
|
||||
}
|
||||
return ids
|
||||
}(),
|
||||
SpecList: specList,
|
||||
SkuList: skuList,
|
||||
SpuTagList: spuTagList,
|
||||
|
||||
243
server/internal/handler/livestream.go
Normal file
243
server/internal/handler/livestream.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"dianshang/internal/service"
|
||||
"dianshang/pkg/response"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LiveStreamHandler struct {
|
||||
liveStreamService service.LiveStreamService
|
||||
}
|
||||
|
||||
func NewLiveStreamHandler(liveStreamService service.LiveStreamService) *LiveStreamHandler {
|
||||
return &LiveStreamHandler{
|
||||
liveStreamService: liveStreamService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLiveStreamList 获取投流源列表
|
||||
func (h *LiveStreamHandler) GetLiveStreamList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
title := c.Query("title")
|
||||
platform := c.Query("platform")
|
||||
|
||||
var status *int
|
||||
if statusStr := c.Query("status"); statusStr != "" {
|
||||
statusVal, err := strconv.Atoi(statusStr)
|
||||
if err == nil {
|
||||
status = &statusVal
|
||||
}
|
||||
}
|
||||
|
||||
streams, total, err := h.liveStreamService.GetLiveStreamList(page, pageSize, title, platform, status)
|
||||
if err != nil {
|
||||
response.Error(c, response.ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"list": streams,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLiveStreamDetail 获取投流源详情
|
||||
func (h *LiveStreamHandler) GetLiveStreamDetail(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := h.liveStreamService.GetLiveStreamByID(uint(id))
|
||||
if err != nil {
|
||||
response.Error(c, response.ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, stream)
|
||||
}
|
||||
|
||||
// GetActiveLiveStreams 获取启用的投流源(前台接口)
|
||||
func (h *LiveStreamHandler) GetActiveLiveStreams(c *gin.Context) {
|
||||
streams, err := h.liveStreamService.GetActiveLiveStreams()
|
||||
if err != nil {
|
||||
response.Error(c, response.ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, streams)
|
||||
}
|
||||
|
||||
// CreateLiveStream 创建投流源
|
||||
func (h *LiveStreamHandler) CreateLiveStream(c *gin.Context) {
|
||||
var req struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
StreamURL string `json:"stream_url" binding:"required"`
|
||||
CoverImage string `json:"cover_image"`
|
||||
Description string `json:"description"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
StartTime *time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stream := &model.LiveStream{
|
||||
Title: req.Title,
|
||||
Platform: req.Platform,
|
||||
StreamURL: req.StreamURL,
|
||||
CoverImage: req.CoverImage,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
Sort: req.Sort,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.CreateLiveStream(stream); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, stream)
|
||||
}
|
||||
|
||||
// UpdateLiveStream 更新投流源
|
||||
func (h *LiveStreamHandler) UpdateLiveStream(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Platform string `json:"platform"`
|
||||
StreamURL string `json:"stream_url"`
|
||||
CoverImage string `json:"cover_image"`
|
||||
Description string `json:"description"`
|
||||
Status int `json:"status"`
|
||||
Sort int `json:"sort"`
|
||||
StartTime *time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
stream := &model.LiveStream{
|
||||
Title: req.Title,
|
||||
Platform: req.Platform,
|
||||
StreamURL: req.StreamURL,
|
||||
CoverImage: req.CoverImage,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
Sort: req.Sort,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.UpdateLiveStream(uint(id), stream); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, stream)
|
||||
}
|
||||
|
||||
// UpdateLiveStreamStatus 更新投流源状态
|
||||
func (h *LiveStreamHandler) UpdateLiveStreamStatus(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status int `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.UpdateLiveStreamStatus(uint(id), req.Status); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// DeleteLiveStream 删除投流源
|
||||
func (h *LiveStreamHandler) DeleteLiveStream(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.DeleteLiveStream(uint(id)); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// BatchDeleteLiveStreams 批量删除投流源
|
||||
func (h *LiveStreamHandler) BatchDeleteLiveStreams(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.BatchDeleteLiveStreams(req.IDs); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加观看次数
|
||||
func (h *LiveStreamHandler) IncrementViewCount(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "投流源ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.liveStreamService.IncrementViewCount(uint(id)); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, nil)
|
||||
}
|
||||
@@ -11,19 +11,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OrderHandler 璁㈠崟澶勭悊鍣?
|
||||
// OrderHandler 订单处理器
|
||||
type OrderHandler struct {
|
||||
orderService *service.OrderService
|
||||
orderService *service.OrderService
|
||||
wechatPayService *service.WeChatPayService
|
||||
}
|
||||
|
||||
// NewOrderHandler 鍒涘缓璁㈠崟澶勭悊鍣?
|
||||
func NewOrderHandler(orderService *service.OrderService) *OrderHandler {
|
||||
// NewOrderHandler 创建订单处理器
|
||||
func NewOrderHandler(orderService *service.OrderService, wechatPayService *service.WeChatPayService) *OrderHandler {
|
||||
return &OrderHandler{
|
||||
orderService: orderService,
|
||||
orderService: orderService,
|
||||
wechatPayService: wechatPayService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOrder 鍒涘缓璁㈠崟
|
||||
// CreateOrder 创建订单
|
||||
func (h *OrderHandler) CreateOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -50,7 +52,7 @@ func (h *OrderHandler) CreateOrder(c *gin.Context) {
|
||||
response.Success(c, order)
|
||||
}
|
||||
|
||||
// GetUserOrders 鑾峰彇鐢ㄦ埛璁㈠崟鍒楄〃
|
||||
// GetUserOrders 获取用户订单列表
|
||||
func (h *OrderHandler) GetUserOrders(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -422,7 +424,7 @@ func (h *OrderHandler) formatOrderDetail(order *model.Order) *OrderDetailRespons
|
||||
}
|
||||
}
|
||||
|
||||
// PayOrder 鏀粯璁㈠崟
|
||||
// PayOrder 支付订单
|
||||
func (h *OrderHandler) PayOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -431,13 +433,66 @@ func (h *OrderHandler) PayOrder(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 从URL路径参数获取订单号
|
||||
orderID := c.Param("id")
|
||||
if orderID == "" {
|
||||
orderNo := c.Param("id")
|
||||
if orderNo == "" {
|
||||
response.BadRequest(c, "订单号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.orderService.PayOrder(userID.(uint), orderID); err != nil {
|
||||
// 解析请求体获取支付方式
|
||||
var req struct {
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// 如果没有提供支付方式,默认使用微信支付
|
||||
req.PaymentMethod = "wechat"
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
order, err := h.orderService.GetOrderByOrderNo(orderNo)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单归属
|
||||
if order.UserID != userID.(uint) {
|
||||
response.ErrorWithMessage(c, response.ERROR, "无权限操作此订单")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单状态
|
||||
if order.Status != 1 { // 1 = 待付款
|
||||
response.ErrorWithMessage(c, response.ERROR, "订单状态不允许支付")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是微信支付,返回支付二维码
|
||||
if req.PaymentMethod == "wechat" {
|
||||
// 调用微信Native扫码支付
|
||||
if h.wechatPayService != nil {
|
||||
paymentResp, err := h.wechatPayService.CreateNativeOrder(c.Request.Context(), order)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, paymentResp.Data)
|
||||
return
|
||||
} else {
|
||||
// 如果没有微信支付服务,返回模拟数据
|
||||
response.Success(c, gin.H{
|
||||
"qrcode_url": "https://api.example.com/qrcode/" + orderNo,
|
||||
"order_no": orderNo,
|
||||
"amount": order.TotalAmount,
|
||||
"sandbox": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 其他支付方式,直接标记为已支付
|
||||
if err := h.orderService.PayOrder(userID.(uint), orderNo); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -445,7 +500,7 @@ func (h *OrderHandler) PayOrder(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// CancelOrder 鍙栨秷璁㈠崟
|
||||
// CancelOrder 取消订单
|
||||
func (h *OrderHandler) CancelOrder(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -534,7 +589,7 @@ func (h *OrderHandler) RefundOrder(c *gin.Context) {
|
||||
response.Success(c, gin.H{"message": "退款申请已提交"})
|
||||
}
|
||||
|
||||
// ConfirmReceive 纭鏀惰揣纭鏀惰揣
|
||||
// ConfirmReceive 确认收货
|
||||
func (h *OrderHandler) ConfirmReceive(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -558,7 +613,7 @@ func (h *OrderHandler) ConfirmReceive(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetOrderList 鑾峰彇璁㈠崟鍒楄〃锛堢鐞嗗憳锟?
|
||||
// GetOrderList 获取订单列表(支持条件查询)
|
||||
func (h *OrderHandler) GetOrderList(c *gin.Context) {
|
||||
page := utils.StringToInt(c.DefaultQuery("page", "1"))
|
||||
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "20"))
|
||||
@@ -589,7 +644,7 @@ func (h *OrderHandler) GetOrderList(c *gin.Context) {
|
||||
response.Page(c, orders, pagination.Total, pagination.Page, pagination.PageSize)
|
||||
}
|
||||
|
||||
// ShipOrder 鍙戣揣锛堢鐞嗗憳锟?
|
||||
// ShipOrder 发货
|
||||
func (h *OrderHandler) ShipOrder(c *gin.Context) {
|
||||
var req struct {
|
||||
OrderNo string `json:"order_no" binding:"required"`
|
||||
@@ -609,7 +664,7 @@ func (h *OrderHandler) ShipOrder(c *gin.Context) {
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
// GetOrderStatistics 鑾峰彇璁㈠崟缁熻
|
||||
// GetOrderStatistics 获取订单统计
|
||||
func (h *OrderHandler) GetOrderStatistics(c *gin.Context) {
|
||||
statistics, err := h.orderService.GetOrderStatistics()
|
||||
if err != nil {
|
||||
@@ -620,7 +675,7 @@ func (h *OrderHandler) GetOrderStatistics(c *gin.Context) {
|
||||
response.Success(c, statistics)
|
||||
}
|
||||
|
||||
// GetDailyOrderStatistics 鑾峰彇姣忔棩璁㈠崟缁熻
|
||||
// GetDailyOrderStatistics 获取每日订单统计
|
||||
func (h *OrderHandler) GetDailyOrderStatistics(c *gin.Context) {
|
||||
days := utils.StringToInt(c.DefaultQuery("days", "30"))
|
||||
|
||||
|
||||
51
server/internal/handler/order_payment_status.go
Normal file
51
server/internal/handler/order_payment_status.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"dianshang/pkg/response"
|
||||
)
|
||||
|
||||
// GetPaymentStatus 获取订单支付状态
|
||||
func (h *OrderHandler) GetPaymentStatus(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
response.Unauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 从URL路径参数获取订单号
|
||||
orderNo := c.Param("id")
|
||||
if orderNo == "" {
|
||||
response.BadRequest(c, "订单号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
order, err := h.orderService.GetOrderByOrderNo(orderNo)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, "订单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单归属
|
||||
if order.UserID != userID.(uint) {
|
||||
response.ErrorWithMessage(c, response.ERROR, "无权限操作此订单")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回支付状态
|
||||
status := "unpaid"
|
||||
if order.Status == 2 || order.Status == 3 || order.Status == 4 || order.Status == 5 || order.Status == 6 {
|
||||
status = "paid"
|
||||
} else if order.Status == 7 {
|
||||
status = "canceled"
|
||||
} else if order.Status == 9 {
|
||||
status = "refunded"
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"status": status,
|
||||
"order_no": order.OrderNo,
|
||||
"order_status": order.Status,
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"dianshang/internal/service"
|
||||
"dianshang/pkg/response"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -169,37 +170,51 @@ func (h *PaymentHandler) CancelPayment(c *gin.Context) {
|
||||
|
||||
// PaymentNotify 支付回调通知
|
||||
func (h *PaymentHandler) PaymentNotify(c *gin.Context) {
|
||||
log.Printf("[=== 微信支付回调 ===] 收到回调请求")
|
||||
log.Printf("[回调请求] 请求方法: %s", c.Request.Method)
|
||||
log.Printf("[回调请求] 请求路径: %s", c.Request.URL.Path)
|
||||
log.Printf("[回调请求] 客户端IP: %s", c.ClientIP())
|
||||
|
||||
// 读取回调数据
|
||||
body, err := c.GetRawData()
|
||||
if err != nil {
|
||||
log.Printf("[回调错误] 读取回调数据失败: %v", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "读取回调数据失败")
|
||||
return
|
||||
}
|
||||
log.Printf("[回调数据] 数据长度: %d bytes", len(body))
|
||||
|
||||
// 获取请求头
|
||||
headers := make(map[string]string)
|
||||
for key, values := range c.Request.Header {
|
||||
if len(values) > 0 {
|
||||
headers[key] = values[0]
|
||||
log.Printf("[回调请求头] %s: %s", key, values[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 处理微信支付回调
|
||||
log.Printf("[回调处理] 开始验证签名并解析数据...")
|
||||
notify, err := h.wechatPayService.HandleNotify(c.Request.Context(), body, headers)
|
||||
if err != nil {
|
||||
log.Printf("[回调错误] 处理支付回调失败: %v", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "处理支付回调失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("[回调数据] 事件类型: %s", notify.EventType)
|
||||
|
||||
// 根据回调类型处理
|
||||
if notify.EventType == "TRANSACTION.SUCCESS" {
|
||||
log.Printf("[支付成功] 开始处理支付成功回调...")
|
||||
// 支付成功,更新订单状态
|
||||
err = h.wechatPayService.ProcessPaymentSuccess(c.Request.Context(), notify)
|
||||
if err != nil {
|
||||
log.Printf("[回调错误] 处理支付成功回调失败: %v", err)
|
||||
response.ErrorWithMessage(c, response.ERROR, "处理支付成功回调失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[支付成功] 订单状态更新成功")
|
||||
response.Success(c, gin.H{
|
||||
"code": "SUCCESS",
|
||||
"message": "处理成功",
|
||||
@@ -207,6 +222,7 @@ func (h *PaymentHandler) PaymentNotify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[回调处理] 非支付成功事件,仅记录")
|
||||
response.Success(c, gin.H{
|
||||
"code": "SUCCESS",
|
||||
"message": "回调已接收",
|
||||
|
||||
139
server/internal/handler/platform.go
Normal file
139
server/internal/handler/platform.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"dianshang/internal/service"
|
||||
"dianshang/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PlatformHandler 平台处理器
|
||||
type PlatformHandler struct {
|
||||
platformService *service.PlatformService
|
||||
}
|
||||
|
||||
// NewPlatformHandler 创建平台处理器
|
||||
func NewPlatformHandler(platformService *service.PlatformService) *PlatformHandler {
|
||||
return &PlatformHandler{
|
||||
platformService: platformService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlatforms 获取平台列表
|
||||
func (h *PlatformHandler) GetPlatforms(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||
name := c.Query("name")
|
||||
|
||||
var status *int
|
||||
if statusStr := c.Query("status"); statusStr != "" {
|
||||
if s, err := strconv.Atoi(statusStr); err == nil {
|
||||
status = &s
|
||||
}
|
||||
}
|
||||
|
||||
platforms, pagination, err := h.platformService.GetPlatformList(page, pageSize, status, name)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if pagination != nil {
|
||||
response.Page(c, platforms, pagination.Total, pagination.Page, pagination.PageSize)
|
||||
} else {
|
||||
response.Success(c, platforms)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlatform 获取平台详情
|
||||
func (h *PlatformHandler) GetPlatform(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
|
||||
return
|
||||
}
|
||||
|
||||
platform, err := h.platformService.GetPlatformByID(uint(id))
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, platform)
|
||||
}
|
||||
|
||||
// CreatePlatform 创建平台
|
||||
func (h *PlatformHandler) CreatePlatform(c *gin.Context) {
|
||||
var platform model.Platform
|
||||
if err := c.ShouldBindJSON(&platform); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if platform.Code == "" {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "平台代码不能为空")
|
||||
return
|
||||
}
|
||||
if platform.Name == "" {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "平台名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.platformService.CreatePlatform(&platform); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, platform)
|
||||
}
|
||||
|
||||
// UpdatePlatform 更新平台
|
||||
func (h *PlatformHandler) UpdatePlatform(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.platformService.UpdatePlatform(uint(id), updates); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "平台更新成功", nil)
|
||||
}
|
||||
|
||||
// DeletePlatform 删除平台
|
||||
func (h *PlatformHandler) DeletePlatform(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR_INVALID_PARAMS, "无效的平台ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.platformService.DeletePlatform(uint(id)); err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithMessage(c, "平台删除成功", nil)
|
||||
}
|
||||
|
||||
// GetAllActivePlatforms 获取所有启用的平台(用于下拉选择)
|
||||
func (h *PlatformHandler) GetAllActivePlatforms(c *gin.Context) {
|
||||
platforms, err := h.platformService.GetAllActivePlatforms()
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, platforms)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"dianshang/pkg/response"
|
||||
"dianshang/pkg/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -26,11 +27,35 @@ func NewProductHandler(productService *service.ProductService) *ProductHandler {
|
||||
func (h *ProductHandler) GetProductList(c *gin.Context) {
|
||||
page := utils.StringToInt(c.DefaultQuery("page", "1"))
|
||||
pageSize := utils.StringToInt(c.DefaultQuery("page_size", "20"))
|
||||
categoryID := utils.StringToUint(c.Query("category_id"))
|
||||
|
||||
// 支持 category_ids (逗号分隔)或 category_id
|
||||
var categoryID uint
|
||||
if categoryIDsStr := c.Query("category_ids"); categoryIDsStr != "" {
|
||||
// 解析第一个分类ID
|
||||
ids := strings.Split(categoryIDsStr, ",")
|
||||
if len(ids) > 0 {
|
||||
categoryID = utils.StringToUint(strings.TrimSpace(ids[0]))
|
||||
}
|
||||
} else {
|
||||
categoryID = utils.StringToUint(c.Query("category_id"))
|
||||
}
|
||||
|
||||
keyword := c.Query("keyword")
|
||||
minPrice, _ := strconv.ParseFloat(c.Query("min_price"), 64)
|
||||
maxPrice, _ := strconv.ParseFloat(c.Query("max_price"), 64)
|
||||
|
||||
// 库存筛选参数
|
||||
var inStock *bool
|
||||
if inStockStr := c.Query("in_stock"); inStockStr != "" {
|
||||
if inStockStr == "true" {
|
||||
trueVal := true
|
||||
inStock = &trueVal
|
||||
} else if inStockStr == "false" {
|
||||
falseVal := false
|
||||
inStock = &falseVal
|
||||
}
|
||||
}
|
||||
|
||||
// 处理排序参数:将前端传递的数字参数转换为后端期望的字符串参数
|
||||
sortParam := c.Query("sort")
|
||||
sortTypeParam := c.Query("sortType")
|
||||
@@ -52,7 +77,7 @@ func (h *ProductHandler) GetProductList(c *gin.Context) {
|
||||
sortType = "desc"
|
||||
}
|
||||
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, categoryID, keyword, minPrice, maxPrice, sort, sortType)
|
||||
products, pagination, err := h.productService.GetProductList(page, pageSize, categoryID, keyword, minPrice, maxPrice, inStock, sort, sortType)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
@@ -137,7 +162,20 @@ func (h *ProductHandler) DeleteProduct(c *gin.Context) {
|
||||
|
||||
// GetCategories 鑾峰彇鍒嗙被鍒楄〃
|
||||
func (h *ProductHandler) GetCategories(c *gin.Context) {
|
||||
categories, err := h.productService.GetCategories()
|
||||
// 支持平台筛选参数:platform=web 或 platform=miniprogram
|
||||
platform := c.Query("platform")
|
||||
|
||||
var categories []model.Category
|
||||
var err error
|
||||
|
||||
if platform != "" {
|
||||
// 根据平台获取分类
|
||||
categories, err = h.productService.GetCategoriesByPlatform(platform)
|
||||
} else {
|
||||
// 获取所有分类
|
||||
categories, err = h.productService.GetCategories()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
|
||||
@@ -36,6 +36,19 @@ type LoginRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
// EmailLoginRequest 邮箱登录请求结构
|
||||
type EmailLoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// EmailRegisterRequest 邮箱注册请求结构
|
||||
type EmailRegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Nickname string `json:"nickname" binding:"required"`
|
||||
}
|
||||
|
||||
// WeChatLoginRequest 微信登录请求结构
|
||||
type WeChatLoginRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
@@ -128,6 +141,56 @@ func (h *UserHandler) WeChatLogin(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// EmailLogin 邮箱登录(Web端使用)
|
||||
func (h *UserHandler) EmailLogin(c *gin.Context) {
|
||||
var req EmailLoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取客户端IP和UserAgent
|
||||
clientIP := utils.GetClientIP(
|
||||
c.ClientIP(),
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"),
|
||||
)
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
// 调用用户服务进行邮箱登录
|
||||
user, token, err := h.userService.EmailLogin(req.Email, req.Password, clientIP, userAgent)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": user,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// EmailRegister 邮箱注册(Web端使用)
|
||||
func (h *UserHandler) EmailRegister(c *gin.Context) {
|
||||
var req EmailRegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 调用用户服务进行注册
|
||||
user, err := h.userService.EmailRegister(req.Email, req.Password, req.Nickname)
|
||||
if err != nil {
|
||||
response.ErrorWithMessage(c, response.ERROR, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user_id": user.ID,
|
||||
"message": "注册成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetWeChatSession 获取微信会话信息
|
||||
func (h *UserHandler) GetWeChatSession(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
|
||||
@@ -3,6 +3,8 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"dianshang/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -11,8 +13,11 @@ func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
|
||||
// 记录 CORS 请求
|
||||
logger.Debugf("[CORS] Method=%s, Origin=%s, Path=%s", method, origin, c.Request.URL.Path)
|
||||
|
||||
// 设置允许的域名
|
||||
// 允许所有域名跨域访问
|
||||
if origin != "" {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
} else {
|
||||
@@ -21,22 +26,26 @@ func CORSMiddleware() gin.HandlerFunc {
|
||||
|
||||
// 设置允许的请求头
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-User-ID")
|
||||
|
||||
|
||||
// 设置允许的请求方法
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
|
||||
// 设置允许携带凭证
|
||||
|
||||
// 设置允许携带凭证(Cookie等)
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
// 设置预检请求的缓存时间
|
||||
|
||||
// 设置预检请求的缓存时间(24小时)
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
// 暴露的响应头(允许前端访问的自定义响应头)
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type, Authorization")
|
||||
|
||||
// 处理预检请求
|
||||
if method == "OPTIONS" {
|
||||
logger.Infof("[CORS] 预检请求 Origin=%s, Path=%s", origin, c.Request.URL.Path)
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
server/internal/model/livestream.go
Normal file
27
server/internal/model/livestream.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LiveStream 直播投流源模型
|
||||
type LiveStream struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Title string `json:"title" gorm:"type:varchar(255);not null;comment:投流源标题"`
|
||||
Platform string `json:"platform" gorm:"type:varchar(50);not null;comment:平台名称(如:抖音,快手,淘宝,京东,小红书等)"`
|
||||
StreamURL string `json:"stream_url" gorm:"type:varchar(500);not null;comment:投流URL地址"`
|
||||
CoverImage string `json:"cover_image" gorm:"type:varchar(500);comment:封面图片URL"`
|
||||
Description string `json:"description" gorm:"type:text;comment:描述信息"`
|
||||
Status int `json:"status" gorm:"type:tinyint;not null;default:1;comment:状态:0-禁用,1-启用"`
|
||||
Sort int `json:"sort" gorm:"type:int;not null;default:0;comment:排序值,数值越大越靠前"`
|
||||
ViewCount int `json:"view_count" gorm:"type:int;not null;default:0;comment:观看次数"`
|
||||
StartTime *time.Time `json:"start_time" gorm:"comment:开始时间"`
|
||||
EndTime *time.Time `json:"end_time" gorm:"comment:结束时间"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (LiveStream) TableName() string {
|
||||
return "ai_live_streams"
|
||||
}
|
||||
21
server/internal/model/platform.go
Normal file
21
server/internal/model/platform.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Platform 平台配置模型
|
||||
type Platform struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Code string `json:"code" gorm:"size:50;uniqueIndex;not null;comment:平台代码,如web,miniprogram"`
|
||||
Name string `json:"name" gorm:"size:100;not null;comment:平台名称"`
|
||||
Description string `json:"description" gorm:"size:255;comment:平台描述"`
|
||||
Icon string `json:"icon" gorm:"size:255;comment:平台图标"`
|
||||
Sort int `json:"sort" gorm:"default:0;comment:排序值"`
|
||||
Status int `json:"status" gorm:"default:1;comment:状态:0-禁用,1-启用"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Platform) TableName() string {
|
||||
return "ai_platforms"
|
||||
}
|
||||
@@ -8,6 +8,25 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JSONUintSlice 自定义JSON uint切片类型
|
||||
type JSONUintSlice []uint
|
||||
|
||||
func (j JSONUintSlice) Value() (driver.Value, error) {
|
||||
return json.Marshal(j)
|
||||
}
|
||||
|
||||
func (j *JSONUintSlice) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
bytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(bytes, j)
|
||||
}
|
||||
|
||||
// JSONSlice 自定义JSON切片类型
|
||||
type JSONSlice []string
|
||||
|
||||
@@ -81,47 +100,50 @@ type Category struct {
|
||||
Level int `json:"level" gorm:"default:1"`
|
||||
Icon string `json:"icon" gorm:"size:255"`
|
||||
Description string `json:"description" gorm:"size:255"`
|
||||
Platform JSONSlice `json:"platform" gorm:"type:json;comment:平台标识列表"`
|
||||
Sort int `json:"sort" gorm:"default:0"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// 关联字段
|
||||
Children []Category `json:"children,omitempty" gorm:"-"`
|
||||
HasChildren bool `json:"hasChildren" gorm:"-"`
|
||||
}
|
||||
|
||||
// Product 商品
|
||||
type Product struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
CategoryID uint `json:"category_id" gorm:"not null"`
|
||||
StoreID uint `json:"store_id" gorm:"default:1"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
|
||||
OrigPrice float64 `json:"orig_price" gorm:"type:decimal(10,2)"`
|
||||
Stock int `json:"stock" gorm:"default:0"`
|
||||
Sales int `json:"sales" gorm:"default:0"`
|
||||
CommentCount int `json:"comment_count" gorm:"default:0"`
|
||||
AverageRating float64 `json:"average_rating" gorm:"type:decimal(3,2);default:0.00"`
|
||||
MainImage string `json:"main_image" gorm:"size:255"`
|
||||
Images JSONSlice `json:"images" gorm:"type:json"`
|
||||
VideoURL string `json:"video_url" gorm:"size:255"`
|
||||
DetailImages JSONSlice `json:"detail_images" gorm:"type:json"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
IsHot bool `json:"is_hot" gorm:"default:false"`
|
||||
IsNew bool `json:"is_new" gorm:"default:false"`
|
||||
IsRecommend bool `json:"is_recommend" gorm:"default:false"`
|
||||
LimitBuy int `json:"limit_buy" gorm:"default:0"`
|
||||
Points int `json:"points" gorm:"default:0"`
|
||||
Level int `json:"level" gorm:"default:0"`
|
||||
Weight float64 `json:"weight" gorm:"type:decimal(8,2)"`
|
||||
Unit string `json:"unit" gorm:"size:20"`
|
||||
Sort int `json:"sort" gorm:"default:0"`
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
CategoryID JSONUintSlice `json:"category_id" gorm:"type:json;comment:分类ID列表"` // 改为JSON数组支持多分类
|
||||
StoreID uint `json:"store_id" gorm:"default:1"`
|
||||
Name string `json:"name" gorm:"size:100;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
|
||||
OrigPrice float64 `json:"orig_price" gorm:"type:decimal(10,2)"`
|
||||
Stock int `json:"stock" gorm:"default:0"`
|
||||
Sales int `json:"sales" gorm:"default:0"`
|
||||
CommentCount int `json:"comment_count" gorm:"default:0"`
|
||||
AverageRating float64 `json:"average_rating" gorm:"type:decimal(3,2);default:0.00"`
|
||||
MainImage string `json:"main_image" gorm:"size:255"`
|
||||
Images JSONSlice `json:"images" gorm:"type:json"`
|
||||
VideoURL string `json:"video_url" gorm:"size:255"`
|
||||
DetailImages JSONSlice `json:"detail_images" gorm:"type:json"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
IsHot bool `json:"is_hot" gorm:"default:false"`
|
||||
IsNew bool `json:"is_new" gorm:"default:false"`
|
||||
IsRecommend bool `json:"is_recommend" gorm:"default:false"`
|
||||
LimitBuy int `json:"limit_buy" gorm:"default:0"`
|
||||
Points int `json:"points" gorm:"default:0"`
|
||||
Level int `json:"level" gorm:"default:0"`
|
||||
Weight float64 `json:"weight" gorm:"type:decimal(8,2)"`
|
||||
Unit string `json:"unit" gorm:"size:20"`
|
||||
Sort int `json:"sort" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
|
||||
// 关联数据
|
||||
Category Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
|
||||
Categories []Category `json:"categories,omitempty" gorm:"-"` // 读取时填充的分类信息
|
||||
Store Store `json:"store,omitempty" gorm:"foreignKey:StoreID"`
|
||||
SKUs []ProductSKU `json:"skus,omitempty" gorm:"foreignKey:ProductID"`
|
||||
Tags []ProductTag `json:"tags,omitempty" gorm:"many2many:ai_product_tag_relations;"`
|
||||
|
||||
@@ -15,7 +15,8 @@ type User struct {
|
||||
Avatar string `json:"avatar" gorm:"size:255"`
|
||||
Gender int `json:"gender" gorm:"default:0"` // 0未知,1男,2女
|
||||
Phone string `json:"phone" gorm:"size:20"`
|
||||
Email string `json:"email" gorm:"size:100"`
|
||||
Email string `json:"email" gorm:"size:100;index"`
|
||||
Password string `json:"-" gorm:"size:255"` // Web端邮箱登录密码,不返回给前端
|
||||
Birthday *time.Time `json:"birthday"`
|
||||
Points int `json:"points" gorm:"default:0"`
|
||||
Level int `json:"level" gorm:"default:1"`
|
||||
|
||||
@@ -202,6 +202,22 @@ func (r *CommentRepository) CreateReply(reply *model.CommentReply) error {
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// GetHighRatingComments 获取高分评论(用于首页展示)
|
||||
func (r *CommentRepository) GetHighRatingComments(limit int, minRating int) ([]model.Comment, error) {
|
||||
var comments []model.Comment
|
||||
|
||||
// 获取评分>=minRating的评论,按评分降序、创建时间降序排列
|
||||
err := r.db.Model(&model.Comment{}).
|
||||
Where("status = ? AND rating >= ?", 1, minRating).
|
||||
Preload("User").
|
||||
Preload("Product").
|
||||
Order("rating DESC, created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&comments).Error
|
||||
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// GetReplies 获取评论回复列表
|
||||
func (r *CommentRepository) GetReplies(commentID uint) ([]model.CommentReply, error) {
|
||||
var replies []model.CommentReply
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dianshang/internal/model"
|
||||
"time"
|
||||
|
||||
@@ -121,3 +122,286 @@ func (r *CouponRepository) RestoreCoupon(userCouponID uint) error {
|
||||
"used_time": nil, // 清除使用时间
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ==================== 管理端方法 ====================
|
||||
|
||||
// GetCouponListForAdmin 获取优惠券列表(管理端)
|
||||
func (r *CouponRepository) GetCouponListForAdmin(page, pageSize int, status, couponType, keyword string) ([]model.Coupon, int64, error) {
|
||||
var coupons []model.Coupon
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.Coupon{})
|
||||
|
||||
// 状态筛选
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if couponType != "" {
|
||||
query = query.Where("type = ?", couponType)
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if keyword != "" {
|
||||
query = query.Where("name LIKE ? OR description LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&coupons).Error
|
||||
|
||||
return coupons, total, err
|
||||
}
|
||||
|
||||
// GetCouponUsageStats 获取优惠券使用统计
|
||||
func (r *CouponRepository) GetCouponUsageStats(couponID uint) (int, int, error) {
|
||||
// 获取领取数
|
||||
var receivedCount int64
|
||||
err := r.db.Model(&model.UserCoupon{}).Where("coupon_id = ?", couponID).Count(&receivedCount).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// 获取使用数
|
||||
var usedCount int64
|
||||
err = r.db.Model(&model.UserCoupon{}).Where("coupon_id = ? AND status = ?", couponID, 1).Count(&usedCount).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return int(receivedCount), int(usedCount), nil
|
||||
}
|
||||
|
||||
// Create 创建优惠券
|
||||
func (r *CouponRepository) Create(coupon *model.Coupon) error {
|
||||
return r.db.Create(coupon).Error
|
||||
}
|
||||
|
||||
// Update 更新优惠券
|
||||
func (r *CouponRepository) Update(couponID uint, updates map[string]interface{}) error {
|
||||
return r.db.Model(&model.Coupon{}).Where("id = ?", couponID).Updates(updates).Error
|
||||
}
|
||||
|
||||
// Delete 删除优惠券
|
||||
func (r *CouponRepository) Delete(couponID uint) error {
|
||||
return r.db.Delete(&model.Coupon{}, couponID).Error
|
||||
}
|
||||
|
||||
// CheckCouponHasUsers 检查优惠券是否有用户领取
|
||||
func (r *CouponRepository) CheckCouponHasUsers(couponID uint) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserCoupon{}).Where("coupon_id = ?", couponID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除优惠券
|
||||
func (r *CouponRepository) BatchDelete(couponIDs []uint) error {
|
||||
return r.db.Delete(&model.Coupon{}, couponIDs).Error
|
||||
}
|
||||
|
||||
// CountTotalCoupons 统计总优惠券数
|
||||
func (r *CouponRepository) CountTotalCoupons(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Coupon{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountActiveCoupons 统计启用的优惠券数
|
||||
func (r *CouponRepository) CountActiveCoupons(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
now := time.Now()
|
||||
err := r.db.WithContext(ctx).Model(&model.Coupon{}).
|
||||
Where("status = ? AND start_time <= ? AND end_time >= ?", 1, now, now).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountTotalReceived 统计总领取数
|
||||
func (r *CouponRepository) CountTotalReceived(ctx context.Context, startTime, endTime time.Time) (int64, error) {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&model.UserCoupon{})
|
||||
|
||||
if !startTime.IsZero() && !endTime.IsZero() {
|
||||
query = query.Where("created_at BETWEEN ? AND ?", startTime, endTime)
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountTotalUsed 统计总使用数
|
||||
func (r *CouponRepository) CountTotalUsed(ctx context.Context, startTime, endTime time.Time) (int64, error) {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&model.UserCoupon{}).Where("status = ?", 1)
|
||||
|
||||
if !startTime.IsZero() && !endTime.IsZero() {
|
||||
query = query.Where("used_time BETWEEN ? AND ?", startTime, endTime)
|
||||
}
|
||||
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetCouponTypeStats 获取各类型优惠券统计
|
||||
func (r *CouponRepository) GetCouponTypeStats(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
rows, err := r.db.WithContext(ctx).Model(&model.Coupon{}).
|
||||
Select("type, COUNT(*) as count").
|
||||
Group("type").
|
||||
Rows()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var couponType, count int
|
||||
if err := rows.Scan(&couponType, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typeName := ""
|
||||
switch couponType {
|
||||
case 1:
|
||||
typeName = "满减券"
|
||||
case 2:
|
||||
typeName = "折扣券"
|
||||
case 3:
|
||||
typeName = "免邮券"
|
||||
}
|
||||
|
||||
results = append(results, map[string]interface{}{
|
||||
"type": couponType,
|
||||
"type_name": typeName,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTopCoupons 获取热门优惠券
|
||||
func (r *CouponRepository) GetTopCoupons(ctx context.Context, limit int) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
err := r.db.WithContext(ctx).Model(&model.Coupon{}).
|
||||
Select("id, name, type, received_count, used_count").
|
||||
Order("received_count DESC").
|
||||
Limit(limit).
|
||||
Scan(&results).Error
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetUserCouponListForAdmin 获取用户优惠券列表(管理端)
|
||||
func (r *CouponRepository) GetUserCouponListForAdmin(userID uint, page, pageSize int) ([]model.UserCoupon, int64, error) {
|
||||
var userCoupons []model.UserCoupon
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.UserCoupon{}).Where("user_id = ?", userID)
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Preload("Coupon").
|
||||
Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&userCoupons).Error
|
||||
|
||||
return userCoupons, total, err
|
||||
}
|
||||
|
||||
// GetDistributeHistory 获取优惠券发放历史
|
||||
func (r *CouponRepository) GetDistributeHistory(page, pageSize int) ([]map[string]interface{}, int64, error) {
|
||||
var history []map[string]interface{}
|
||||
var total int64
|
||||
|
||||
// 先获取总数
|
||||
type CountResult struct {
|
||||
Count int64
|
||||
}
|
||||
var countResult CountResult
|
||||
err := r.db.Model(&model.UserCoupon{}).
|
||||
Select("COUNT(DISTINCT DATE(created_at), coupon_id) as count").
|
||||
Scan(&countResult).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
total = countResult.Count
|
||||
|
||||
// 分页查询分组数据
|
||||
type DistributeRecord struct {
|
||||
DistributeDate string `gorm:"column:distribute_date"`
|
||||
CouponID uint `gorm:"column:coupon_id"`
|
||||
TotalCount int `gorm:"column:total_count"`
|
||||
UnusedCount int `gorm:"column:unused_count"`
|
||||
UsedCount int `gorm:"column:used_count"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
var records []DistributeRecord
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.Model(&model.UserCoupon{}).
|
||||
Select(`
|
||||
DATE(created_at) as distribute_date,
|
||||
coupon_id,
|
||||
COUNT(*) as total_count,
|
||||
SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) as unused_count,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as used_count,
|
||||
MIN(created_at) as created_at
|
||||
`).
|
||||
Group("DATE(created_at), coupon_id").
|
||||
Order("created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Scan(&records).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 处理结果
|
||||
for i, record := range records {
|
||||
// 获取优惠券信息
|
||||
var coupon model.Coupon
|
||||
r.db.First(&coupon, record.CouponID)
|
||||
|
||||
// 判断发放类型
|
||||
distributeType := "batch"
|
||||
if record.TotalCount == 1 {
|
||||
distributeType = "single"
|
||||
}
|
||||
|
||||
history = append(history, map[string]interface{}{
|
||||
"id": i + 1 + offset,
|
||||
"coupon_id": record.CouponID,
|
||||
"coupon_name": coupon.Name,
|
||||
"distribute_type": distributeType,
|
||||
"distribute_date": record.DistributeDate,
|
||||
"total_count": record.TotalCount,
|
||||
"success_count": record.TotalCount,
|
||||
"fail_count": 0,
|
||||
"used_count": record.UsedCount,
|
||||
"unused_count": record.UnusedCount,
|
||||
"admin_name": "系统",
|
||||
"created_at": record.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return history, total, nil
|
||||
}
|
||||
|
||||
124
server/internal/repository/livestream.go
Normal file
124
server/internal/repository/livestream.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LiveStreamRepository interface {
|
||||
GetList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error)
|
||||
GetByID(id uint) (*model.LiveStream, error)
|
||||
GetActiveLiveStreams() ([]model.LiveStream, error)
|
||||
Create(stream *model.LiveStream) error
|
||||
Update(id uint, stream *model.LiveStream) error
|
||||
UpdateStatus(id uint, status int) error
|
||||
Delete(id uint) error
|
||||
BatchDelete(ids []uint) error
|
||||
IncrementViewCount(id uint) error
|
||||
}
|
||||
|
||||
type liveStreamRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewLiveStreamRepository(db *gorm.DB) LiveStreamRepository {
|
||||
return &liveStreamRepository{db: db}
|
||||
}
|
||||
|
||||
// GetList 获取投流源列表
|
||||
func (r *liveStreamRepository) GetList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error) {
|
||||
var streams []model.LiveStream
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&model.LiveStream{})
|
||||
|
||||
// 标题筛选
|
||||
if title != "" {
|
||||
query = query.Where("title LIKE ?", "%"+title+"%")
|
||||
}
|
||||
|
||||
// 平台筛选
|
||||
if platform != "" {
|
||||
query = query.Where("platform = ?", platform)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if status != nil {
|
||||
query = query.Where("status = ?", *status)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询,按排序和创建时间排序
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("sort DESC, created_at DESC").
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Find(&streams).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return streams, total, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取投流源详情
|
||||
func (r *liveStreamRepository) GetByID(id uint) (*model.LiveStream, error) {
|
||||
var stream model.LiveStream
|
||||
if err := r.db.First(&stream, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stream, nil
|
||||
}
|
||||
|
||||
// GetActiveLiveStreams 获取所有启用且在有效期内的投流源
|
||||
func (r *liveStreamRepository) GetActiveLiveStreams() ([]model.LiveStream, error) {
|
||||
var streams []model.LiveStream
|
||||
now := time.Now()
|
||||
|
||||
query := r.db.Where("status = ?", 1)
|
||||
|
||||
// 查询有效时间范围内的投流源
|
||||
query = query.Where("(start_time IS NULL OR start_time <= ?) AND (end_time IS NULL OR end_time >= ?)", now, now)
|
||||
|
||||
if err := query.Order("sort DESC, created_at DESC").Find(&streams).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
// Create 创建投流源
|
||||
func (r *liveStreamRepository) Create(stream *model.LiveStream) error {
|
||||
return r.db.Create(stream).Error
|
||||
}
|
||||
|
||||
// Update 更新投流源
|
||||
func (r *liveStreamRepository) Update(id uint, stream *model.LiveStream) error {
|
||||
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).Updates(stream).Error
|
||||
}
|
||||
|
||||
// UpdateStatus 更新投流源状态
|
||||
func (r *liveStreamRepository) UpdateStatus(id uint, status int) error {
|
||||
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
// Delete 删除投流源
|
||||
func (r *liveStreamRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&model.LiveStream{}, id).Error
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除投流源
|
||||
func (r *liveStreamRepository) BatchDelete(ids []uint) error {
|
||||
return r.db.Delete(&model.LiveStream{}, ids).Error
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加观看次数
|
||||
func (r *liveStreamRepository) IncrementViewCount(id uint) error {
|
||||
return r.db.Model(&model.LiveStream{}).Where("id = ?", id).
|
||||
UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
|
||||
}
|
||||
@@ -192,9 +192,12 @@ func (r *OrderRepository) UpdateOrderItem(id uint, updates map[string]interface{
|
||||
}
|
||||
|
||||
// GetCart 获取购物车
|
||||
// 优化: 减少不必要的Preload,只加载必需的关联数据
|
||||
func (r *OrderRepository) GetCart(userID uint) ([]model.Cart, error) {
|
||||
var cart []model.Cart
|
||||
err := r.db.Preload("Product").Preload("Product.SKUs", "status = ?", 1).Preload("SKU").Where("user_id = ?", userID).Find(&cart).Error
|
||||
// 移除 Product.SKUs 的预加载,因为购物车已经有单独的SKU字段
|
||||
// 只保留必要的Product和SKU信息
|
||||
err := r.db.Preload("Product").Preload("SKU").Where("user_id = ?", userID).Find(&cart).Error
|
||||
return cart, err
|
||||
}
|
||||
|
||||
|
||||
55
server/internal/repository/platform.go
Normal file
55
server/internal/repository/platform.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PlatformRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPlatformRepository(db *gorm.DB) *PlatformRepository {
|
||||
return &PlatformRepository{db: db}
|
||||
}
|
||||
|
||||
// GetAll 获取所有平台
|
||||
func (r *PlatformRepository) GetAll() ([]model.Platform, error) {
|
||||
var platforms []model.Platform
|
||||
err := r.db.Where("status = ?", 1).Order("sort DESC, created_at ASC").Find(&platforms).Error
|
||||
return platforms, err
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取平台
|
||||
func (r *PlatformRepository) GetByID(id uint) (*model.Platform, error) {
|
||||
var platform model.Platform
|
||||
err := r.db.Where("id = ?", id).First(&platform).Error
|
||||
return &platform, err
|
||||
}
|
||||
|
||||
// GetByCode 根据代码获取平台
|
||||
func (r *PlatformRepository) GetByCode(code string) (*model.Platform, error) {
|
||||
var platform model.Platform
|
||||
err := r.db.Where("code = ? AND status = ?", code, 1).First(&platform).Error
|
||||
return &platform, err
|
||||
}
|
||||
|
||||
// Create 创建平台
|
||||
func (r *PlatformRepository) Create(platform *model.Platform) error {
|
||||
return r.db.Create(platform).Error
|
||||
}
|
||||
|
||||
// Update 更新平台
|
||||
func (r *PlatformRepository) Update(id uint, updates map[string]interface{}) error {
|
||||
return r.db.Model(&model.Platform{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// Delete 删除平台
|
||||
func (r *PlatformRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&model.Platform{}, id).Error
|
||||
}
|
||||
|
||||
// GetDB 获取数据库连接
|
||||
func (r *PlatformRepository) GetDB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -46,7 +48,7 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
|
||||
for key, value := range conditions {
|
||||
switch key {
|
||||
case "category_id":
|
||||
// 支持包含子分类的筛选
|
||||
// category_id 现在是 JSON 数组,使用 JSON_CONTAINS 查询
|
||||
var catID uint
|
||||
switch v := value.(type) {
|
||||
case uint:
|
||||
@@ -57,12 +59,21 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
|
||||
catID = uint(v)
|
||||
}
|
||||
if catID > 0 {
|
||||
// 获取包含子分类的所有分类ID
|
||||
categoryIDs, err := r.getCategoryIDsIncludingChildren(catID)
|
||||
if err == nil && len(categoryIDs) > 0 {
|
||||
query = query.Where("category_id IN (?)", categoryIDs)
|
||||
// 使用 JSON_CONTAINS 查询包含任意一个分类ID的商品
|
||||
// 构建 OR 条件:JSON_CONTAINS(category_id, '1') OR JSON_CONTAINS(category_id, '2') ...
|
||||
conditions := make([]string, len(categoryIDs))
|
||||
args := make([]interface{}, len(categoryIDs))
|
||||
for i, id := range categoryIDs {
|
||||
conditions[i] = "JSON_CONTAINS(category_id, ?)"
|
||||
args[i] = fmt.Sprintf("%d", id)
|
||||
}
|
||||
query = query.Where(strings.Join(conditions, " OR "), args...)
|
||||
} else {
|
||||
// 兜底:如果获取子分类失败,退化为当前分类
|
||||
query = query.Where("category_id = ?", catID)
|
||||
// 兜底:如果获取子分类失败,只查询当前分类
|
||||
query = query.Where("JSON_CONTAINS(category_id, ?)", fmt.Sprintf("%d", catID))
|
||||
}
|
||||
}
|
||||
case "keyword":
|
||||
@@ -71,6 +82,15 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
|
||||
query = query.Where("price >= ?", value)
|
||||
case "max_price":
|
||||
query = query.Where("price <= ?", value)
|
||||
case "in_stock":
|
||||
// 库存筛选:true=有货,false=缺货
|
||||
if inStockValue, ok := value.(bool); ok {
|
||||
if inStockValue {
|
||||
query = query.Where("stock > ?", 0)
|
||||
} else {
|
||||
query = query.Where("stock = ?", 0)
|
||||
}
|
||||
}
|
||||
case "is_hot":
|
||||
if value.(string) == "true" {
|
||||
query = query.Where("is_hot = ?", true)
|
||||
@@ -129,16 +149,15 @@ func (r *ProductRepository) GetList(offset, limit int, conditions map[string]int
|
||||
}
|
||||
}
|
||||
|
||||
// 获取列表,预加载分类
|
||||
err := query.Preload("Category").
|
||||
Offset(offset).Limit(limit).Order(orderBy).Find(&products).Error
|
||||
// 获取列表
|
||||
err := query.Offset(offset).Limit(limit).Order(orderBy).Find(&products).Error
|
||||
return products, total, err
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取产品详情
|
||||
func (r *ProductRepository) GetByID(id uint) (*model.Product, error) {
|
||||
var product model.Product
|
||||
err := r.db.Preload("Category").Preload("Specs").Preload("SKUs", "status = ?", 1).
|
||||
err := r.db.Preload("Specs").Preload("SKUs", "status = ?", 1).
|
||||
Where("id = ?", id).First(&product).Error
|
||||
return &product, err
|
||||
}
|
||||
@@ -216,8 +235,21 @@ func (r *ProductRepository) RestoreStock(id uint, quantity int) error {
|
||||
|
||||
// GetCategories 获取分类列表
|
||||
func (r *ProductRepository) GetCategories() ([]model.Category, error) {
|
||||
return r.GetCategoriesByPlatform("")
|
||||
}
|
||||
|
||||
// GetCategoriesByPlatform 根据平台获取分类列表
|
||||
func (r *ProductRepository) GetCategoriesByPlatform(platformCode string) ([]model.Category, error) {
|
||||
var allCategories []model.Category
|
||||
err := r.db.Where("status = ?", 1).Order("level ASC, sort DESC, created_at ASC").Find(&allCategories).Error
|
||||
query := r.db.Where("status = ?", 1)
|
||||
|
||||
// 如果指定了平台,筛选包含该平台的分类
|
||||
if platformCode != "" {
|
||||
// 使用 JSON_CONTAINS 查询包含指定平台的分类
|
||||
query = query.Where("JSON_CONTAINS(platform, ?)", `"`+platformCode+`"`)
|
||||
}
|
||||
|
||||
err := query.Order("level ASC, sort DESC, created_at ASC").Find(&allCategories).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -409,8 +441,7 @@ func (r *ProductRepository) DeleteProductSpec(id uint) error {
|
||||
// GetHotProducts 获取热门产品
|
||||
func (r *ProductRepository) GetHotProducts(limit int) ([]model.Product, error) {
|
||||
var products []model.Product
|
||||
err := r.db.Preload("Category").
|
||||
Where("status = ? AND is_hot = ?", 1, 1).
|
||||
err := r.db.Where("status = ? AND is_hot = ?", 1, 1).
|
||||
Order("sales DESC, created_at DESC").Limit(limit).Find(&products).Error
|
||||
return products, err
|
||||
}
|
||||
@@ -418,8 +449,7 @@ func (r *ProductRepository) GetHotProducts(limit int) ([]model.Product, error) {
|
||||
// GetRecommendProducts 获取推荐产品
|
||||
func (r *ProductRepository) GetRecommendProducts(limit int) ([]model.Product, error) {
|
||||
var products []model.Product
|
||||
err := r.db.Preload("Category").
|
||||
Where("status = ? AND is_recommend = ?", 1, 1).
|
||||
err := r.db.Where("status = ? AND is_recommend = ?", 1, 1).
|
||||
Order("sort DESC, created_at DESC").Limit(limit).Find(&products).Error
|
||||
return products, err
|
||||
}
|
||||
@@ -660,7 +690,7 @@ func (r *ProductRepository) AssignTagsToProduct(productID uint, tagIDs []uint) e
|
||||
func (r *ProductRepository) GetLowStockProducts(threshold int) ([]model.Product, error) {
|
||||
var products []model.Product
|
||||
err := r.db.Where("stock <= ? AND status = ?", threshold, 1).
|
||||
Preload("Category").Find(&products).Error
|
||||
Find(&products).Error
|
||||
return products, err
|
||||
}
|
||||
|
||||
@@ -698,7 +728,7 @@ func (r *ProductRepository) GetInventoryStatistics() (map[string]interface{}, er
|
||||
// GetProductsForExport 获取用于导出的商品数据
|
||||
func (r *ProductRepository) GetProductsForExport(conditions map[string]interface{}) ([]model.Product, error) {
|
||||
var products []model.Product
|
||||
query := r.db.Model(&model.Product{}).Preload("Category")
|
||||
query := r.db.Model(&model.Product{})
|
||||
|
||||
// 添加查询条件
|
||||
for key, value := range conditions {
|
||||
|
||||
@@ -35,6 +35,13 @@ func (r *UserRepository) GetByOpenID(openID string) (*model.User, error) {
|
||||
return &user, err
|
||||
}
|
||||
|
||||
// GetByEmail 根据邮箱获取用户
|
||||
func (r *UserRepository) GetByEmail(email string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *UserRepository) Update(id uint, updates map[string]interface{}) error {
|
||||
return r.db.Model(&model.User{}).Where("id = ?", id).Updates(updates).Error
|
||||
|
||||
@@ -35,6 +35,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
pointsRepo := repository.NewPointsRepository(db)
|
||||
refundRepo := repository.NewRefundRepository(db)
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
platformRepo := repository.NewPlatformRepository(db)
|
||||
liveStreamRepo := repository.NewLiveStreamRepository(db)
|
||||
|
||||
// 初始化services
|
||||
userService := service.NewUserService(db)
|
||||
@@ -49,6 +51,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
roleService := service.NewRoleService(db)
|
||||
logService := service.NewLogService(db)
|
||||
commentService := service.NewCommentService(commentRepo, orderRepo, productRepo)
|
||||
platformService := service.NewPlatformService(platformRepo, productRepo)
|
||||
liveStreamService := service.NewLiveStreamService(liveStreamRepo)
|
||||
// 初始化微信支付服务 - 使用官方SDK
|
||||
var wechatPayService *service.WeChatPayService
|
||||
if cfg.WeChatPay.AppID != "" && cfg.WeChatPay.MchID != "" {
|
||||
@@ -133,6 +137,8 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
{
|
||||
userRoutes.POST("/login", userHandler.Login) // 用户登录(兼容旧版本)
|
||||
userRoutes.POST("/wechat-login", userHandler.WeChatLogin) // 微信登录
|
||||
userRoutes.POST("/email-login", userHandler.EmailLogin) // 邮箱登录(Web端)
|
||||
userRoutes.POST("/email-register", userHandler.EmailRegister) // 邮箱注册(Web端)
|
||||
userRoutes.GET("/wechat-session", middleware.AuthMiddleware(), userHandler.GetWeChatSession) // 获取微信会话
|
||||
userRoutes.POST("/register", userHandler.Register) // 用户注册
|
||||
userRoutes.GET("/profile", middleware.AuthMiddleware(), userHandler.GetProfile) // 获取用户信息
|
||||
@@ -166,6 +172,11 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
// 轮播图相关路由
|
||||
bannerHandler := handler.NewBannerHandler(bannerService)
|
||||
api.GET("/banners", bannerHandler.GetBanners) // 获取轮播图
|
||||
|
||||
// 直播投流源相关路由(前台)
|
||||
liveStreamHandler := handler.NewLiveStreamHandler(liveStreamService)
|
||||
api.GET("/livestreams", liveStreamHandler.GetActiveLiveStreams) // 获取启用的投流源
|
||||
api.POST("/livestreams/:id/view", liveStreamHandler.IncrementViewCount) // 增加观看次数
|
||||
|
||||
// 优惠券相关路由
|
||||
couponHandler := handler.NewCouponHandler(couponService)
|
||||
@@ -261,7 +272,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
}
|
||||
|
||||
// 订单相关路由
|
||||
orderHandler := handler.NewOrderHandler(orderService)
|
||||
orderHandler := handler.NewOrderHandler(orderService, wechatPayService)
|
||||
orderSettleHandler := handler.NewOrderSettleHandler(orderService, productService, userService)
|
||||
orderRoutes := api.Group("/orders", middleware.AuthMiddleware())
|
||||
{
|
||||
@@ -271,6 +282,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
orderRoutes.POST("/settle", orderSettleHandler.SettleOrder) // 订单结算
|
||||
orderRoutes.POST("/merge", orderHandler.MergeOrders) // 合并订单
|
||||
orderRoutes.PUT("/:id/pay", orderHandler.PayOrder) // 支付订单
|
||||
orderRoutes.GET("/:id/payment/status", orderHandler.GetPaymentStatus) // 获取支付状态
|
||||
orderRoutes.PUT("/:id/cancel", orderHandler.CancelOrder) // 取消订单
|
||||
orderRoutes.PUT("/:id/remind-ship", orderHandler.RemindShip) // 提醒发货
|
||||
orderRoutes.PUT("/:id/receive", orderHandler.ConfirmReceive) // 确认收货
|
||||
@@ -300,6 +312,7 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
commentRoutes := api.Group("/comments")
|
||||
{
|
||||
// 公开路由(无需认证)
|
||||
commentRoutes.GET("/high-rating", commentHandler.GetHighRatingComments) // 获取高分评论(首页展示)
|
||||
commentRoutes.GET("/products/:product_id", commentHandler.GetProductComments) // 获取商品评论列表
|
||||
commentRoutes.GET("/products/:product_id/stats", commentHandler.GetCommentStats) // 获取商品评论统计
|
||||
commentRoutes.GET("/:id", commentHandler.GetCommentDetail) // 获取评论详情
|
||||
@@ -406,6 +419,18 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
categoryAdmin.DELETE("/:id", adminProductHandler.DeleteCategory) // 删除分类
|
||||
}
|
||||
|
||||
// 平台管理
|
||||
platformHandler := handler.NewPlatformHandler(platformService)
|
||||
platformAdmin := admin.Group("/platforms")
|
||||
{
|
||||
platformAdmin.GET("", platformHandler.GetPlatforms) // 获取平台列表
|
||||
platformAdmin.GET("/all/active", platformHandler.GetAllActivePlatforms) // 获取所有启用平台(用于下拉选择)
|
||||
platformAdmin.GET("/:id", platformHandler.GetPlatform) // 获取平台详情
|
||||
platformAdmin.POST("", platformHandler.CreatePlatform) // 创建平台
|
||||
platformAdmin.PUT("/:id", platformHandler.UpdatePlatform) // 更新平台
|
||||
platformAdmin.DELETE("/:id", platformHandler.DeletePlatform) // 删除平台
|
||||
}
|
||||
|
||||
// 店铺管理
|
||||
admin.GET("/stores", adminProductHandler.GetStores) // 获取店铺列表
|
||||
|
||||
@@ -503,6 +528,19 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
bannerAdmin.POST("/clean-expired", adminBannerHandler.CleanExpiredBanners) // 清理过期轮播图
|
||||
}
|
||||
|
||||
// 直播投流源管理
|
||||
adminLiveStreamHandler := handler.NewLiveStreamHandler(liveStreamService)
|
||||
liveStreamAdmin := admin.Group("/livestreams")
|
||||
{
|
||||
liveStreamAdmin.GET("", adminLiveStreamHandler.GetLiveStreamList) // 获取投流源列表
|
||||
liveStreamAdmin.GET("/:id", adminLiveStreamHandler.GetLiveStreamDetail) // 获取投流源详情
|
||||
liveStreamAdmin.POST("", adminLiveStreamHandler.CreateLiveStream) // 创建投流源
|
||||
liveStreamAdmin.PUT("/:id", adminLiveStreamHandler.UpdateLiveStream) // 更新投流源
|
||||
liveStreamAdmin.DELETE("/:id", adminLiveStreamHandler.DeleteLiveStream) // 删除投流源
|
||||
liveStreamAdmin.DELETE("/batch", adminLiveStreamHandler.BatchDeleteLiveStreams) // 批量删除投流源
|
||||
liveStreamAdmin.PUT("/:id/status", adminLiveStreamHandler.UpdateLiveStreamStatus) // 更新投流源状态
|
||||
}
|
||||
|
||||
// 文件上传管理(管理员专用)
|
||||
uploadAdmin := admin.Group("/upload")
|
||||
{
|
||||
@@ -518,6 +556,23 @@ func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
pointsAdmin.POST("/users/:id/deduct", pointsHandler.DeductPoints) // 扣除用户积分
|
||||
}
|
||||
|
||||
// 优惠券管理
|
||||
adminCouponHandler := handler.NewAdminCouponHandler(couponService)
|
||||
couponAdmin := admin.Group("/coupons")
|
||||
{
|
||||
couponAdmin.GET("", adminCouponHandler.GetCouponList) // 获取优惠券列表
|
||||
couponAdmin.GET("/:id", adminCouponHandler.GetCouponDetail) // 获取优惠券详情
|
||||
couponAdmin.POST("", adminCouponHandler.CreateCoupon) // 创建优惠券
|
||||
couponAdmin.PUT("/:id", adminCouponHandler.UpdateCoupon) // 更新优惠券
|
||||
couponAdmin.DELETE("/:id", adminCouponHandler.DeleteCoupon) // 删除优惠券
|
||||
couponAdmin.PUT("/:id/status", adminCouponHandler.UpdateCouponStatus) // 更新优惠券状态
|
||||
couponAdmin.DELETE("/batch", adminCouponHandler.BatchDeleteCoupons) // 批量删除优惠券
|
||||
couponAdmin.GET("/statistics", adminCouponHandler.GetCouponStatistics) // 获取优惠券统计
|
||||
couponAdmin.GET("/user-coupons", adminCouponHandler.GetUserCouponList) // 获取用户优惠券列表
|
||||
couponAdmin.POST("/distribute", adminCouponHandler.DistributeCoupon) // 发放优惠券
|
||||
couponAdmin.GET("/distribute/history", adminCouponHandler.GetDistributeHistory) // 获取发放历史
|
||||
}
|
||||
|
||||
// 角色权限管理
|
||||
adminRoleHandler := handler.NewAdminRoleHandler(db, roleService)
|
||||
roleAdmin := admin.Group("/roles")
|
||||
|
||||
@@ -27,13 +27,8 @@ func NewCartService(orderRepo *repository.OrderRepository, productRepo *reposito
|
||||
}
|
||||
|
||||
// GetCart 获取购物车
|
||||
// 优化: 移除用户存在性检查,因为中间件已经验证过token和用户
|
||||
func (s *CartService) GetCart(userID uint) ([]model.Cart, error) {
|
||||
// 检查用户是否存在
|
||||
_, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
|
||||
return s.orderRepo.GetCart(userID)
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,15 @@ func (s *CommentService) GetCommentStats(productID uint) (*model.CommentStats, e
|
||||
return s.commentRepo.GetStats(productID)
|
||||
}
|
||||
|
||||
// GetHighRatingComments 获取高分评论(用于首页展示)
|
||||
func (s *CommentService) GetHighRatingComments(limit int) ([]model.Comment, error) {
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 6 // 默认6条
|
||||
}
|
||||
// 获取评分>=4的高分评论
|
||||
return s.commentRepo.GetHighRatingComments(limit, 4)
|
||||
}
|
||||
|
||||
// GetCommentByID 获取评论详情
|
||||
func (s *CommentService) GetCommentByID(id uint) (*model.Comment, error) {
|
||||
return s.commentRepo.GetByID(id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dianshang/internal/model"
|
||||
"dianshang/internal/repository"
|
||||
"errors"
|
||||
@@ -224,3 +225,338 @@ func (s *CouponService) GetAvailableCouponsForOrder(userID uint, orderAmount flo
|
||||
|
||||
return availableCoupons, nil
|
||||
}
|
||||
|
||||
// ==================== 管理端方法 ====================
|
||||
|
||||
// GetCouponListForAdmin 获取优惠券列表(管理端)
|
||||
func (s *CouponService) GetCouponListForAdmin(page, pageSize int, status, couponType, keyword string) ([]model.Coupon, int64, error) {
|
||||
return s.couponRepo.GetCouponListForAdmin(page, pageSize, status, couponType, keyword)
|
||||
}
|
||||
|
||||
// GetCouponDetailForAdmin 获取优惠券详情(管理端)
|
||||
func (s *CouponService) GetCouponDetailForAdmin(couponID uint) (*model.Coupon, error) {
|
||||
coupon, err := s.couponRepo.GetByID(couponID)
|
||||
if err != nil {
|
||||
return nil, errors.New("优惠券不存在")
|
||||
}
|
||||
|
||||
// 获取优惠券的使用统计(注意:领取数和使用数不在Coupon表中,而是在UserCoupon表中统计)
|
||||
// 这里仅作为查询,不修改coupon对象
|
||||
_, _, _ = s.couponRepo.GetCouponUsageStats(couponID)
|
||||
|
||||
return coupon, nil
|
||||
}
|
||||
|
||||
// CreateCoupon 创建优惠券
|
||||
func (s *CouponService) CreateCoupon(req interface{}, adminID uint) (*model.Coupon, error) {
|
||||
// 类型断言
|
||||
type CreateCouponRequest struct {
|
||||
Name string
|
||||
Type int
|
||||
Value int64
|
||||
MinAmount int64
|
||||
Description string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
TotalCount int
|
||||
Status int
|
||||
}
|
||||
|
||||
reqData, ok := req.(*CreateCouponRequest)
|
||||
if !ok {
|
||||
return nil, errors.New("无效的请求参数")
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if reqData.EndTime.Before(reqData.StartTime) {
|
||||
return nil, errors.New("结束时间不能早于开始时间")
|
||||
}
|
||||
|
||||
// 验证优惠券类型和值
|
||||
switch reqData.Type {
|
||||
case 1: // 满减券,value是金额(分)
|
||||
if reqData.Value <= 0 {
|
||||
return nil, errors.New("满减券优惠金额必须大于0")
|
||||
}
|
||||
case 2: // 折扣券,value是折扣(85表示8.5折)
|
||||
if reqData.Value <= 0 || reqData.Value > 100 {
|
||||
return nil, errors.New("折扣券折扣必须在0-100之间")
|
||||
}
|
||||
case 3: // 免邮券
|
||||
// 免邮券不需要验证value
|
||||
default:
|
||||
return nil, errors.New("不支持的优惠券类型")
|
||||
}
|
||||
|
||||
coupon := &model.Coupon{
|
||||
Name: reqData.Name,
|
||||
Type: uint8(reqData.Type),
|
||||
Value: uint(reqData.Value),
|
||||
MinAmount: uint(reqData.MinAmount),
|
||||
Description: reqData.Description,
|
||||
StartTime: reqData.StartTime,
|
||||
EndTime: reqData.EndTime,
|
||||
TotalCount: uint(reqData.TotalCount),
|
||||
UsedCount: 0,
|
||||
Status: uint8(reqData.Status),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := s.couponRepo.Create(coupon)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建优惠券失败: %v", err)
|
||||
}
|
||||
|
||||
return coupon, nil
|
||||
}
|
||||
|
||||
// UpdateCoupon 更新优惠券
|
||||
func (s *CouponService) UpdateCoupon(couponID uint, req interface{}) error {
|
||||
// 检查优惠券是否存在
|
||||
_, err := s.couponRepo.GetByID(couponID)
|
||||
if err != nil {
|
||||
return errors.New("优惠券不存在")
|
||||
}
|
||||
|
||||
// 类型断言
|
||||
type UpdateCouponRequest struct {
|
||||
Name string
|
||||
Type int
|
||||
Value int64
|
||||
MinAmount int64
|
||||
Description string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
TotalCount int
|
||||
Status int
|
||||
}
|
||||
|
||||
reqData, ok := req.(*UpdateCouponRequest)
|
||||
if !ok {
|
||||
return errors.New("无效的请求参数")
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if !reqData.EndTime.IsZero() && !reqData.StartTime.IsZero() {
|
||||
if reqData.EndTime.Before(reqData.StartTime) {
|
||||
return errors.New("结束时间不能早于开始时间")
|
||||
}
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if reqData.Name != "" {
|
||||
updates["name"] = reqData.Name
|
||||
}
|
||||
if reqData.Type > 0 {
|
||||
updates["type"] = reqData.Type
|
||||
}
|
||||
if reqData.Value > 0 {
|
||||
updates["value"] = reqData.Value
|
||||
}
|
||||
if reqData.MinAmount >= 0 {
|
||||
updates["min_amount"] = reqData.MinAmount
|
||||
}
|
||||
if reqData.Description != "" {
|
||||
updates["description"] = reqData.Description
|
||||
}
|
||||
if !reqData.StartTime.IsZero() {
|
||||
updates["start_time"] = reqData.StartTime
|
||||
}
|
||||
if !reqData.EndTime.IsZero() {
|
||||
updates["end_time"] = reqData.EndTime
|
||||
}
|
||||
if reqData.TotalCount >= 0 {
|
||||
updates["total_count"] = reqData.TotalCount
|
||||
}
|
||||
if reqData.Status >= 0 {
|
||||
updates["status"] = reqData.Status
|
||||
}
|
||||
updates["updated_at"] = time.Now()
|
||||
|
||||
return s.couponRepo.Update(couponID, updates)
|
||||
}
|
||||
|
||||
// DeleteCoupon 删除优惠券
|
||||
func (s *CouponService) DeleteCoupon(couponID uint) error {
|
||||
// 检查是否有用户已领取
|
||||
hasUsers, err := s.couponRepo.CheckCouponHasUsers(couponID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasUsers {
|
||||
return errors.New("该优惠券已被用户领取,无法删除")
|
||||
}
|
||||
|
||||
return s.couponRepo.Delete(couponID)
|
||||
}
|
||||
|
||||
// UpdateCouponStatus 更新优惠券状态
|
||||
func (s *CouponService) UpdateCouponStatus(couponID uint, status int) error {
|
||||
_, err := s.couponRepo.GetByID(couponID)
|
||||
if err != nil {
|
||||
return errors.New("优惠券不存在")
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
return s.couponRepo.Update(couponID, updates)
|
||||
}
|
||||
|
||||
// BatchDeleteCoupons 批量删除优惠券
|
||||
func (s *CouponService) BatchDeleteCoupons(couponIDs []uint) error {
|
||||
// 检查每个优惠券是否可以删除
|
||||
for _, id := range couponIDs {
|
||||
hasUsers, err := s.couponRepo.CheckCouponHasUsers(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasUsers {
|
||||
return fmt.Errorf("优惠券ID %d 已被用户领取,无法删除", id)
|
||||
}
|
||||
}
|
||||
|
||||
return s.couponRepo.BatchDelete(couponIDs)
|
||||
}
|
||||
|
||||
// GetCouponStatistics 获取优惠券统计
|
||||
func (s *CouponService) GetCouponStatistics(startTime, endTime time.Time) (map[string]interface{}, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 获取总优惠券数
|
||||
totalCoupons, err := s.couponRepo.CountTotalCoupons(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取启用的优惠券数
|
||||
activeCoupons, err := s.couponRepo.CountActiveCoupons(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取总领取数
|
||||
totalReceived, err := s.couponRepo.CountTotalReceived(ctx, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取总使用数
|
||||
totalUsed, err := s.couponRepo.CountTotalUsed(ctx, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各类型优惠券统计
|
||||
typeStats, err := s.couponRepo.GetCouponTypeStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取热门优惠券
|
||||
topCoupons, err := s.couponRepo.GetTopCoupons(ctx, 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算使用率
|
||||
useRate := 0.0
|
||||
if totalReceived > 0 {
|
||||
useRate = float64(totalUsed) / float64(totalReceived) * 100
|
||||
}
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_coupons": totalCoupons,
|
||||
"active_coupons": activeCoupons,
|
||||
"total_received": totalReceived,
|
||||
"total_used": totalUsed,
|
||||
"use_rate": fmt.Sprintf("%.2f%%", useRate),
|
||||
"type_stats": typeStats,
|
||||
"top_coupons": topCoupons,
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetUserCouponListForAdmin 获取用户优惠券列表(管理端)
|
||||
func (s *CouponService) GetUserCouponListForAdmin(userID uint, page, pageSize int) ([]model.UserCoupon, int64, error) {
|
||||
return s.couponRepo.GetUserCouponListForAdmin(userID, page, pageSize)
|
||||
}
|
||||
|
||||
// DistributeCoupon 发放优惠券
|
||||
func (s *CouponService) DistributeCoupon(couponID uint, userIDs []uint, distributeAll bool, quantity int, adminID uint) (map[string]interface{}, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// 验证优惠券是否存在
|
||||
coupon, err := s.couponRepo.GetByID(couponID)
|
||||
if err != nil {
|
||||
return nil, errors.New("优惠券不存在")
|
||||
}
|
||||
|
||||
// 检查优惠券状态
|
||||
if coupon.Status != 1 {
|
||||
return nil, errors.New("优惠券已禁用,无法发放")
|
||||
}
|
||||
|
||||
// 如果是全员发放,获取所有用户ID
|
||||
var targetUserIDs []uint
|
||||
distributeType := "single"
|
||||
if distributeAll {
|
||||
distributeType = "all"
|
||||
// TODO: 从用户表获取所有用户ID
|
||||
// targetUserIDs, err = s.userRepo.GetAllUserIDs()
|
||||
// 暂时返回错误,需要注入 userRepo
|
||||
return nil, errors.New("全员发放功能暂未实现")
|
||||
} else {
|
||||
targetUserIDs = userIDs
|
||||
if len(targetUserIDs) > 1 {
|
||||
distributeType = "batch"
|
||||
}
|
||||
}
|
||||
|
||||
// 记录发放结果
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
|
||||
// 给每个用户发放优惠券
|
||||
for _, userID := range targetUserIDs {
|
||||
for i := 0; i < quantity; i++ {
|
||||
userCoupon := &model.UserCoupon{
|
||||
UserID: userID,
|
||||
CouponID: couponID,
|
||||
Status: 0, // 未使用
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := s.couponRepo.CreateUserCoupon(userCoupon)
|
||||
if err != nil {
|
||||
failCount++
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 记录发放历史到数据库
|
||||
// 需要创建 coupon_distribute_history 表
|
||||
_ = ctx
|
||||
_ = adminID
|
||||
|
||||
result := map[string]interface{}{
|
||||
"total_count": len(targetUserIDs) * quantity,
|
||||
"success_count": successCount,
|
||||
"fail_count": failCount,
|
||||
"distribute_type": distributeType,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDistributeHistory 获取发放历史
|
||||
func (s *CouponService) GetDistributeHistory(page, pageSize int) ([]map[string]interface{}, int64, error) {
|
||||
return s.couponRepo.GetDistributeHistory(page, pageSize)
|
||||
}
|
||||
|
||||
144
server/internal/service/livestream.go
Normal file
144
server/internal/service/livestream.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"dianshang/internal/repository"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type LiveStreamService interface {
|
||||
GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error)
|
||||
GetLiveStreamByID(id uint) (*model.LiveStream, error)
|
||||
GetActiveLiveStreams() ([]model.LiveStream, error)
|
||||
CreateLiveStream(stream *model.LiveStream) error
|
||||
UpdateLiveStream(id uint, stream *model.LiveStream) error
|
||||
UpdateLiveStreamStatus(id uint, status int) error
|
||||
DeleteLiveStream(id uint) error
|
||||
BatchDeleteLiveStreams(ids []uint) error
|
||||
IncrementViewCount(id uint) error
|
||||
}
|
||||
|
||||
type liveStreamService struct {
|
||||
liveStreamRepo repository.LiveStreamRepository
|
||||
}
|
||||
|
||||
func NewLiveStreamService(liveStreamRepo repository.LiveStreamRepository) LiveStreamService {
|
||||
return &liveStreamService{
|
||||
liveStreamRepo: liveStreamRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLiveStreamList 获取投流源列表
|
||||
func (s *liveStreamService) GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.GetList(page, pageSize, title, platform, status)
|
||||
}
|
||||
|
||||
// GetLiveStreamByID 获取投流源详情
|
||||
func (s *liveStreamService) GetLiveStreamByID(id uint) (*model.LiveStream, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("无效的投流源ID")
|
||||
}
|
||||
return s.liveStreamRepo.GetByID(id)
|
||||
}
|
||||
|
||||
// GetActiveLiveStreams 获取所有启用的投流源
|
||||
func (s *liveStreamService) GetActiveLiveStreams() ([]model.LiveStream, error) {
|
||||
return s.liveStreamRepo.GetActiveLiveStreams()
|
||||
}
|
||||
|
||||
// CreateLiveStream 创建投流源
|
||||
func (s *liveStreamService) CreateLiveStream(stream *model.LiveStream) error {
|
||||
if stream.Title == "" {
|
||||
return errors.New("投流源标题不能为空")
|
||||
}
|
||||
if stream.Platform == "" {
|
||||
return errors.New("平台名称不能为空")
|
||||
}
|
||||
if stream.StreamURL == "" {
|
||||
return errors.New("投流URL不能为空")
|
||||
}
|
||||
|
||||
// 检查该平台是否已有投流源
|
||||
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
|
||||
if err != nil {
|
||||
return errors.New("检查平台投流源失败")
|
||||
}
|
||||
if len(existingStreams) > 0 {
|
||||
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.Create(stream)
|
||||
}
|
||||
|
||||
// UpdateLiveStream 更新投流源
|
||||
func (s *liveStreamService) UpdateLiveStream(id uint, stream *model.LiveStream) error {
|
||||
if id == 0 {
|
||||
return errors.New("无效的投流源ID")
|
||||
}
|
||||
|
||||
// 检查投流源是否存在
|
||||
existing, err := s.liveStreamRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("投流源不存在")
|
||||
}
|
||||
|
||||
// 如果修改了平台,检查新平台是否已有其他投流源
|
||||
if stream.Platform != "" && stream.Platform != existing.Platform {
|
||||
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
|
||||
if err != nil {
|
||||
return errors.New("检查平台投流源失败")
|
||||
}
|
||||
if len(existingStreams) > 0 {
|
||||
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
|
||||
}
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.Update(id, stream)
|
||||
}
|
||||
|
||||
// UpdateLiveStreamStatus 更新投流源状态
|
||||
func (s *liveStreamService) UpdateLiveStreamStatus(id uint, status int) error {
|
||||
if id == 0 {
|
||||
return errors.New("无效的投流源ID")
|
||||
}
|
||||
|
||||
if status != 0 && status != 1 {
|
||||
return errors.New("无效的状态值")
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.UpdateStatus(id, status)
|
||||
}
|
||||
|
||||
// DeleteLiveStream 删除投流源
|
||||
func (s *liveStreamService) DeleteLiveStream(id uint) error {
|
||||
if id == 0 {
|
||||
return errors.New("无效的投流源ID")
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.Delete(id)
|
||||
}
|
||||
|
||||
// BatchDeleteLiveStreams 批量删除投流源
|
||||
func (s *liveStreamService) BatchDeleteLiveStreams(ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("请选择要删除的投流源")
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.BatchDelete(ids)
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加观看次数
|
||||
func (s *liveStreamService) IncrementViewCount(id uint) error {
|
||||
if id == 0 {
|
||||
return errors.New("无效的投流源ID")
|
||||
}
|
||||
|
||||
return s.liveStreamRepo.IncrementViewCount(id)
|
||||
}
|
||||
150
server/internal/service/platform.go
Normal file
150
server/internal/service/platform.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"dianshang/internal/model"
|
||||
"dianshang/internal/repository"
|
||||
"dianshang/pkg/utils"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// PlatformService 平台服务
|
||||
type PlatformService struct {
|
||||
platformRepo *repository.PlatformRepository
|
||||
productRepo *repository.ProductRepository
|
||||
}
|
||||
|
||||
// NewPlatformService 创建平台服务
|
||||
func NewPlatformService(platformRepo *repository.PlatformRepository, productRepo *repository.ProductRepository) *PlatformService {
|
||||
return &PlatformService{
|
||||
platformRepo: platformRepo,
|
||||
productRepo: productRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlatformList 获取平台列表
|
||||
func (s *PlatformService) GetPlatformList(page, pageSize int, status *int, name string) ([]model.Platform, *utils.Pagination, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 如果不需要分页,获取所有平台
|
||||
if page == 0 && pageSize == 0 {
|
||||
platforms, err := s.platformRepo.GetAll()
|
||||
return platforms, nil, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// TODO: 实现带筛选的分页查询
|
||||
// 暂时先返回所有平台
|
||||
platforms, err := s.platformRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 简单筛选
|
||||
var filteredPlatforms []model.Platform
|
||||
for _, p := range platforms {
|
||||
if status != nil && p.Status != *status {
|
||||
continue
|
||||
}
|
||||
if name != "" && p.Name != name && p.Code != name {
|
||||
continue
|
||||
}
|
||||
filteredPlatforms = append(filteredPlatforms, p)
|
||||
}
|
||||
|
||||
total := len(filteredPlatforms)
|
||||
|
||||
// 分页
|
||||
start := offset
|
||||
end := offset + pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
result := filteredPlatforms[start:end]
|
||||
|
||||
pagination := utils.NewPagination(page, pageSize)
|
||||
pagination.Total = int64(total)
|
||||
|
||||
return result, pagination, nil
|
||||
}
|
||||
|
||||
// GetPlatformByID 根据ID获取平台
|
||||
func (s *PlatformService) GetPlatformByID(id uint) (*model.Platform, error) {
|
||||
return s.platformRepo.GetByID(id)
|
||||
}
|
||||
|
||||
// GetPlatformByCode 根据代码获取平台
|
||||
func (s *PlatformService) GetPlatformByCode(code string) (*model.Platform, error) {
|
||||
return s.platformRepo.GetByCode(code)
|
||||
}
|
||||
|
||||
// CreatePlatform 创建平台
|
||||
func (s *PlatformService) CreatePlatform(platform *model.Platform) error {
|
||||
// 检查平台代码是否已存在
|
||||
existing, _ := s.platformRepo.GetByCode(platform.Code)
|
||||
if existing != nil {
|
||||
return errors.New("平台代码已存在")
|
||||
}
|
||||
|
||||
return s.platformRepo.Create(platform)
|
||||
}
|
||||
|
||||
// UpdatePlatform 更新平台
|
||||
func (s *PlatformService) UpdatePlatform(id uint, updates map[string]interface{}) error {
|
||||
// 检查平台是否存在
|
||||
_, err := s.platformRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("平台不存在")
|
||||
}
|
||||
|
||||
// 如果更新代码,检查是否与其他平台冲突
|
||||
if code, ok := updates["code"].(string); ok {
|
||||
existing, _ := s.platformRepo.GetByCode(code)
|
||||
if existing != nil && existing.ID != id {
|
||||
return errors.New("平台代码已被其他平台使用")
|
||||
}
|
||||
}
|
||||
|
||||
return s.platformRepo.Update(id, updates)
|
||||
}
|
||||
|
||||
// DeletePlatform 删除平台
|
||||
func (s *PlatformService) DeletePlatform(id uint) error {
|
||||
// 检查平台是否存在
|
||||
_, err := s.platformRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("平台不存在")
|
||||
}
|
||||
|
||||
// TODO: 检查是否有分类关联到该平台
|
||||
// 可以选择级联删除或禁止删除
|
||||
|
||||
return s.platformRepo.Delete(id)
|
||||
}
|
||||
|
||||
// GetAllActivePlatforms 获取所有启用的平台
|
||||
func (s *PlatformService) GetAllActivePlatforms() ([]model.Platform, error) {
|
||||
platforms, err := s.platformRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 过滤启用的平台
|
||||
var activePlatforms []model.Platform
|
||||
for _, p := range platforms {
|
||||
if p.Status == 1 {
|
||||
activePlatforms = append(activePlatforms, p)
|
||||
}
|
||||
}
|
||||
|
||||
return activePlatforms, nil
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func NewProductService(productRepo *repository.ProductRepository, userRepo *repo
|
||||
}
|
||||
|
||||
// GetProductList 获取产品列表(前端用户)
|
||||
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
|
||||
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, inStock *bool, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
@@ -48,6 +48,9 @@ func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, key
|
||||
if maxPrice > 0 {
|
||||
conditions["max_price"] = maxPrice
|
||||
}
|
||||
if inStock != nil {
|
||||
conditions["in_stock"] = *inStock
|
||||
}
|
||||
if sort != "" {
|
||||
conditions["sort"] = sort
|
||||
}
|
||||
@@ -128,10 +131,12 @@ func (s *ProductService) GetProductDetail(id uint) (*model.Product, error) {
|
||||
// CreateProduct 创建产品
|
||||
func (s *ProductService) CreateProduct(product *model.Product) error {
|
||||
// 验证分类是否存在
|
||||
if product.CategoryID > 0 {
|
||||
_, err := s.productRepo.GetCategoryByID(product.CategoryID)
|
||||
if err != nil {
|
||||
return errors.New("分类不存在")
|
||||
if len(product.CategoryID) > 0 {
|
||||
for _, catID := range product.CategoryID {
|
||||
_, err := s.productRepo.GetCategoryByID(catID)
|
||||
if err != nil {
|
||||
return errors.New("分类ID" + strconv.Itoa(int(catID)) + "不存在")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,28 +151,6 @@ func (s *ProductService) UpdateProduct(id uint, updates map[string]interface{})
|
||||
return errors.New("产品不存在")
|
||||
}
|
||||
|
||||
// 如果更新分类,验证分类是否存在
|
||||
if categoryID, ok := updates["category_id"]; ok {
|
||||
var catID uint
|
||||
switch v := categoryID.(type) {
|
||||
case uint:
|
||||
catID = v
|
||||
case float64:
|
||||
catID = uint(v)
|
||||
case int:
|
||||
catID = uint(v)
|
||||
default:
|
||||
return errors.New("分类ID格式错误")
|
||||
}
|
||||
|
||||
if catID > 0 {
|
||||
_, err := s.productRepo.GetCategoryByID(catID)
|
||||
if err != nil {
|
||||
return errors.New("分类不存在")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 detail_images 字段 - 确保正确转换为 JSONSlice 类型
|
||||
if detailImages, ok := updates["detail_images"]; ok {
|
||||
switch v := detailImages.(type) {
|
||||
@@ -376,6 +359,11 @@ func (s *ProductService) GetCategories() ([]model.Category, error) {
|
||||
return s.productRepo.GetCategories()
|
||||
}
|
||||
|
||||
// GetCategoriesByPlatform 根据平台获取分类列表
|
||||
func (s *ProductService) GetCategoriesByPlatform(platform string) ([]model.Category, error) {
|
||||
return s.productRepo.GetCategoriesByPlatform(platform)
|
||||
}
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func (s *ProductService) CreateCategory(category *model.Category) error {
|
||||
return s.productRepo.CreateCategory(category)
|
||||
@@ -492,7 +480,8 @@ func (s *ProductService) SearchProducts(keyword string, page, pageSize int, minP
|
||||
return []model.Product{}, utils.NewPagination(page, pageSize), nil
|
||||
}
|
||||
|
||||
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, sort, sortType)
|
||||
// 搜索不筛选库存,传递 nil
|
||||
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, nil, sort, sortType)
|
||||
}
|
||||
|
||||
// UpdateStock 更新库存
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -63,6 +64,102 @@ func (s *UserService) WeChatLogin(code string) (*model.User, string, error) {
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
// EmailLogin 邮箱登录
|
||||
func (s *UserService) EmailLogin(email, password, clientIP, userAgent string) (*model.User, string, error) {
|
||||
// 查找用户
|
||||
user, err := s.userRepo.GetByEmail(email)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", errors.New("邮箱或密码错误")
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, "", errors.New("邮箱或密码错误")
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if user.Status == 0 {
|
||||
return nil, "", errors.New("用户已被禁用")
|
||||
}
|
||||
|
||||
// 生成JWT token (7天有效期)
|
||||
tokenExpiry := 7 * 24 * 3600
|
||||
token, err := jwt.GenerateToken(user.ID, "user", tokenExpiry)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("生成token失败")
|
||||
}
|
||||
|
||||
// 记录登录日志
|
||||
s.logUserLogin(user.ID, "email", true, "", clientIP, userAgent)
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
// EmailRegister 邮箱注册
|
||||
func (s *UserService) EmailRegister(email, password, nickname string) (*model.User, error) {
|
||||
// 检查邮箱是否已注册
|
||||
_, err := s.userRepo.GetByEmail(email)
|
||||
if err == nil {
|
||||
// 找到了用户,说明邮箱已注册
|
||||
return nil, errors.New("该邮箱已被注册")
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 其他数据库错误
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, errors.New("密码加密失败")
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
// 为邮箱用户生成唯一的 OpenID(使用 email: 前缀避免与微信 OpenID 冲突)
|
||||
user := &model.User{
|
||||
OpenID: "email:" + email, // 使用邮箱作为 OpenID,避免唯一索引冲突
|
||||
Email: email,
|
||||
Password: string(hashedPassword),
|
||||
Nickname: nickname,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// logUserLogin 记录用户登录日志
|
||||
func (s *UserService) logUserLogin(userID uint, loginType string, success bool, errorMsg, ip, userAgent string) {
|
||||
status := 1
|
||||
if !success {
|
||||
status = 0
|
||||
}
|
||||
|
||||
remark := loginType + " 登录"
|
||||
if errorMsg != "" {
|
||||
remark = errorMsg
|
||||
}
|
||||
|
||||
log := &model.UserLoginLog{
|
||||
UserID: userID,
|
||||
LoginIP: ip,
|
||||
UserAgent: userAgent,
|
||||
LoginTime: time.Now(),
|
||||
Status: status,
|
||||
Remark: remark,
|
||||
}
|
||||
|
||||
if err := s.db.Create(log).Error; err != nil {
|
||||
fmt.Printf("记录登录日志失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
func (s *UserService) CreateUser(user *model.User) error {
|
||||
// 检查用户是否已存在
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
wechatutils "github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
@@ -25,6 +26,7 @@ type WeChatPayService struct {
|
||||
config *config.WeChatPayConfig
|
||||
client *core.Client
|
||||
jsapiSvc *jsapi.JsapiApiService
|
||||
nativeSvc *native.NativeApiService
|
||||
refundSvc *refunddomestic.RefundsApiService
|
||||
privateKey *rsa.PrivateKey
|
||||
orderRepo *repository.OrderRepository
|
||||
@@ -74,6 +76,9 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
|
||||
// 创建JSAPI服务
|
||||
jsapiSvc := &jsapi.JsapiApiService{Client: client}
|
||||
|
||||
// 创建Native扫码支付服务
|
||||
nativeSvc := &native.NativeApiService{Client: client}
|
||||
|
||||
// 创建退款服务
|
||||
refundSvc := &refunddomestic.RefundsApiService{Client: client}
|
||||
|
||||
@@ -86,6 +91,7 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
|
||||
config: cfg,
|
||||
client: client,
|
||||
jsapiSvc: jsapiSvc,
|
||||
nativeSvc: nativeSvc,
|
||||
refundSvc: refundSvc,
|
||||
privateKey: privateKey,
|
||||
orderRepo: orderRepo,
|
||||
@@ -174,6 +180,99 @@ func (s *WeChatPayService) CreateOrder(ctx context.Context, order *model.Order,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateNativeOrder 创建Native扫码支付订单(用于PC端)
|
||||
func (s *WeChatPayService) CreateNativeOrder(ctx context.Context, order *model.Order) (*WeChatPayResponse, error) {
|
||||
// 生成唯一的微信支付订单号
|
||||
wechatOutTradeNo := utils.GenerateWechatOutTradeNo()
|
||||
|
||||
logger.Info("开始创建Native扫码支付订单",
|
||||
"orderNo", order.OrderNo,
|
||||
"wechatOutTradeNo", wechatOutTradeNo,
|
||||
"totalAmount", order.TotalAmount,
|
||||
"hasClient", s.client != nil)
|
||||
|
||||
// 更新订单的微信支付订单号
|
||||
err := s.orderRepo.UpdateByOrderNo(order.OrderNo, map[string]interface{}{
|
||||
"wechat_out_trade_no": wechatOutTradeNo,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("更新订单微信支付订单号失败", "error", err, "orderNo", order.OrderNo)
|
||||
return nil, fmt.Errorf("更新订单失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果没有客户端(开发环境),使用模拟数据
|
||||
if s.client == nil {
|
||||
logger.Warn("开发环境下使用模拟Native支付数据")
|
||||
return s.createMockNativePayment(order)
|
||||
}
|
||||
|
||||
// 构建Native预支付请求
|
||||
req := native.PrepayRequest{
|
||||
Appid: core.String(s.config.AppID),
|
||||
Mchid: core.String(s.config.MchID),
|
||||
Description: core.String(fmt.Sprintf("订单号: %s", order.OrderNo)),
|
||||
OutTradeNo: core.String(wechatOutTradeNo),
|
||||
NotifyUrl: core.String(s.config.NotifyURL),
|
||||
Amount: &native.Amount{
|
||||
Total: core.Int64(int64(order.TotalAmount)),
|
||||
Currency: core.String("CNY"),
|
||||
},
|
||||
}
|
||||
|
||||
// 调用Native预支付API
|
||||
resp, result, err := s.nativeSvc.Prepay(ctx, req)
|
||||
if err != nil {
|
||||
log.Printf("call Native Prepay err:%s", err)
|
||||
logger.Error("创建Native支付订单失败", "error", err, "orderNo", order.OrderNo)
|
||||
return nil, fmt.Errorf("创建Native支付订单失败: %v", err)
|
||||
}
|
||||
|
||||
if result.Response.StatusCode != 200 {
|
||||
log.Printf("Native Prepay status=%d", result.Response.StatusCode)
|
||||
return nil, fmt.Errorf("Native预支付请求失败,状态码: %d", result.Response.StatusCode)
|
||||
}
|
||||
|
||||
log.Printf("Native Prepay success, code_url=%s", *resp.CodeUrl)
|
||||
logger.Info("微信Native支付API响应",
|
||||
"codeUrl", *resp.CodeUrl,
|
||||
"orderNo", order.OrderNo)
|
||||
|
||||
return &WeChatPayResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: map[string]interface{}{
|
||||
"qrcode_url": *resp.CodeUrl,
|
||||
"order_no": order.OrderNo,
|
||||
"amount": order.TotalAmount,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createMockNativePayment 创建模拟Native支付数据(开发环境使用)
|
||||
func (s *WeChatPayService) createMockNativePayment(order *model.Order) (*WeChatPayResponse, error) {
|
||||
// 生成模拟的二维码URL
|
||||
mockQRCodeURL := fmt.Sprintf("weixin://wxpay/bizpayurl?pr=mock_%s", utils.GenerateRandomString(10))
|
||||
|
||||
logger.Info("生成模拟Native支付参数",
|
||||
"environment", s.config.Environment,
|
||||
"qrcodeUrl", mockQRCodeURL,
|
||||
"orderNo", order.OrderNo,
|
||||
"totalAmount", order.TotalAmount)
|
||||
|
||||
return &WeChatPayResponse{
|
||||
Code: 0,
|
||||
Message: "模拟支付创建成功",
|
||||
Data: map[string]interface{}{
|
||||
"qrcode_url": mockQRCodeURL,
|
||||
"order_no": order.OrderNo,
|
||||
"amount": order.TotalAmount,
|
||||
"sandbox": true,
|
||||
"tips": "这是模拟环境的Native支付,请使用测试接口模拟支付成功",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createMockPayment 创建模拟支付数据(沙盒环境使用)
|
||||
func (s *WeChatPayService) createMockPayment(order *model.Order, openID string) (*WeChatPayResponse, error) {
|
||||
mockPrepayID := fmt.Sprintf("wx%d%s", time.Now().Unix(), generateNonceStr()[:8])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
-- 创建退款记录表
|
||||
-- 执行时间: 2024-01-01
|
||||
-- 描述: 为微信退款功能创建退款记录表,记录所有退款申请和处理状态
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ai_refunds` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '退款记录ID',
|
||||
`refund_no` varchar(64) NOT NULL COMMENT '退款单号,系统生成的唯一退款编号',
|
||||
`order_id` bigint unsigned NOT NULL COMMENT '关联订单ID',
|
||||
`order_no` varchar(32) NOT NULL COMMENT '订单号',
|
||||
`user_id` bigint unsigned NOT NULL COMMENT '用户ID',
|
||||
`refund_type` tinyint NOT NULL DEFAULT 1 COMMENT '退款类型:1=仅退款,2=退货退款',
|
||||
`refund_reason` varchar(255) NOT NULL COMMENT '退款原因',
|
||||
`refund_description` text COMMENT '退款详细说明',
|
||||
`refund_amount` decimal(10,2) NOT NULL COMMENT '退款金额(分)',
|
||||
`refund_fee` decimal(10,2) DEFAULT 0.00 COMMENT '退款手续费(分)',
|
||||
`actual_refund_amount` decimal(10,2) NOT NULL COMMENT '实际退款金额(分)',
|
||||
`status` tinyint NOT NULL DEFAULT 1 COMMENT '退款状态:1=待审核,2=审核通过,3=审核拒绝,4=退款中,5=退款成功,6=退款失败',
|
||||
`apply_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
|
||||
`audit_time` timestamp NULL COMMENT '审核时间',
|
||||
`refund_time` timestamp NULL COMMENT '退款完成时间',
|
||||
`admin_id` bigint unsigned NULL COMMENT '审核管理员ID',
|
||||
`admin_remark` text COMMENT '管理员备注',
|
||||
`reject_reason` varchar(255) NULL COMMENT '拒绝原因',
|
||||
|
||||
-- 微信退款相关字段
|
||||
`wechat_refund_id` varchar(64) NULL COMMENT '微信退款单号',
|
||||
`wechat_out_refund_no` varchar(64) NULL COMMENT '商户退款单号',
|
||||
`wechat_transaction_id` varchar(64) NULL COMMENT '微信支付交易号',
|
||||
`wechat_refund_status` varchar(32) NULL COMMENT '微信退款状态:SUCCESS=成功,CLOSED=关闭,PROCESSING=处理中',
|
||||
`wechat_refund_recv_accout` varchar(64) NULL COMMENT '退款入账账户',
|
||||
`wechat_success_time` timestamp NULL COMMENT '微信退款成功时间',
|
||||
`wechat_user_received_account` varchar(64) NULL COMMENT '退款到账账户',
|
||||
`wechat_refund_account` varchar(32) NULL COMMENT '退款资金来源:AVAILABLE=可用余额,UNAVAILABLE=不可用余额',
|
||||
|
||||
-- 退货相关字段(当refund_type=2时使用)
|
||||
`return_logistics_company` varchar(50) NULL COMMENT '退货物流公司',
|
||||
`return_logistics_no` varchar(100) NULL COMMENT '退货物流单号',
|
||||
`return_address` varchar(255) NULL COMMENT '退货地址',
|
||||
`goods_received_time` timestamp NULL COMMENT '商家收货时间',
|
||||
|
||||
-- 图片证据
|
||||
`evidence_images` json NULL COMMENT '退款凭证图片JSON数组',
|
||||
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_refund_no` (`refund_no`),
|
||||
UNIQUE KEY `uk_wechat_out_refund_no` (`wechat_out_refund_no`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_order_no` (`order_no`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_time` (`apply_time`),
|
||||
KEY `idx_wechat_refund_id` (`wechat_refund_id`),
|
||||
KEY `idx_wechat_transaction_id` (`wechat_transaction_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款记录表';
|
||||
|
||||
-- 为订单表添加退款相关字段(如果不存在)
|
||||
ALTER TABLE `ai_orders`
|
||||
ADD COLUMN IF NOT EXISTS `refunded_at` timestamp NULL COMMENT '退款时间' AFTER `refund_time`,
|
||||
ADD COLUMN IF NOT EXISTS `total_refund_amount` decimal(10,2) DEFAULT 0.00 COMMENT '累计退款金额' AFTER `refund_amount`,
|
||||
ADD COLUMN IF NOT EXISTS `refund_count` int DEFAULT 0 COMMENT '退款次数' AFTER `total_refund_amount`;
|
||||
|
||||
-- 创建退款项目表(支持部分退款)
|
||||
CREATE TABLE IF NOT EXISTS `ai_refund_items` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '退款项目ID',
|
||||
`refund_id` bigint unsigned NOT NULL COMMENT '退款记录ID',
|
||||
`order_item_id` bigint unsigned NOT NULL COMMENT '订单项ID',
|
||||
`product_id` bigint unsigned NOT NULL COMMENT '商品ID',
|
||||
`sku_id` bigint unsigned NULL COMMENT 'SKU ID',
|
||||
`product_name` varchar(100) NOT NULL COMMENT '商品名称',
|
||||
`product_image` varchar(255) NULL COMMENT '商品图片',
|
||||
`spec_info` json NULL COMMENT '规格信息',
|
||||
`quantity` int NOT NULL COMMENT '退款数量',
|
||||
`unit_price` decimal(10,2) NOT NULL COMMENT '单价(分)',
|
||||
`total_price` decimal(10,2) NOT NULL COMMENT '退款总价(分)',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_refund_id` (`refund_id`),
|
||||
KEY `idx_order_item_id` (`order_item_id`),
|
||||
KEY `idx_product_id` (`product_id`),
|
||||
FOREIGN KEY (`refund_id`) REFERENCES `ai_refunds` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款项目表';
|
||||
|
||||
-- 创建退款日志表
|
||||
CREATE TABLE IF NOT EXISTS `ai_refund_logs` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`refund_id` bigint unsigned NOT NULL COMMENT '退款记录ID',
|
||||
`action` varchar(50) NOT NULL COMMENT '操作类型:apply=申请,audit=审核,refund=退款,callback=回调',
|
||||
`status_from` tinyint NULL COMMENT '状态变更前',
|
||||
`status_to` tinyint NULL COMMENT '状态变更后',
|
||||
`operator_type` varchar(20) NOT NULL COMMENT '操作者类型:user=用户,admin=管理员,system=系统',
|
||||
`operator_id` bigint unsigned NULL COMMENT '操作者ID',
|
||||
`remark` text NULL COMMENT '操作备注',
|
||||
`extra_data` json NULL COMMENT '额外数据',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_refund_id` (`refund_id`),
|
||||
KEY `idx_action` (`action`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
FOREIGN KEY (`refund_id`) REFERENCES `ai_refunds` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款操作日志表';
|
||||
@@ -1,98 +0,0 @@
|
||||
-- 创建退款记录表
|
||||
-- 执行时间: 2024-01-01
|
||||
-- 描述: 为微信退款功能创建退款记录表,记录所有退款申请和处理状态
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ai_refunds` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '退款记录ID',
|
||||
`refund_no` varchar(64) NOT NULL COMMENT '退款单号,系统生成的唯一退款编号',
|
||||
`order_id` bigint unsigned NOT NULL COMMENT '关联订单ID',
|
||||
`order_no` varchar(32) NOT NULL COMMENT '订单号',
|
||||
`user_id` bigint unsigned NOT NULL COMMENT '用户ID',
|
||||
`refund_type` tinyint NOT NULL DEFAULT 1 COMMENT '退款类型:1=仅退款,2=退货退款',
|
||||
`refund_reason` varchar(255) NOT NULL COMMENT '退款原因',
|
||||
`refund_description` text COMMENT '退款详细说明',
|
||||
`refund_amount` decimal(10,2) NOT NULL COMMENT '退款金额(分)',
|
||||
`refund_fee` decimal(10,2) DEFAULT 0.00 COMMENT '退款手续费(分)',
|
||||
`actual_refund_amount` decimal(10,2) NOT NULL COMMENT '实际退款金额(分)',
|
||||
`status` tinyint NOT NULL DEFAULT 1 COMMENT '退款状态:1=待审核,2=审核通过,3=审核拒绝,4=退款中,5=退款成功,6=退款失败',
|
||||
`apply_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间',
|
||||
`audit_time` timestamp NULL COMMENT '审核时间',
|
||||
`refund_time` timestamp NULL COMMENT '退款完成时间',
|
||||
`admin_id` bigint unsigned NULL COMMENT '审核管理员ID',
|
||||
`admin_remark` text COMMENT '管理员备注',
|
||||
`reject_reason` varchar(255) NULL COMMENT '拒绝原因',
|
||||
|
||||
-- 微信退款相关字段
|
||||
`wechat_refund_id` varchar(64) NULL COMMENT '微信退款单号',
|
||||
`wechat_out_refund_no` varchar(64) NULL COMMENT '商户退款单号',
|
||||
`wechat_transaction_id` varchar(64) NULL COMMENT '微信支付交易号',
|
||||
`wechat_refund_status` varchar(32) NULL COMMENT '微信退款状态:SUCCESS=成功,CLOSED=关闭,PROCESSING=处理中',
|
||||
`wechat_refund_recv_accout` varchar(64) NULL COMMENT '退款入账账户',
|
||||
`wechat_success_time` timestamp NULL COMMENT '微信退款成功时间',
|
||||
`wechat_user_received_account` varchar(64) NULL COMMENT '退款到账账户',
|
||||
`wechat_refund_account` varchar(32) NULL COMMENT '退款资金来源:AVAILABLE=可用余额,UNAVAILABLE=不可用余额',
|
||||
|
||||
-- 退货相关字段(当refund_type=2时使用)
|
||||
`return_logistics_company` varchar(50) NULL COMMENT '退货物流公司',
|
||||
`return_logistics_no` varchar(100) NULL COMMENT '退货物流单号',
|
||||
`return_address` varchar(255) NULL COMMENT '退货地址',
|
||||
`goods_received_time` timestamp NULL COMMENT '商家收货时间',
|
||||
|
||||
-- 图片证据
|
||||
`evidence_images` json NULL COMMENT '退款凭证图片JSON数组',
|
||||
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_refund_no` (`refund_no`),
|
||||
UNIQUE KEY `uk_wechat_out_refund_no` (`wechat_out_refund_no`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_order_no` (`order_no`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_time` (`apply_time`),
|
||||
KEY `idx_wechat_refund_id` (`wechat_refund_id`),
|
||||
KEY `idx_wechat_transaction_id` (`wechat_transaction_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款记录表';
|
||||
|
||||
-- 创建退款项目表(支持部分退款)
|
||||
CREATE TABLE IF NOT EXISTS `ai_refund_items` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '退款项目ID',
|
||||
`refund_id` bigint unsigned NOT NULL COMMENT '退款记录ID',
|
||||
`order_item_id` bigint unsigned NOT NULL COMMENT '订单项ID',
|
||||
`product_id` bigint unsigned NOT NULL COMMENT '商品ID',
|
||||
`sku_id` bigint unsigned NULL COMMENT 'SKU ID',
|
||||
`product_name` varchar(100) NOT NULL COMMENT '商品名称',
|
||||
`product_image` varchar(255) NULL COMMENT '商品图片',
|
||||
`spec_info` json NULL COMMENT '规格信息',
|
||||
`quantity` int NOT NULL COMMENT '退款数量',
|
||||
`unit_price` decimal(10,2) NOT NULL COMMENT '单价(分)',
|
||||
`total_price` decimal(10,2) NOT NULL COMMENT '退款总价(分)',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_refund_id` (`refund_id`),
|
||||
KEY `idx_order_item_id` (`order_item_id`),
|
||||
KEY `idx_product_id` (`product_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款项目表';
|
||||
|
||||
-- 创建退款日志表
|
||||
CREATE TABLE IF NOT EXISTS `ai_refund_logs` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '日志ID',
|
||||
`refund_id` bigint unsigned NOT NULL COMMENT '退款记录ID',
|
||||
`action` varchar(50) NOT NULL COMMENT '操作类型:apply=申请,audit=审核,refund=退款,callback=回调',
|
||||
`status_from` tinyint NULL COMMENT '状态变更前',
|
||||
`status_to` tinyint NULL COMMENT '状态变更后',
|
||||
`operator_type` varchar(20) NOT NULL COMMENT '操作者类型:user=用户,admin=管理员,system=系统',
|
||||
`operator_id` bigint unsigned NULL COMMENT '操作者ID',
|
||||
`remark` text NULL COMMENT '操作备注',
|
||||
`extra_data` json NULL COMMENT '额外数据',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_refund_id` (`refund_id`),
|
||||
KEY `idx_action` (`action`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款操作日志表';
|
||||
@@ -1,15 +0,0 @@
|
||||
-- 修复 ai_products 表的 id 字段为自增主键,解决 Error 1364: Field 'id' doesn't have a default value
|
||||
-- 说明:如果表结构早期创建未设置 AUTO_INCREMENT,插入时会因严格模式报错。
|
||||
-- 注意:本脚本不会移除主键或外键,仅在现有主键上设置自增。
|
||||
|
||||
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS;
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
-- 将 id 字段调整为 BIGINT UNSIGNED 自增主键(根据实际类型可改为 INT UNSIGNED)
|
||||
ALTER TABLE `ai_products`
|
||||
MODIFY COLUMN `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
|
||||
|
||||
-- 验证:执行 SHOW CREATE TABLE ai_products; 应看到 `id` 为 AUTO_INCREMENT
|
||||
-- 参考:在应用代码中,model.Product 的 id 为 `gorm:"primaryKey;autoIncrement"`
|
||||
@@ -1,2 +0,0 @@
|
||||
-- 添加退款时间字段到订单表
|
||||
ALTER TABLE ai_orders ADD COLUMN refunded_at TIMESTAMP NULL DEFAULT NULL COMMENT '退款时间';
|
||||
24
server/migrations/006_create_live_streams_table.sql
Normal file
24
server/migrations/006_create_live_streams_table.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- 创建直播投流源表
|
||||
CREATE TABLE IF NOT EXISTS `ai_live_streams` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`title` varchar(255) NOT NULL COMMENT '投流源标题',
|
||||
`platform` varchar(50) NOT NULL COMMENT '平台名称(如:抖音,快手,淘宝,京东,小红书等)',
|
||||
`stream_url` varchar(500) NOT NULL COMMENT '投流URL地址',
|
||||
`cover_image` varchar(500) DEFAULT NULL COMMENT '封面图片URL',
|
||||
`description` text COMMENT '描述信息',
|
||||
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
|
||||
`sort` int NOT NULL DEFAULT '0' COMMENT '排序值,数值越大越靠前',
|
||||
`view_count` int NOT NULL DEFAULT '0' COMMENT '观看次数',
|
||||
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
|
||||
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_platform` (`platform`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_sort` (`sort`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='直播投流源表';
|
||||
|
||||
ALTER TABLE `ai_live_streams`
|
||||
ADD UNIQUE INDEX `idx_platform_unique` (`platform`);
|
||||
@@ -1,66 +0,0 @@
|
||||
-- 评论功能相关表创建脚本
|
||||
-- 创建时间: 2024-12-19
|
||||
|
||||
-- 商品评论表
|
||||
CREATE TABLE `ai_comments` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
|
||||
`product_id` bigint(20) unsigned NOT NULL COMMENT '商品ID',
|
||||
`order_id` bigint(20) unsigned NOT NULL COMMENT '订单ID',
|
||||
`order_item_id` bigint(20) unsigned NOT NULL COMMENT '订单项ID',
|
||||
`rating` tinyint(4) NOT NULL DEFAULT '5' COMMENT '评分 1-5星',
|
||||
`content` text COMMENT '评论内容',
|
||||
`images` text COMMENT '评论图片,JSON格式存储',
|
||||
`is_anonymous` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否匿名评论',
|
||||
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-正常,2-隐藏,3-删除',
|
||||
`reply_count` int(11) NOT NULL DEFAULT '0' COMMENT '回复数量',
|
||||
`like_count` int(11) NOT NULL DEFAULT '0' COMMENT '点赞数量',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_product_id` (`product_id`),
|
||||
KEY `idx_order_id` (`order_id`),
|
||||
KEY `idx_order_item_id` (`order_item_id`),
|
||||
KEY `idx_rating` (`rating`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品评论表';
|
||||
|
||||
-- 评论回复表
|
||||
CREATE TABLE `ai_comment_replies` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`comment_id` bigint(20) unsigned NOT NULL COMMENT '评论ID',
|
||||
`user_id` bigint(20) unsigned NOT NULL COMMENT '回复用户ID',
|
||||
`content` text NOT NULL COMMENT '回复内容',
|
||||
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否管理员回复',
|
||||
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-正常,2-隐藏,3-删除',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_comment_id` (`comment_id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论回复表';
|
||||
|
||||
-- 评论点赞表
|
||||
CREATE TABLE `ai_comment_likes` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`comment_id` bigint(20) unsigned NOT NULL COMMENT '评论ID',
|
||||
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_comment_user` (`comment_id`, `user_id`),
|
||||
KEY `idx_comment_id` (`comment_id`),
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论点赞表';
|
||||
|
||||
-- 为商品表添加评论统计字段
|
||||
ALTER TABLE `ai_products`
|
||||
ADD COLUMN `comment_count` int(11) NOT NULL DEFAULT '0' COMMENT '评论数量' AFTER `sales`,
|
||||
ADD COLUMN `average_rating` decimal(3,2) NOT NULL DEFAULT '0.00' COMMENT '平均评分' AFTER `comment_count`;
|
||||
|
||||
-- 为订单项表添加评论状态字段
|
||||
ALTER TABLE `order_items`
|
||||
ADD COLUMN `is_commented` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已评论' AFTER `spec_info`;
|
||||
@@ -1,26 +0,0 @@
|
||||
-- 添加微信相关字段到用户表
|
||||
-- 创建时间: 2024-12-19
|
||||
-- 描述: 为ai_users表添加微信登录所需的字段
|
||||
|
||||
USE ai_dianshang;
|
||||
|
||||
-- 为ai_users表添加微信相关字段(表中已有open_id和union_id,只需添加session相关字段)
|
||||
ALTER TABLE `ai_users`
|
||||
ADD COLUMN `wechat_session_key` VARCHAR(255) NULL COMMENT 'WeChat SessionKey' AFTER `status`;
|
||||
|
||||
ALTER TABLE `ai_users`
|
||||
ADD COLUMN `session_expiry` TIMESTAMP NULL COMMENT 'Session expiry time' AFTER `wechat_session_key`;
|
||||
|
||||
-- 添加索引以提高查询性能
|
||||
CREATE INDEX `idx_wechat_session_key` ON `ai_users` (`wechat_session_key`);
|
||||
CREATE INDEX `idx_session_expiry` ON `ai_users` (`session_expiry`);
|
||||
|
||||
-- 注释:
|
||||
-- 1. wechat_openid: 微信用户唯一标识,用于标识用户身份
|
||||
-- 2. wechat_unionid: 微信开放平台统一标识,同一用户在不同应用下的唯一标识
|
||||
-- 3. wechat_session_key: 微信会话密钥,用于解密用户数据,敏感信息需要安全存储
|
||||
-- 4. session_expiry: 会话过期时间,通常为7天,用于验证session_key有效性
|
||||
|
||||
-- 安全提醒:
|
||||
-- 在生产环境中,建议对wechat_session_key进行加密存储
|
||||
-- 定期清理过期的session_key以提高安全性
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "选择运行环境:"
|
||||
echo "1. 开发环境 (development)"
|
||||
echo "2. 测试环境 (test)"
|
||||
echo "3. 生产环境 (production)"
|
||||
echo "4. 默认环境 (使用config.yaml)"
|
||||
|
||||
read -p "请输入选择 (1-4): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
echo "启动开发环境..."
|
||||
export GO_ENV=development
|
||||
go run cmd/main.go
|
||||
;;
|
||||
2)
|
||||
echo "启动测试环境..."
|
||||
export GO_ENV=test
|
||||
go run cmd/main.go
|
||||
;;
|
||||
3)
|
||||
echo "启动生产环境..."
|
||||
export GO_ENV=production
|
||||
go run cmd/main.go
|
||||
;;
|
||||
4)
|
||||
echo "启动默认环境..."
|
||||
go run cmd/main.go
|
||||
;;
|
||||
*)
|
||||
echo "无效选择,使用默认环境..."
|
||||
go run cmd/main.go
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user