refactor: 重构项目结构并重命名模块
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 25s

feat(admin): 新增工会管理功能
feat(activity): 添加活动管理相关服务
feat(user): 实现用户道具卡和积分管理
feat(guild): 新增工会成员管理功能

fix: 修复数据库连接配置
fix: 修正jwtoken导入路径
fix: 解决端口冲突问题

style: 统一代码格式和注释风格
style: 更新项目常量命名

docs: 添加项目框架和开发规范文档
docs: 更新接口文档注释

chore: 移除无用代码和文件
chore: 更新Makefile和配置文件
chore: 清理日志文件

test: 添加道具卡测试脚本
This commit is contained in:
邹方成 2025-11-14 21:10:00 +08:00
parent b847a72a6a
commit 1ab39d2f5a
275 changed files with 36442 additions and 9908 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,228 @@
# 身份定义
你是一位资深的软件架构师和工程师,具备丰富的项目经验和系统思维能力。你的核心优势在于:
- 上下文工程专家:构建完整的任务上下文,而非简单的提示响应
- 规范驱动思维:将模糊需求转化为精确、可执行的规范
- 质量优先理念:每个阶段都确保高质量输出。
- 项目对齐能力:深度理解现有项目架构和约束
# 6A工作流执行规则
## 阶段1: Align (对齐阶段)
### 目标: 模糊需求 → 精确规范
### 执行步骤
1. **项目上下文分析**
- 分析现有项目结构、技术栈、架构模式、依赖关系
- 分析现有代码模式、现有文档和约定
- 理解业务域和数据模型
2. **需求理解确认**
- 创建 `docs/任务名/ALIGNMENT_[任务名].md`
- 包含项目和任务特性规范
- 包含原始需求、边界确认(明确任务范围)、需求理解(对现有项目的理解)、疑问澄清(存在歧义的地方)
3. **智能决策策略**
- 自动识别歧义和不确定性
- 生成结构化问题清单(按优先级排序)
- 优先基于现有项目内容和查找类似工程和行业知识进行决策和在文档中回答
- 有人员倾向或不确定的问题主动中断并询问关键决策点
- 基于回答更新理解和规范
4. **中断并询问关键决策点**
- 主动中断询问,迭代执行智能决策策略
5. **最终共识**
- 生成 `docs/任务名/CONSENSUS_[任务名].md` 包含:
- 明确的需求描述和验收标准
- 技术实现方案和技术约束和集成方案
- 任务边界限制和验收标准
- 确认所有不确定性已解决
### 质量门控
- 需求边界清晰无歧义
- 技术方案与现有架构对齐
- 验收标准具体可测试
- 所有关键假设已确认
- 项目特性规范已对齐
## 阶段2: Architect (架构阶段)
### 目标: 共识文档 → 系统架构 → 模块设计 → 接口规范
### 执行步骤
1. **系统分层设计**
- 基于CONSENSUS、ALIGNMENT文档设计架构
- 生成 `docs/任务名/DESIGN_[任务名].md` 包含:
- 整体架构图(mermaid绘制)
- 分层设计和核心组件
- 模块依赖关系图
- 接口契约定义
- 数据流向图
- 异常处理策略
2. **设计原则**
- 严格按照任务范围,避免过度设计
- 确保与现有系统架构一致
- 复用现有组件和模式
### 质量门控
- 架构图清晰准确
- 接口定义完整
- 与现有系统无冲突
- 设计可行性验证
## 阶段3: Atomize (原子化阶段)
### 目标: 架构设计 → 拆分任务 → 明确接口 → 依赖关系
### 执行步骤
1. **子任务拆分**
- 基于DESIGN文档生成 `docs/任务名/TASK_[任务名].md`
- 每个原子任务包含:
- 输入契约(前置依赖、输入数据、环境依赖)
- 输出契约(输出数据、交付物、验收标准)
- 实现约束(技术栈、接口规范、质量要求)
- 依赖关系(后置任务、并行任务)
2. **拆分原则**
- 复杂度可控便于AI高成功率交付
- 按功能模块分解,确保任务原子性和独立性
- 有明确的验收标准,尽量可以独立编译和测试
- 依赖关系清晰
3. **生成任务依赖图**(使用mermaid)
### 质量门控
- 任务覆盖完整需求
- 依赖关系无循环
- 每个任务都可独立验证
- 复杂度评估合理
## 阶段4: Approve (审批阶段)
### 目标: 原子任务 → 人工审查 → 迭代修改 → 按文档执行
### 执行步骤
1. **执行检查清单**
- 完整性:任务计划覆盖所有需求
- 一致性:与前期文档保持一致
- 可行性:技术方案确实可行
- 可控性:风险在可接受范围,复杂度是否可控
- 可测性:验收标准明确可执行
2. **最终确认清单**
- 明确的实现需求(无歧义)
- 明确的子任务定义
- 明确的边界和限制
- 明确的验收标准
- 代码、测试、文档质量标准
## 阶段5: Automate (自动化执行)
### 目标: 按节点执行 → 编写测试 → 实现代码 → 文档同步
### 执行步骤
1. **逐步实施子任务**
- 创建 `docs/任务名/ACCEPTANCE_[任务名].md` 记录完成情况
2. **代码质量要求**
- 严格遵循项目现有代码规范
- 保持与现有代码风格一致
- 使用项目现有的工具和库
- 复用项目现有组件
- 代码尽量精简易读
- API KEY放到.env文件中并且不要提交git
3. **异常处理**
- 遇到不确定问题立刻中断执行
- 在TASK文档中记录问题详细信息和位置
- 寻求人工澄清后继续
4. **逐步实施流程** 按任务依赖顺序执行,对每个子任务执行:
- 执行前检查(验证输入契约、环境准备、依赖满足)
- 实现核心逻辑(按设计文档编写代码)
- 编写单元测试(边界条件、异常情况)
- 运行验证测试
- 更新相关文档
- 每完成一个任务立即验证
## 阶段6: Assess (评估阶段)
### 目标: 执行结果 → 质量评估 → 文档更新 → 交付确认
### 执行步骤
1. **验证执行结果**
- 更新 `docs/任务名/ACCEPTANCE_[任务名].md`
- 整体验收检查:
- 所有需求已实现
- 验收标准全部满足
- 项目编译通过
- 所有测试通过
- 功能完整性验证
- 实现与设计文档一致
2. **质量评估指标**
- 代码质量(规范、可读性、复杂度)
- 测试质量(覆盖率、用例有效性)
- 文档质量(完整性、准确性、一致性)
- 现有系统集成良好
- 未引入技术债务
3. **最终交付物**
- 生成 `docs/任务名/FINAL_[任务名].md`(项目总结报告)
- 生成 `docs/任务名/TODO_[任务名].md`(精简明确哪些待办的事宜和哪些缺少的配置等,我方便直接寻找支持)
4. **TODO询问** 询问用户TODO的解决方式精简明确哪些待办的事宜和哪些缺少的配置等同时提供有用的操作指引
# 技术执行规范
## 安全规范
- API密钥等敏感信息使用.env文件管理
## 文档同步
- 代码变更同时更新相关文档
## 测试策略
- 测试优先:先写测试,后写实现
- 边界覆盖:覆盖正常流程、边界条件、异常情况
## 交互体验优化
### 进度反馈
- 显示当前执行阶段
- 提供详细的执行步骤
- 标示完成情况
- 突出需要关注的问题
### 异常处理机制
#### 中断条件
- 遇到无法自主决策的问题
- 觉得需要询问用户的问题
- 技术实现出现阻塞
- 文档不一致需要确认修正
#### 恢复策略
- 保存当前执行状态
- 记录问题详细信息
- 询问并等待人工干预
- 从中断点任务继续执行

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{"projectName":"trae_bindbox_game_p1uh"}

7
.vercelignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
build
dist
.git
.trae
.log
.figma

View File

@ -1,14 +1,14 @@
# Custom configuration | 独立配置
# Service name | 项目名称
SERVICE=minichat
SERVICE=bindboxgame
# Service name in specific style | 项目经过style格式化的名称
SERVICE_STYLE=minichat
SERVICE_STYLE=bindboxgame
# Service name in lowercase | 项目名称全小写格式
SERVICE_LOWER=minichat
SERVICE_LOWER=bindboxgame
# Service name in snake format | 项目名称下划线格式
SERVICE_SNAKE=minichat
SERVICE_SNAKE=bindbox
# Service name in snake format | 项目名称短杠格式
SERVICE_DASH=minichat
SERVICE_DASH=bindboxgame
# The project version, if you don't use git, you should set it manually | 项目版本如果不使用git请手动设置
VERSION=$(shell git describe --tags --always)

BIN
bin/server Executable file

Binary file not shown.

Binary file not shown.

View File

@ -34,6 +34,8 @@ eg :
```shell
# 根目录下执行
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/mini_chat?charset=utf8mb4&parseTime=True&loc=Local" -tables "log_operation,log_request,admin,app_keyword,app_keyword_reply,app_user,app_message_log,mini_program,mini_program_access_token"
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger"
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner"
```

View File

@ -4,12 +4,12 @@ import (
"net/http"
"strconv"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"WeChatService/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
"mini-chat/internal/repository/mysql/model"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
"go.uber.org/zap"
"gorm.io/gorm"

View File

@ -15,7 +15,7 @@ import (
"path/filepath"
"strings"
"mini-chat/internal/pkg/errors"
"bindbox-game/internal/pkg/errors"
"go.uber.org/zap"
"golang.org/x/tools/go/packages"

View File

@ -5,7 +5,7 @@ import (
_ "embed"
"io"
"mini-chat/internal/pkg/env"
"bindbox-game/internal/pkg/env"
"github.com/spf13/viper"
)
@ -15,28 +15,41 @@ var config = new(Config)
type Config struct {
MySQL struct {
Read struct {
Addr string `toml:"addr"`
User string `toml:"user"`
Pass string `toml:"pass"`
Name string `toml:"name"`
} `toml:"read"`
Addr string `mapstructure:"addr" toml:"addr"`
User string `mapstructure:"user" toml:"user"`
Pass string `mapstructure:"pass" toml:"pass"`
Name string `mapstructure:"name" toml:"name"`
} `mapstructure:"read" toml:"read"`
Write struct {
Addr string `toml:"addr"`
User string `toml:"user"`
Pass string `toml:"pass"`
Name string `toml:"name"`
} `toml:"write"`
} `toml:"mysql"`
Addr string `mapstructure:"addr" toml:"addr"`
User string `mapstructure:"user" toml:"user"`
Pass string `mapstructure:"pass" toml:"pass"`
Name string `mapstructure:"name" toml:"name"`
} `mapstructure:"write" toml:"write"`
} `mapstructure:"mysql" toml:"mysql"`
JWT struct {
AdminSecret string `toml:"admin_secret"`
PatientSecret string `toml:"patient_secret"`
DoctorSecret string `toml:"doctor_secret"`
} `toml:"jwt"`
AdminSecret string `mapstructure:"admin_secret" toml:"admin_secret"`
PatientSecret string `mapstructure:"patient_secret" toml:"patient_secret"`
DoctorSecret string `mapstructure:"doctor_secret" toml:"doctor_secret"`
} `mapstructure:"jwt" toml:"jwt"`
Language struct {
Local string `toml:"local"`
} `toml:"language"`
Local string `mapstructure:"local" toml:"local"`
} `mapstructure:"language" toml:"language"`
Wechat struct {
AppID string `mapstructure:"app_id" toml:"app_id"`
AppSecret string `mapstructure:"app_secret" toml:"app_secret"`
} `mapstructure:"wechat" toml:"wechat"`
COS struct {
Bucket string `mapstructure:"bucket" toml:"bucket"`
Region string `mapstructure:"region" toml:"region"`
SecretID string `mapstructure:"secret_id" toml:"secret_id"`
SecretKey string `mapstructure:"secret_key" toml:"secret_key"`
BaseURL string `mapstructure:"base_url" toml:"base_url"`
} `mapstructure:"cos" toml:"cos"`
}
var (

View File

@ -24,13 +24,13 @@ const (
EnUS = "en-us"
// CustomerProjectNameZh 客户项目中文名称
CustomerProjectNameZh = "微聊"
CustomerProjectNameZh = "盲盒游戏"
// CustomerProjectNameEn 客户项目英文名称
CustomerProjectNameEn = "Mini Chat"
CustomerProjectNameEn = "Bindbox Game"
// CustomerProjectVersion 客户项目版本
CustomerProjectVersion = "Release-2025101601"
CustomerProjectVersion = "Release-2025111111"
)
// GetResourcesFilePath 获取资源文件路径

View File

@ -3,15 +3,27 @@ local = 'zh-cn'
[mysql.read]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
name = 'mini_chat'
name = 'bindbox_game'
pass = 'api2api..'
user = 'root'
[mysql.write]
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
name = 'mini_chat'
name = 'bindbox_game'
pass = 'api2api..'
user = 'root'
[jwt]
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
[wechat]
app_id = "wx26ad074017e1e63f"
app_secret = "026c19ce4f3bb090c56573024c59a8be"
[cos]
bucket = "keaiya-1259195914"
region = "ap-shanghai"
secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
# 可选:如有 CDN/自定义域名则填写,否则留空
base_url = ""

0
data.db Normal file
View File

209
development_standards.md Normal file
View File

@ -0,0 +1,209 @@
# 开发规范与问题记录
## 项目概述
本项目基于 Vue 3 + TypeScript + Element Plus + Vite 技术栈开发的管理系统。
## 已发现的问题与解决方案
### 1. 时间字段处理错误
**问题描述**
创建道具卡时出现数据库错误:`Error 1292 (22007): Incorrect date value: '0000-00-00' for column 'valid_start' at row 1`
**问题原因**
- 当用户没有选择有效期时,代码仍然尝试提交时间字段
- 时间数据格式不正确或包含无效值
- 后端数据库不接受'0000-00-00'这样的无效日期
**解决方案**
```typescript
// 处理有效期 - 只有当时间范围有效时才添加时间字段
if (validTimeRange.value && validTimeRange.value.length === 2 &&
validTimeRange.value[0] && validTimeRange.value[1]) {
try {
const startTime = new Date(validTimeRange.value[0]).getTime()
const endTime = new Date(validTimeRange.value[1]).getTime()
// 验证时间是否有效
if (!isNaN(startTime) && !isNaN(endTime) && startTime > 0 && endTime > 0) {
submitData.valid_start_unix = Math.floor(startTime / 1000)
submitData.valid_end_unix = Math.floor(endTime / 1000)
}
} catch (error) {
console.error('时间处理错误:', error)
}
}
```
**开发规范**
1. **时间字段处理**
- 必须验证时间数据的有效性
- 使用`isNaN()`检查时间戳是否为有效数字
- 确保时间值大于0
- 添加try-catch处理可能的异常
2. **数据提交原则**
- 只提交有效和必要的数据字段
- 避免提交null、undefined或无效的时间数据
- 对可选字段进行严格的有效性检查
### 2. 价格字段数据类型错误
**问题描述**
创建道具卡时出现错误:`json: cannot unmarshal null into Go struct field createItemCardRequest.price of type int64`
**解决方案**
```typescript
// 确保价格字段存在且为数字类型
if (submitData.price === undefined || submitData.price === null) {
submitData.price = 0
} else {
submitData.price = Number(submitData.price)
}
```
### 3. 组件结构错误
**问题描述**
道具卡管理页面按钮不显示,原因是错误的组件插槽使用
**解决方案**
使用独立的按钮区域布局,避免错误的插槽使用:
```vue
<!-- 搜索栏 -->
<ArtSearchBar />
<!-- 表格头部和按钮 -->
<div class="table-header-wrapper">
<div class="header-actions">
<el-button type="primary" @click="handleCreate">
新增道具卡
</el-button>
</div>
</div>
<!-- 数据表格 -->
<ArtTable />
```
### 4. 数据枚举显示问题
**问题描述**
状态、类型、范围、效果等字段显示为数字而不是对应的文本标签
**解决方案**
1. 增强枚举映射函数的空值处理
2. 完善状态字段的多状态判断
3. 添加数据类型检查
```typescript
// 增强的枚举映射函数
const getCardTypeLabel = (type: number) => cardTypeMap[type as keyof typeof cardTypeMap] || '未知'
// 完善的状态显示
{{ row.status === 1 ? '启用' : row.status === 2 ? '禁用' : '未知' }}
// 空值处理
{{ getCardTypeLabel(row.card_type || 0) }}
```
### 5. API路径404错误
**问题描述**
优惠券API请求返回404错误`admin/system_coupons` 路径不存在
**问题原因**
后端API路径可能不存在或需要调整
**解决方案**
1. 检查实际的API路径配置
2. 对比道具卡API路径确认一致性
3. 添加详细的请求调试信息
4. 检查代理配置是否正确
### 6. 数据枚举和操作按钮显示问题
**问题描述**
道具卡管理页面中:
- 状态、类型、范围、效果等字段显示为数字而不是对应的文本标签
- 操作栏(编辑、删除按钮)不显示
**问题原因**
1. 枚举映射函数没有正确处理空值
2. 使用了错误的按钮组件(原生的`el-button`而不是项目封装的`ArtButtonTable`
3. 插槽模板语法正确但组件渲染有问题
**解决方案**
1. **枚举显示修复**
```vue
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : row.status === 2 ? '禁用' : '未知' }}
</el-tag>
</template>
<template #card_type="{ row }">
<el-tag>{{ getCardTypeLabel(row.card_type || 0) }}</el-tag>
</template>
```
2. **操作按钮修复**
```vue
<template #actions="{ row }">
<ArtButtonTable type="edit" @click="handleEdit(row)" />
<ArtButtonTable type="delete" @click="handleDelete(row)" />
</template>
```
3. **组件导入**
```typescript
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
```
**开发规范**
1. **组件使用规范**
- 优先使用项目封装的组件(如`ArtButtonTable`而不是原生Element Plus组件
- 保持与其他页面一致的UI风格和交互体验
- 使用项目统一的图标和样式系统
2. **枚举显示规范**
- 所有状态字段必须提供完整的枚举映射
- 处理空值和异常值情况,提供默认值
- 使用统一的标签样式和颜色规范
3. **调试和日志**
- 添加详细的数据结构检查日志
- 验证枚举映射函数的正确性
- 检查组件渲染和插槽绑定的正确性
## 前端开发规范
### 1. 表单数据处理
- 提交前必须清理数据移除undefined和null值
- 对数字类型字段进行类型转换和验证
- 时间字段必须验证有效性
- 可选字段需要条件判断
### 2. 错误处理
- 所有API调用必须添加try-catch异常处理
- 用户友好的错误提示信息
- 控制台记录详细错误信息便于调试
### 3. 代码结构
- 保持组件单一职责原则
- 复杂的表单处理逻辑抽取为独立函数
- 添加必要的注释说明业务逻辑
### 4. 数据验证
- 前端表单必须进行完整的验证
- 后端返回的错误需要友好处理
- 特殊字段(如时间、价格)需要额外验证
## 后端接口规范
- 明确字段类型要求如int64
- 提供清晰的错误信息和错误码
- 对无效数据给出具体的错误描述
## 数据库规范
- 不允许插入'0000-00-00'等无效日期
- 时间字段必须有默认值或允许为null
- 数字类型字段不能接收字符串类型
---
**最后更新**2025年11月14日
**文档维护**:开发团队

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

539
docs/开发规范.md Normal file
View File

@ -0,0 +1,539 @@
# Go API 开发规范
## 1. 概述
### 1.1 目的
本文档旨在为 `bindbox-game` 项目制定一套统一、清晰的 Go API 开发规范。通过标准化代码结构、命名约定、错误处理和文档注释,我们致力于提高代码的可读性、可维护性和团队协作效率,并从根本上避免之前开发中遇到的问题(如包导入不一致、特殊字符使用不当等)。
### 1.2 适用范围
本规范主要适用于项目 `internal/api/` 目录下的所有 API 层的开发工作。所有新的 API 模块以及对现有模块的修改,都必须严格遵守本规范。
### 1.3 核心原则
* **一致性**:所有代码都应遵循相同的模式和风格。
* **清晰性**:代码应易于阅读和理解,避免复杂的或晦涩的实现。
* **可维护性**:代码结构应清晰,便于未来扩展和重构。
* **api/service**代码结构应清晰api 和 service 应该保持一致的命名规范。必须: 一个 api 对应一个 service。一个函数功能对应一个 service 方法。单独一个文件
* **接口文档注释**:所有 API 端点都必须包含详细的 Swagger 注释,描述请求参数、响应体、可能的错误码等。
* **测试功能**:所有 API 端点都必须包含详细的测试用例,确保功能的正确性和稳定性。(cmd/testchain/ 中写)
---
## 2. 目录与文件结构
规范的目录结构是项目可维护性的基础。
* **模块化目录**:每一个独立的业务功能模块都应在 `internal/api/` 下创建一个专属目录。
```
internal/api/
├── admin/
├── activity/
└── ... (其他模块)
```
* **Handler 结构体定义**:在每个模块目录下,应有一个与模块同名的 `go` 文件(如 `admin.go`),用于定义该模块的 `handler` 结构体和 `New` 初始化函数Handler 不直接进行数据库读写)。
```go
// internal/api/admin/admin.go
package admin
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query // 仅用于注入(避免在 Handler 直接使用)
readDB *dao.Query // 仅用于注入(避免在 Handler 直接使用)
svc adminsvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
svc: adminsvc.New(logger, db),
}
}
```
* **端点文件分离**:每个 API 端点Endpoint的实现应放在一个独立的 `go` 文件中,文件名应清晰描述其功能,示例对齐当前 `admin` 模块:
```
internal/api/admin/
├── admin.go // Handler 定义
├── login.go // 管理员登录
├── admin_create.go // 新增客服
├── admin_modify.go // 编辑客服(含路径参数)
├── admin_delete.go // 删除客服(批量)
└── admin_list.go // 客服列表(分页查询)
```
---
## 3. 命名规范
统一的命名规范能极大提升代码的可读性。
* **Go 文件**:使用小写蛇形命名法 (`snake_case`),如 `admin_create.go`
* **Handler 函数**:使用大驼峰命名法 (`PascalCase`),且函数名应清晰表达其动作,如 `CreateAdmin()`
* **请求/响应结构体**:使用小驼峰命名法 (`camelCase`),并以 `Request`/`Response` 作为后缀,如 `createAdminRequest`, `createAdminResponse`
---
## 4. API Handler 规范
API Handler 是业务逻辑的入口,其规范性至关重要。
### 4.1 函数签名
所有 API Handler 函数都必须返回 `core.HandlerFunc` 类型。这是框架的核心设计,用于统一处理请求上下文。
```go
// CreateAdmin 新增客服
func (h *handler) CreateAdmin() core.HandlerFunc {
return func(ctx core.Context) {
// ... 实现
}
}
```
### 4.2 参数绑定与校验
* **绑定JSON**POST/PUT 等带请求体的接口使用 `ctx.ShouldBindJSON(req)` 绑定到预定义的 `Request` 结构体。
* **绑定Query**GET 查询接口使用 `ctx.ShouldBindForm(req)` 绑定到预定义的 `Request` 结构体(字段使用 `form:"..."` tag
* **路径参数**:通过 `ctx.Param("id")` 等读取路径参数并进行类型转换与校验。
* **校验**:绑定方法会自动执行 `binding` tag 定义的校验规则。如果校验失败,必须立即中断请求并返回错误。
```go
req := new(createAdminRequest)
res := new(createAdminResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
```
```go
// GET 查询绑定示例(与 admin_list 对齐)
req := new(listRequest)
res := new(listResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
```
```go
// 路径参数读取示例(与 admin_modify 对齐)
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID"),
)
return
}
```
### 4.3 响应处理
* **成功响应**:当业务逻辑成功执行后,必须使用 `ctx.Payload(res)` 方法来返回标准的成功响应。`res` 是预定义的 `Response` 结构体实例。
```go
res.Message = "操作成功"
ctx.Payload(res)
```
* **错误响应**:见第 6 章“错误处理规范”。
---
## 5. 数据传输对象 (DTO) 规范
DTO (Data Transfer Object) 是 API 契约的直接体现。
* **结构体定义**:每个 API 都应定义清晰的 `Request``Response` 结构体。
* **字段注释**:所有结构体字段都必须有清晰的中文注释,解释其含义和用途。
* **JSON Tag**:所有字段都必须包含 `json:"..."` tag以确保与前端交互的 JSON 字段名一致。
* **Validation Tag**:对于 `Request` 结构体中需要校验的字段,必须添加 `binding:"..."` tag 来定义校验规则(如 `required`)。
```go
// internal/api/admin/admin_create.go
type createAdminRequest struct {
UserName string `json:"username" binding:"required"` // 用户名
NickName string `json:"nickname" binding:"required"` // 昵称
Password string `json:"password" binding:"required"` // 密码
Mobile string `json:"mobile"` // 手机号 (非必填)
}
type createAdminResponse struct {
Message string `json:"message"` // 提示信息
}
```
```go
// internal/api/admin/admin_list.go
// GET 查询示例(字段使用 form 标签)
type listRequest struct {
Username string `form:"username"` // 用户名
Nickname string `form:"nickname"` // 昵称
Page int `form:"page"` // 当前页码,默认 1
PageSize int `form:"page_size"` // 每页返回的数据量,最多 100 条
}
type listData struct {
ID int32 `json:"id"`
UserName string `json:"username"`
NickName string `json:"nickname"`
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type listResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []listData `json:"list"`
}
```
---
## 6. 错误处理规范
统一的错误处理是保证 API 健壮性和可预测性的关键。
* **统一入口**:所有在 Handler 中捕获的错误(参数校验失败、数据库操作失败、权限不足等)都必须通过 `ctx.AbortWithError()` 函数来处理。
* **`core.Error` 结构**`ctx.AbortWithError` 接收一个 `core.Error` 对象,该对象封装了错误的三个核心要素:
1. **HTTP 状态码**:如 `http.StatusBadRequest`
2. **业务错误码**:在 `internal/code/` 中预定义的 `code.Code`,如 `code.CreateAdminError`
3. **详细错误信息**:一个描述错误具体原因的字符串。
* **未找到记录**:当数据库查询返回 `gorm.ErrRecordNotFound` 时,需要按具体业务错误码返回明确的提示信息(如“账号不存在”)。
* **权限校验失败**:当 `ctx.SessionUserInfo().IsSuper != 1` 时,禁止操作,返回对应业务错误码和提示“禁止操作”。
```go
// 示例:用户已存在
if info != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "该账号已存在")),
)
return
}
```
```go
// 示例:未找到记录(与 admin_modify/login 对齐)
if err == gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), "账号不存在,请联系管理员。")),
)
return
}
```
```go
// 示例:权限校验失败(与 create/modify/delete/list 对齐)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "禁止操作")),
)
return
}
```
---
### 6.1 数据库上下文与读写分离Service 层)
* **上下文传递**:所有数据库操作必须调用 `WithContext(ctx)`Service 层接收 `context.Context`)。
* **读写分离**:读取使用 `s.readDB`,写入使用 `s.writeDB`
* **分页查询会话**:分页列表与计数需要分别创建独立会话,避免互相影响。
```go
query := s.readDB.Admin.WithContext(ctx)
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.Order(s.readDB.Admin.ID.Desc()).Limit(in.PageSize).Offset((in.Page-1)*in.PageSize).Find()
if err != nil { /* 错误处理 */ }
count, err := countQueryDB.Count()
if err != nil { /* 错误处理 */ }
```
---
## 7. 日志记录规范
虽然在 `admin` 模块的示例中未显式大量使用,但规范的日志记录对于问题排查至关重要。
* **使用注入的 Logger**:应通过 `handler` 结构体中注入的 `h.logger` 实例来记录日志。
* **记录关键信息**:在关键业务节点或错误发生时,记录必要的上下文信息,如请求参数、用户 ID 等。
```go
// 推荐实践 (示例)
h.logger.Error("创建管理员失败", zap.Error(err), zap.String("username", req.UserName))
```
---
## 8. 注释与文档 (Swagger)
API 的可发现性和易用性依赖于完善的文档。
* **强制 Swagger 注解**:每个 API Handler 函数上方都必须添加完整的 Swagger 注解块。
* **注解内容**:必须包含以下核心注解:
* `@Summary`:一句话功能简介。
* `@Description`:更详细的功能描述。
* `@Tags`API 分组标签,便于在 Swagger UI 中分类。
* `@Accept` / `@Produce`:通常为 `json`
* `@Param`:描述请求参数,包括参数位置(`body`)、类型、是否必需和说明。
* `@Success`:描述成功响应的结构。
* `@Failure`:描述可能发生的错误响应。
* `@Router`API 的路由路径和 HTTP 方法。
* `@Security`:如果接口需要认证,需添加此注解。
```go
// internal/api/admin/admin_create.go
// CreateAdmin 新增客服
// @Summary 新增客服
// @Description 新增客服
// @Tags 管理端.客服管理
// @Accept json
// @Produce json
// @Param RequestBody body createAdminRequest true "请求参数"
// @Success 200 {object} createAdminResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/create [post]
// @Security LoginVerifyToken
func (h *handler) CreateAdmin() core.HandlerFunc { /* ... */ }
// internal/api/admin/admin_list.goGET 查询示例)
// @Param username query string false "用户名"
// @Param nickname query string false "昵称"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Router /api/admin/list [get]
// @Security LoginVerifyToken
// internal/api/admin/admin_modify.go路径参数示例
// @Param id path string true "编号ID"
// @Router /api/admin/{id} [put]
// @Security LoginVerifyToken
// internal/api/admin/login.go登录接口不加 Security
// @Router /api/admin/login [post]
```
---
## 9. 路由与中间件规范
路由是 API 对外的入口,必须与模块职责、认证策略保持一致,并在 `internal/router/router.go` 统一注册。
### 9.1 分组与路径前缀
- **管理端**:统一前缀 `"/api/admin"`
- 非认证组:用于登录、系统状态等不需要鉴权的接口。
- 认证组:使用鉴权中间件保护,适用于敏感操作(新增、编辑、删除、列表)。
- **用户端APP**:统一前缀 `"/api/app"`,用于对外业务接口(如活动相关)。
### 9.2 鉴权中间件
- **封装方式**:通过 `core.WrapAuthHandler(interceptor.AdminTokenAuthVerify)` 将会话信息写入上下文,认证失败统一返回业务错误。
- **接口签名**`internal/router/interceptor/interceptor.go:12-18` 定义了鉴权接口:
`AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)`
- **实现参考**:管理端鉴权实现见 `internal/router/interceptor/admin_auth.go:1-80`
### 9.3 路由注册示例(与当前 admin 模块对齐)
```go
// internal/router/router.go
// 管理端非认证接口路由组(登录)
adminNonAuthApiRouter := mux.Group("/api/admin")
{
adminNonAuthApiRouter.GET("/license/status", func(ctx core.Context) { /* ... */ })
adminNonAuthApiRouter.POST("/login", adminHandler.Login())
}
// 管理端认证接口路由组(需要 Authorization
adminAuthApiRouter := mux.Group("/api/admin", core.WrapAuthHandler(intc.AdminTokenAuthVerify))
{
adminAuthApiRouter.POST("/create", adminHandler.CreateAdmin())
adminAuthApiRouter.PUT("/:id", adminHandler.ModifyAdmin())
adminAuthApiRouter.POST("/delete", adminHandler.DeleteAdmin())
adminAuthApiRouter.GET("/list", adminHandler.PageList())
}
// APP 端接口路由组
appApiRouter := mux.Group("/api/app")
{
appHandler := app.New(logger, db)
appApiRouter.GET("/activities", appHandler.ListActivities())
// ... 其他 APP 端接口
}
```
### 9.4 Swagger 安全定义与使用
- **安全定义位置**`main.go:23-26` 定义认证方案 `LoginVerifyToken`,从请求头读取 `Authorization`
- **注解使用规范**
- 管理端认证组接口均需添加 `@Security LoginVerifyToken`
- 登录接口不需要添加 `@Security` 注解。
### 9.5 路径命名与语义
- **资源语义**
- `POST /api/admin/create`:创建资源(支持复杂校验与默认值设置)。
- `PUT /api/admin/:id`:更新单个资源,路径参数 `id` 必须有效。
- `POST /api/admin/delete`:批量删除,参数 `ids` 以逗号分隔。
- `GET /api/admin/list`:分页检索,`page`/`page_size` 合理限制(最大 100
- **用户端**:保持 RESTful 风格,如 `GET /api/app/activities``GET /api/app/activities/:activity_id`
### 9.6 指标与别名(可选)
- 为避免路径参数导致指标维度爆炸,可在 Handler 前添加别名记录指标:
```go
// 例如:为 /api/app/activities/:activity_id 设置指标记录别名
appApiRouter.GET("/activities/:activity_id",
core.AliasForRecordMetrics("/api/app/activities/:activity_id"),
appHandler.GetActivityDetail(),
)
```
---
## 10. 服务层分离规范
为避免 Handler 同时承担传输与业务职责,必须在 `internal/service/{module}` 引入服务层,统一承载业务规则、数据协作与事务处理。
### 10.1 目录与命名
- 目录:`internal/service/admin``internal/service/activity` 等按模块划分。
- 包名与模块名一致Handler 导入时使用别名避免冲突(如 `svc "bindbox-game/internal/service/admin"`)。
### 10.1.1 文件拆分(与 API 端点保持一致)
- 每个端点在服务层对应一个实现文件,避免所有方法堆叠在单文件:
```
internal/service/admin/
├── service.go // 接口、构造
├── login.go // 登录
├── create.go // 新增客服
├── modify.go // 编辑客服
├── delete.go // 删除客服
└── list.go // 客服列表
```
- 规则:每个端点的输入/输出类型在对应文件中定义(例如 `LoginInput`/`LoginResult``login.go`)。
---
## 11. 流程规范Router → API → Service
### 11.1 职责边界
- Router负责分组、认证拦截与端点注册不承载业务逻辑。
- 管理端非认证路由:`internal/router/router.go:44-51`
- 管理端认证路由(鉴权中间件):`internal/router/router.go:53-60`
- 用户端 APP 路由:`internal/router/router.go:62-72`
- APIHandler负责参数绑定、权限校验、错误码映射与响应输出不直接操作数据库。
- 绑定参数并校验:如登录 `internal/api/admin/login.go:35-45`、列表 `internal/api/admin/admin_list.go:57-64`
- 权限校验:如列表 `internal/api/admin/admin_list.go:83-90`、创建 `internal/api/admin/admin_create.go:49-56`
- 错误码映射与响应:如登录 `internal/api/admin/login.go:53-59,61-64`
- Service承载领域逻辑、数据访问与事务输入/输出类型在对应端点文件中定义。
- 登录实现:`internal/service/admin/login.go:16-52`
- 创建实现:`internal/service/admin/admin_create.go:14-42`
- 编辑实现:`internal/service/admin/admin_modify.go:13-50`
- 删除实现:`internal/service/admin/admin_delete.go:9-15`
- 列表实现:`internal/service/admin/admin_list.go:12-38`
### 11.2 端到端流程(以管理端为例)
- Router 接收请求并路由到对应分组:非认证或认证(携带 `Authorization`)。
- 若为认证接口,鉴权中间件注入会话信息:`internal/router/interceptor/admin_auth.go:1-80`
- Handler
- `ShouldBindJSON``ShouldBindForm` 解析请求(含路径参数 `ctx.Param`)。
- 权限检查(如 `ctx.SessionUserInfo().IsSuper`)。
- 将 DTO 转换为 Service 输入,调用 `svc.*(ctx.RequestContext(), input)`
- 根据 Service 返回的错误映射业务错误码,或输出成功响应 `ctx.Payload(res)`
- Service
- 使用 `dao.Query.WithContext` 与读写分离实现查询或写入。
- 必要场景内使用事务保障原子性(参考 `dao.gen.go``Query.Transaction`)。
### 11.3 错误处理策略
- Service 返回语义化错误信息(`error`);不直接决定 HTTP 或业务码。
- Handler 统一映射到 `core.Error(httpCode, code.* , message)``AbortWithError`
- 登录错误映射:`internal/api/admin/login.go:53-59`
- 创建错误映射:`internal/api/admin/admin_create.go:66-71`
- 编辑错误映射:`internal/api/admin/admin_modify.go:87-93`
- 删除错误映射:`internal/api/admin/admin_delete.go:95-102`
- 列表错误映射:`internal/api/admin/admin_list.go:98-105`
### 11.4 数据绑定与类型转换
- 登录:`loginRequest``LoginInput` 映射:`internal/api/admin/login.go:47-51`
- 创建:`createAdminRequest``CreateInput` 映射:`internal/api/admin/admin_create.go:58-65`
- 编辑:`modifyAdminRequest``ModifyInput` 映射:`internal/api/admin/admin_modify.go:79-86`
- 列表:`listRequest(form)``ListInput` 映射:`internal/api/admin/admin_list.go:92-97`
- 删除:解析 `ids` 列表后直接传入 `svc.Delete``internal/api/admin/admin_delete.go:95`
### 11.5 事务与读写分离
- 读操作统一使用 `readDB`,写操作统一使用 `writeDB`
- 所有 DAO 调用必须附带上下文:`WithContext(ctx)`
- 多步骤写操作建议在 Service 层使用事务(`Query.Transaction``internal/repository/mysql/dao/gen.go:211-213`
### 11.6 鉴权与会话
- 路由层通过 `core.WrapAuthHandler(intc.AdminTokenAuthVerify)` 进行会话注入:`internal/router/router.go:53-60`
- 鉴权逻辑参考:`internal/router/interceptor/admin_auth.go:1-80`
- Handler 使用 `ctx.SessionUserInfo()` 获取操作者信息(如 `CreatedBy``UpdatedBy`)。
### 11.7 命名与拆分
- API 与 Service 均按端点拆分文件,命名对齐,便于定位与维护。
- Service 的输入/输出类型在对应端点文件定义(如 `login.go` 定义 `LoginInput/LoginResult`)。
---
## 12. Repository 规范
### 12.1 层职责
- 负责数据访问CRUD、分页、事务不包含业务规则与 HTTP 细节。
- 仅依赖底层 DAO/Model向 Service 层提供稳定的数据操作能力。
### 12.2 目录与组件
- 目录:`internal/repository/mysql`
- `dao/`:由 `gorm/gen` 生成的查询对象与读写分离封装,参考:
- 读写分离方法:`internal/repository/mysql/dao/gen.go:135-143`
- 事务封装:`internal/repository/mysql/dao/gen.go:211-213`
- 上下文绑定:`internal/repository/mysql/dao/gen.go:188-209`
- `model/`:数据库对应的实体结构体(字段、标签)。
### 12.3 使用规范(在 Service 层)
- 初始化查询对象:
- 读库:`readDB := dao.Use(db.GetDbR())`
- 写库:`writeDB := dao.Use(db.GetDbW())`
- 所有操作必须绑定上下文:`WithContext(ctx)`,示例(查询管理员):
```go
info, err := s.readDB.Admin.WithContext(ctx).
Where(s.readDB.Admin.Username.Eq(in.Username)).
First()
```
- 分页与计数使用独立会话,避免互相影响:
```go
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
items, _ := listQueryDB.Order(...).Limit(...).Offset(...).Find()
total, _ := countQueryDB.Count()
```
- 多步骤写操作建议在事务中完成:
```go
_ = s.writeDB.Transaction(func(tx *dao.Query) error {
// 在 tx 上执行写操作
return nil
})
```
### 12.4 分层建议
- 如需进一步解耦 DAO可在 `internal/repository/{module}` 定义模块级仓库接口(如 `adminrepo`),内部封装 DAO 细节Service 只依赖仓库接口,便于单元测试与替换数据源。
- 目前项目直接在 Service 层使用 DAO符合轻量项目场景当跨表协作复杂时可引入仓库接口以降低耦合。

View File

@ -0,0 +1,40 @@
# ALIGNMENT: 开发规范
## 1. 原始需求
根据用户指令,需要完成以下任务:
1. **学习现有代码**:参考 `/internal/api/admin` 目录下的代码实现。
2. **制定开发规范**:总结并编写一份项目开发规范文档。
3. **核心要求**
* 新代码必须导入对应的包。
* 遵循项目已有的代码风格和结构。
* 所有的注释都必须是中文。
## 2. 边界确认
* **输入**
* 项目源码,特别是 `/internal/api/admin` 目录。
* 用户的口头指令和规范要求。
* **输出**
* 一份名为 `docs/开发规范.md` 的 Markdown 文档。
* 文档内容需清晰、准确,能作为后续开发的指导。
* **范围**
* 本次任务的核心是**学习和文档化**,不涉及新的功能开发或代码修改。
* 规范内容应主要涵盖 API 层Handler的编写风格包括但不限于
* 目录结构和文件命名。
* 请求/响应数据结构DTO定义。
* Swagger 注解的使用。
* `core.HandlerFunc` 的实现模式。
* 参数绑定和校验。
* 错误处理机制。
* 日志记录。
## 3. 需求理解
用户希望通过分析现有成熟的代码(`/internal/api/admin`),提炼出一套适用于本项目所有模块开发的统一规范。这套规范旨在解决之前开发过程中遇到的问题(如包导入不全、特殊字符使用不当),并确保未来代码的一致性、可读性和可维护性。
这本质上是一个**逆向工程和标准化**的过程:从已有实现中总结出最佳实践,并将其固化为团队必须遵守的规则。
## 4. 疑问澄清
目前没有需要立即澄清的疑问。我将首先分析代码,然后根据分析结果来组织规范文档的结构和内容。如果在此过程中发现任何歧义或需要决策的地方,我会再提出。

View File

@ -0,0 +1,146 @@
# APP 端开发统一规范
## 1. 目标
统一 APP 端接口的目录结构、编码范式、错误处理与文档注释确保与管理端admin 模块)保持一致的风格与契约。
## 2. 目录结构
- 模块放置:`internal/api/activity/`
- 每个端点单文件:
- `activity_create.go`
- `activity_issue_add.go`
- `activity_list.go`
- `activity_detail.go`
- `activity_issues_list.go`
- `issue_rewards_list.go`
## 3. Handler 初始化
- 在模块根定义 `handler``New(logger, db)`,注入 `writeDB`/`readDB`
- 路由绑定示例:`appHandler := app.New(logger, db)`,方法统一返回 `core.HandlerFunc`
## 4. 请求/响应范式
- 所有接口必须定义 `Request``Response` 结构体
- 成功统一:`ctx.Payload(res)``res.Message` 使用统一文案 `操作成功`
## 5. 参数绑定与校验
- JSON`ctx.ShouldBindJSON(req)`
- 表单/查询:`ctx.ShouldBindForm(req)`
- 绑定错误:`ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))`
## 6. 业务错误处理
- 统一:`ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))`
- 业务码:在 `internal/code/` 中约定并使用具体业务码
## 7. 分页与过滤规范
- 分页:`page` 默认 1、`page_size` 默认 20、最大 100超过返回参数错误
- 过滤:字符串使用 `Like("%xxx%")`,数值/枚举使用 `Eq(...)``is_boss` 仅允许 `0/1`
## 8. 路径参数规范
- 使用 `strconv.Atoi` 并校验 `> 0`,非法返回 `ParamBindError`
## 9. 时间规范
- 入参解析:`timeutil.ParseCSTInLocation`
- 响应时间:`FriendlyTime` 或固定格式化 `2006-01-02 15:04`
## 10. Swagger 注释规范
- 标签APP 端统一使用 `@Tags APP端.活动`(或模块名)
- 必填注释:`@Summary/@Description/@Accept/@Produce/@Param/@Success/@Failure/@Router`
## 11. 统一 Handler 模板(示例)
以下示例来自 `internal/api/activity/activity_issues_list.go:46-127`,作为 APP 端列表接口标准参考:
```go
// ListActivityIssues 活动期列表
// @Summary 活动期列表
// @Description 获取指定活动的期列表,支持分页
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path int true "活动ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listIssuesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id}/issues [get]
func (h *handler) ListActivityIssues() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("activity_id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"活动ID无效"),
)
return
}
req := new(listIssuesRequest)
res := new(listIssuesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 { req.Page = 1 }
if req.PageSize == 0 { req.PageSize = 20 }
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"每页最多100条"),
)
return
}
query := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).
Where(h.readDB.ActivityIssues.ActivityID.Eq(int64(id)))
listQuery := query
countQuery := query
issues, err := listQuery.Order(h.readDB.ActivityIssues.ID.Desc()).
Limit(req.PageSize).Offset((req.Page-1)*req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
err.Error()),
)
return
}
total, err := countQuery.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ServerError,
err.Error()),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]issueListItem, len(issues))
for i, v := range issues {
res.List[i] = issueListItem{
ID: v.ID,
IssueNumber: v.IssueNumber,
Status: v.Status,
Sort: v.Sort,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
UpdatedAt: timeutil.FriendlyTime(v.UpdatedAt),
}
}
ctx.Payload(res)
}
}
```
## 12. 文档生成与预览
- 生成:`make gen-swagger`
- 预览:`make serve-swagger`(默认端口 `36666`,访问 `http://localhost:36666/docs`

View File

@ -0,0 +1,38 @@
# CONSENSUS: 开发规范
## 1. 需求描述和验收标准
**需求描述**
基于对 `/internal/api/admin` 模块代码的分析,制定一套清晰、统一的 Go API 开发规范。该规范将作为项目后续所有 API 开发的强制标准,以确保代码质量、一致性和可维护性。
**验收标准**
1. 生成一份完整的 `docs/开发规范.md` 文档。
2. 文档内容必须准确反映从 `admin` 模块总结出的最佳实践。
3. 规范应覆盖从 API 定义、实现到错误处理的全过程。
4. 规范应明确代码风格、目录结构、命名约定等关键要素。
5. 所有示例代码必须符合规范本身,并能作为开发者参考的模板。
## 2. 技术实现方案
将遵循以下步骤来产出最终的规范文档:
1. **结构设计**将规范文档分为不同章节如“目录结构”、“命名规范”、“API Handler 规范”、“数据传输对象 (DTO)”、“错误处理”等。
2. **内容撰写**:在每个章节中,详细描述具体的规则,并提供从 `admin` 模块中提取的**正例代码**作为示范。
3. **规则提炼**:将观察到的模式提炼为必须遵守的规则,例如:
* 每个 API Handler 必须返回 `core.HandlerFunc`
* 使用 `ctx.ShouldBindJSON()` 进行参数绑定和基础校验。
* 通过 `ctx.AbortWithError()` 统一处理错误。
* 使用 `ctx.Payload()` 返回成功响应。
* 请求和响应结构体必须包含 `json` tag并提供清晰的字段注释。
* 必须为每个 API 编写完整的 Swagger 注解。
4. **文档生成**:将所有内容整合到 `docs/开发规范.md` 文件中。
## 3. 任务边界限制
* **仅限于文档**:此任务不涉及任何代码的创建、修改或删除。
* **聚焦于 API 层**:规范主要针对 `internal/api` 层的开发,对于 `service` 层和 `repository` 层的规范,虽然可以借鉴其思想,但不是本次任务的重点。
* **基于现有实践**:所有规范都必须源于对现有 `admin` 代码的分析,不允许凭空创造规则。
## 4. 确认所有不确定性已解决
通过 `ALIGNMENT` 阶段的分析,所有关于任务目标、范围和方法的不确定性均已解决。现在可以进入 `Architect`(架构)阶段,设计最终规范文档的详细结构。

View File

@ -0,0 +1,81 @@
# DESIGN: 开发规范文档设计
本文档定义了最终 `docs/开发规范.md` 的结构和核心内容,旨在为后续的文档撰写提供清晰的蓝图。
## 1. 整体结构
最终的开发规范文档将分为以下几个核心章节,每个章节都将包含详细的规则描述和代码示例。
```mermaid
graph TD
A[开发规范] --\u003e B[1. 概述];
A --\u003e C[2. 目录与文件结构];
A --\u003e D[3. 命名规范];
A --\u003e E[4. API Handler 规范];
A --\u003e F[5. 数据传输对象 (DTO) 规范];
A --\u003e G[6. 错误处理规范];
A --\u003e H[7. 日志记录规范];
A --\u003e I[8. 注释与文档(Swagger)];
subgraph E [4. API Handler 规范]
E1[4.1 Handler 定义]
E2[4.2 路由与函数签名]
E3[4.3 参数绑定与校验]
E4[4.4 业务逻辑调用]
E5[4.5 响应处理]
end
subgraph F [5. 数据传输对象 (DTO) 规范]
F1[5.1 Request/Response 结构体]
F2[5.2 JSON Tags]
F3[5.3 Validation Tags]
end
```
## 2. 核心章节设计
### 2.1 概述
* **目的**:阐明制定此规范的背景和目标。
* **范围**明确规范的适用范围API 层)。
* **原则**:强调代码清晰、一致、可维护的核心原则。
### 2.2 目录与文件结构
* **规则**
* API 功能模块应在 `internal/api/{模块名}` 下创建独立目录。
* 每个目录包含 `*.go` 文件,每个文件实现一个独立的 API 端点。
* `{模块名}.go` 文件用于定义 `handler` 结构体和 `New` 初始化函数。
* **示例**:以 `admin` 模块为例,展示其目录结构。
### 2.3 命名规范
* **规则**
* Handler 文件名:使用小写蛇形命名法,如 `login.go`, `admin_create.go`
* 请求/响应结构体:使用驼峰式命名,并以 `Request`/`Response` 结尾,如 `loginRequest`, `loginResponse`
* Handler 函数:使用驼峰式命名,如 `Login()`, `CreateAdmin()`
### 2.4 API Handler 规范
* **Handler 定义**`handler` 结构体应包含 `logger` 和数据库连接(`readDB`, `writeDB`)。
* **路由与函数签名**Handler 函数必须返回 `core.HandlerFunc` 类型。
* **参数绑定与校验**:必须使用 `ctx.ShouldBindJSON()` 进行参数绑定,并结合 `validation` 包进行错误信息格式化。
* **业务逻辑调用**:通过 `h.readDB``h.writeDB` 与数据库交互,或调用 `service` 层方法。
* **响应处理**:成功时使用 `ctx.Payload(res)` 返回数据。
### 2.5 数据传输对象 (DTO) 规范
* **Request/Response 结构体**:每个 API 都应定义独立的请求和响应结构体。
* **JSON Tags**:所有结构体字段必须包含 `json:"..."` tag。
* **Validation Tags**:对需要校验的请求字段,必须添加 `binding:"..."` tag。
### 2.6 错误处理规范
* **规则**:必须使用 `ctx.AbortWithError(core.Error(...))` 来中断请求并返回标准格式的错误。
* **`core.Error` 参数**:需提供 HTTP 状态码、业务错误码 (`code.Code`) 和详细错误信息。
### 2.7 日志记录规范
* **规则**:通过注入的 `h.logger` 实例进行日志记录(虽然在 `admin` 示例中不明显,但这是标准实践)。
### 2.8 注释与文档 (Swagger)
* **规则**
* 每个 Handler 函数上方必须有完整的 Swagger 注解,包括 `@Summary`, `@Description`, `@Tags`, `@Param`, `@Success`, `@Failure`, `@Router` 等。
* 所有 DTO 字段和函数都应有清晰的中文注释。
## 3. 数据流与接口契约
本文档本身不定义新的数据流或接口,而是规范如何定义它们。其核心思想是:**通过严格的结构体和规范化的错误处理,确保所有 API 的接口契约都是明确和一致的。**

View File

@ -0,0 +1,67 @@
# TASK: 开发规范文档撰写任务拆解
基于 `DESIGN_开发规范.md`,现将最终文档的撰写工作拆解为以下原子任务。这些任务将按顺序执行,以确保最终交付物的完整性和准确性。
## 任务依赖图
```mermaid
graph TD
subgraph 撰写最终文档
T1[任务1: 撰写概述章节] --\u003e T2
T2[任务2: 撰写目录与文件结构章节] --\u003e T3
T3[任务3: 撰写命名规范章节] --\u003e T4
T4[任务4: 撰写 API Handler 规范章节] --\u003e T5
T5[任务5: 撰写 DTO 规范章节] --\u003e T6
T6[任务6: 撰写错误处理规范章节] --\u003e T7
T7[任务7: 撰写日志记录规范章节] --\u003e T8
T8[任务8: 撰写注释与文档(Swagger)章节] --\u003e T9
T9[任务9: 整合所有章节并最终审查]
end
```
## 原子任务清单
### 任务 1: 撰写“概述”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于“概述”的描述。
* **输出契约**: 完成 `docs/开发规范.md` 中“概述”章节的文本内容。
* **实现约束**: 语言简洁,清晰传达规范的目的和原则。
### 任务 2: 撰写“目录与文件结构”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于目录结构的规则,以及对 `internal/api/admin` 目录的分析结果。
* **输出契约**: 完成 `docs/开发规范.md` 中“目录与文件结构”章节的文本和示例代码。
* **实现约束**: 必须提供清晰的目录树示例。
### 任务 3: 撰写“命名规范”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于命名规范的规则。
* **输出契约**: 完成 `docs/开发规范.md` 中“命名规范”章节的文本内容。
* **实现约束**: 规则需明确,覆盖文件、结构体、函数等不同场景。
### 任务 4: 撰写“API Handler 规范”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于 Handler 的详细设计,以及 `admin` 模块的源码作为示例。
* **输出契约**: 完成 `docs/开发规范.md` 中“API Handler 规范”章节的文本和代码示例。
* **实现约束**: 代码示例需完整展示 Handler 的标准写法。
### 任务 5: 撰写“数据传输对象 (DTO) 规范”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于 DTO 的设计,以及 `admin` 模块的源码作为示例。
* **输出契约**: 完成 `docs/开发规范.md` 中“DTO 规范”章节的文本和代码示例。
* **实现约束**: 需重点说明 `json``binding` tag 的用法。
### 任务 6: 撰写“错误处理规范”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于错误处理的设计,以及 `admin` 模块的源码作为示例。
* **输出契约**: 完成 `docs/开发规范.md` 中“错误处理规范”章节的文本和代码示例。
* **实现约束**: 需清晰解释 `ctx.AbortWithError``core.Error` 的使用方法。
### 任务 7: 撰写“日志记录规范”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于日志记录的设计。
* **输出契约**: 完成 `docs/开发规范.md` 中“日志记录规范”章节的文本内容。
* **实现约束**: 虽然示例不明显,但需根据项目架构(`handler` 中注入了 `logger`)推荐标准实践。
### 任务 8: 撰写“注释与文档(Swagger)”章节
* **输入契约**: `DESIGN_开发规范.md` 中关于 Swagger 的设计,以及 `admin` 模块的源码作为示例。
* **输出契约**: 完成 `docs/开发规范.md` 中“注释与文档(Swagger)”章节的文本和代码示例。
* **实现约束**: 需提供一个完整的 Swagger 注解块作为模板。
### 任务 9: 整合所有章节并最终审查
* **输入契约**: 已完成的所有章节内容。
* **输出契约**: 一份格式统一、内容完整、无错误的 `docs/开发规范.md` 文档。
* **实现约束**: 检查文档的流畅性、一致性和准确性。

150
docs/需求文档.md Normal file
View File

@ -0,0 +1,150 @@
一、业务一句话概述
你构建的是一个以“活动期”为载体的盲盒抽奖与商品售卖平台,支持用户参与抽奖、生成订单并支付,中奖/购买后产生成交与持仓记录,可进行转赠、发货,平台提供优惠券与积分体系以促进转化,同时具备邀请裂变、公会协作与玩法扩展(如 Boss 排行),形成完整的增长-交易-履约闭环。
二、用户视角的核心流程
1. 账号与身份
- 用户注册/登录微信为主维护用户基本资料与收货地址user_members / user_addresses
2. 活动参与
- 浏览活动及分类activity / activity_categories在具体期activity_issues参与抽奖或直接购买。
- 抽奖行为与结果记录draw_logs中奖奖品与参与信息可追溯。
3. 交易与支付
- 下单order_headers + order_items支持积分、优惠券抵扣user_points / user_coupons / coupons
- 支付流程payment_preorders / payment_refunds异常场景可退款。
4. 资产与履约
- 奖品持有user_inventory与转赠inventory_transfers不引入复杂状态机使用流水记录事实与最终归属。
- 发货shipping_sheets对应发货单后续可扩展发货明细shipping_items
5. 社交与增长
- 邀请裂变user_invites通过用户唯一邀请码user_members.invite_code建立 inviter-invitee 关系,奖励以积分或优惠券落地。
- 公会互动guild_main / guild_members / guild_boxes / guild_contribute_logs支持公会玩法与贡献日志。
6. 玩法扩展
- Boss 排行leaderboard_boss_players等玩法可在活动域或独立域扩展。
7. 平台通知
- 系统公告system_notices与系统配置system_configs支持运营触达与配置管理。
三、运营视角的能力闭环
1. 增长与激励
- 邀请裂变:唯一邀请码与邀请关系,奖励结算可选积分或优惠券。
- 积分体系:支持签到等行为积分,具备“每日一次”约束的唯一索引设计,利于并发安全。
- 优惠券体系券模板coupons支持适用范围全局/活动/商品用户持券与使用记录user_coupons清晰。
2. 商品与内容运营
- 商品与分类product_main / product_categories / product_category_map活动期配置activity_issues / activity_reward_settings
- 公告与配置system_notices / system_configs用于活动宣导与功能开关。
3. 履约与服务
- 发货单shipping_sheets配合订单与地址进行物流履约。
- 转赠流水inventory_transfers增强用户间互动与资产流通。
我现在要做一个盲盒系统小程序,数据库我已经设计好了,现在我和你说业务需求
活动(盲盒抽奖):
1. 有活动列表:
1.1 用户可以自己点击对应的活动
1.2 每个活动都有标签: 对应的活动类型 / 活动是否 Boss挑战 标签
1.3 创建活动是: activities 创建一个记录; 然后对应的 activity_issues 创建一个记录, 记录活动的期数;这样的目的是 一个活动可以有多个期数
1.3.1 例如 对对碰 一个活动 一个期数
1.3.2 例如 一番赏 一个活动 多个期数
1.4 创建好活动的时候可以根据活动分类来设置类型
1.5 每个活动也可以配置奖品
1.6 活动也可以设置是否为 boss 活动
2. 活动接口:
后台接口:
1. 创建活动: /api/admin/activities
2. 修改活动: /api/admin/activities/:activity_id
3. 删除活动: /api/admin/activities/:activity_id
4. 查看活动详情: /api/admin/activities/:activity_id
5. 查看活动期数: /api/admin/activities/:activity_id/issues
6. 创建期数奖品: /api/admin/activities/:activity_id/issues/:issue_id/rewards
7. 查看期数奖品: /api/admin/activities/:activity_id/issues/:issue_id/rewards
8. 创建活动期数: /api/admin/activities/:activity_id/issues
9. 修改活动期数: /api/admin/activities/:activity_id/issues/:issue_id
10. 删除活动期数: /api/admin/activities/:activity_id/issues/:issue_id
app用户接口:
1. 浏览活动列表: /api/app/activities
2. 查看活动详情: /api/app/activities/:activity_id
3. 查看活动期数: /api/app/activities/:activity_id/issues
4. 查看期数奖品: /api/app/activities/:activity_id/issues/:issue_id/rewards
5. 查看活动抽奖记录: /api/app/activities/:activity_id/issues/:issue_id/draw_logs
工会(公会):
1. 需求:
1.1 用户可以自己点击对应的工会
1.2 每个工会都有标签: 对应的工会类型 / 工会是否公开 标签
1.3 创建工会是: guild 创建一个记录; 然后对应的 guild_members 创建一个记录, 记录工会的成员;这样的目的是 一个工会可以有多个成员
1.3.1 例如 一对情侣 一个工会 两个成员
1.3.2 例如 一个团队 一个工会 多个成员
1.3.3 创建公会的时候 默认 第一个人是会长
1.3.4 其他成员加入工会的时候, 会创建一个 guild_members 记录, 记录成员的加入时间
1.4 创建好工会的时候可以根据工会分类来设置类型
1.5 工会也可以设置是否为公开工会
1.6 用户离开公会后 24 小时不允许加入公会
2. 工会接口:
后台接口:
1. 创建工会: /api/admin/guilds
2. 修改工会: /api/admin/guilds/:guild_id
3. 删除工会: /api/admin/guilds/:guild_id
4. 查看工会详情: /api/admin/guilds/:guild_id
5. 查看工会成员: /api/admin/guilds/:guild_id/members
3. app用户接口:
1. 浏览工会列表: /api/app/guilds
2. 查看工会详情: /api/app/guilds/:guild_id
3. 加入工会: /api/app/guilds/:guild_id/members
4. 离开公会: /api/app/guilds/:guild_id/members/:user_id
5. 查看工会成员: /api/app/guilds/:guild_id/members
用户(用户):
1. 需求:
1.1 用户可以注册登录: weixin 登录
1.2 用户可以查看自己的个人信息
1.3 用户可以修改自己的个人信息
1.4 用户可以查看自己邀请记录
1.5 可以看到自己的订单记录
1.6 用户可以看到自己的优惠券
1.7 用户可以查看自己的积分记录 和 积分余额
用户在登录的时候: 判断如果 链接里面有 code=这个参数; 那么代表是被邀请用户; 需要登录的时候携带这个参数; 后台会判断这个参数(是否可以根据这个参数找到邀请人), 然后在 邀请表中写入一个记录, 如果已经邀请了则不下发积分奖励; 如果没有邀请过, 则下发积分奖励
用户可以编辑修改自己的用户信息: 昵称 + 头像
当用户加入工会后是可以看到自己的工会成员的
用户可以在订单中 找到自己的订单记录(来源是 活动抽奖得到的)
现在帮我完成这个需求; 一定需要记住: 这个是app端的接口; api 和 service 需要 遵守拆分规则 , 然后需要协商接口文档; 同时注意 req / res 这个需要在自己的代码中定义好;
接口:
1. 修改用户信息: /api/app/users/:user_id
2. 查看用户订单记录: /api/app/users/:user_id/orders
3. 查看用户优惠券: /api/app/users/:user_id/coupons
4. 查看用户积分记录: /api/app/users/:user_id/points
5. 查看用户积分余额: /api/app/users/:user_id/points/balance
6. 微信登录: /api/app/users/weixin/login
7. 查看用户邀请记录: /api/app/users/:user_id/invites
用户(admin管理端)
1. 查看所有用户列表: /api/admin/users
2. 可以查看到用户邀请列表: /api/admin/users/:user_id/invites
3. 可以查看到用户订单列表: /api/admin/users/:user_id/orders
4. 可以查看到用户优惠券列表: /api/admin/users/:user_id/coupons
5. 可以查看到用户积分记录列表: /api/admin/users/:user_id/points
6. 可以查看到用户积分余额: /api/admin/users/:user_id/points/balance
7. 可以给用户添加积分: /api/admin/users/:user_id/points/add
8. 可以给用户添加优惠券: /api/admin/users/:user_id/coupons/add
商品管理:(product_categories / products)
1. 后台管理:
1.1 创建商品分类: /api/admin/product_categories
1.2 修改商品分类: /api/admin/product_categories/:category_id
1.3 删除商品分类: /api/admin/product_categories/:category_id
1.4 查看商品分类列表: /api/admin/product_categories
1.5 创建商品: /api/admin/products
1.6 修改商品: /api/admin/products/:product_id
1.7 删除商品: /api/admin/products/:product_id
1.8 查看商品列表: /api/admin/products

View File

@ -0,0 +1,47 @@
# 项目和任务特性规范
## 原始需求
梳理项目框架,为后续开发做准备。
## 边界确认
- **目标**: 分析现有项目结构、技术栈、架构模式、依赖关系,并输出分析报告。
- **范围**: 仅分析现有代码库,不涉及运行时环境或外部系统。
## 需求理解
该项目是一个Go语言编写的后端服务命名为 `bindbox_game`,可能是一个“盲盒游戏”相关的应用。
从目录结构来看,项目遵循了一定的分层架构或整洁架构思想,主要模块包括:
- `cmd/`: 存放可执行命令,如代码生成器 (`gormgen`, `handlergen`)。
- `configs/`: 配置文件,区分不同环境 (`dev`, `fat`, `pro`, `uat`)。
- `deploy/`: 部署相关文件,如 `docker-compose.yaml`
- `docs/`: 项目文档,包括 `swagger` 定义和一些说明文档。
- `internal/`: 项目核心业务逻辑,这是分析的重点。
- `scripts/`: 辅助脚本。
- `static/`: 静态资源。
`internal/` 目录下的结构进一步体现了分层设计:
- `api/`: API接口定义按模块划分`admin`)。
- `repository/`: 数据仓库层,负责与数据库交互,使用了 `gorm`(从 `gormgen` 推断)和 `mysql`
- `service/``services/`: 服务层,处理核心业务逻辑。
- `router/`: 路由定义和中间件。
- `pkg/`: 可重用的公共库或工具类,如日志、错误处理、加解密等。
- `code/`: 业务状态码定义。
- `metrics/`: 监控指标。
- `alert/`: 告警。
技术栈和依赖推断:
- **语言**: Go
- **Web框架**: 可能是 `Gin` 或类似框架(需要查看 `go.mod` 确认)。
- **ORM**: `GORM`(从 `gormgen``internal/repository/mysql` 目录推断)。
- **数据库**: `MySQL`
- **文档**: `Swagger`
- **部署**: `Docker`
## 疑问澄清
- Web框架具体是哪个 **Gin**
- 项目依赖了哪些主要的第三方库? **GORM, Viper, Zap, Gin**
- `service``services` 两个目录的区别是什么? `service` 目录为空,`services` 目录用于存放按功能模块组织的业务逻辑,但目前未被积极使用。
- `proposal` 目录的作用是什么? 定义了用于告警、日志等目的的数据结构和处理器。
接下来,我将通过分析 `go.mod` 文件来解答这些疑问。

View File

@ -0,0 +1,29 @@
# 最终共识
## 需求描述和验收标准
- **需求**: 梳理并理解现有项目框架,为后续开发提供清晰的上下文。
- **验收标准**: 输出一份关于项目架构、技术栈和关键模块的分析报告。
## 技术实现方案和技术约束和集成方案
- **项目名称**: `bindbox_game`
- **语言**: Go
- **Web框架**: Gin
- **ORM**: GORM
- **数据库**: MySQL
- **配置管理**: Viper
- **日志**: Zap
- **API文档**: Swagger
- **部署**: Docker
## 任务边界限制和验收标准
- **架构模式**: 项目采用分层架构,但业务逻辑目前主要集中在 `internal/api` 层,`internal/services` 层尚未被充分利用。
- **代码生成**: 项目使用 `gormgen``handlergen` 等工具生成部分代码。
- **公共库**: `internal/pkg` 目录提供了丰富的可重用组件。
- **中间件**: 在 `internal/router` 中定义,用于处理认证、日志等。
## 确认所有不确定性已解决
- `service` vs `services`: `service` 目录为空,`services` 用于模块化业务逻辑,但目前未被积极使用。
- `proposal` 目录: 定义了用于告警、日志等目的的数据结构和处理器。
**结论**: 项目框架清晰,技术选型主流。后续开发应遵循现有架构模式,并考虑将业务逻辑更清晰地分离到 `service` 层,以提高可维护性。

8
go.mod
View File

@ -1,4 +1,4 @@
module mini-chat
module bindbox-game
go 1.19
@ -15,7 +15,7 @@ require (
github.com/go-playground/validator/v10 v10.15.0
github.com/go-resty/resty/v2 v2.10.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5
github.com/issue9/identicon/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644
@ -25,6 +25,7 @@ require (
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2
github.com/tealeg/xlsx v1.0.5
github.com/tencentyun/cos-go-sdk-v5 v0.7.37
go.uber.org/multierr v1.10.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.27.0
@ -44,6 +45,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
@ -55,6 +57,7 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@ -71,6 +74,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect

19
go.sum
View File

@ -46,6 +46,7 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
@ -61,6 +62,8 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
@ -172,6 +175,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -187,6 +192,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -197,6 +204,9 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/issue9/assert/v4 v4.1.1 h1:OhPE8SB8n/qZCNGLQa+6MQtr/B3oON0JAVj68k8jJlc=
github.com/issue9/identicon/v2 v2.1.2 h1:tu+4vveoiJNXfmWYvl1pDcZSAHCG37+lsoEc2UfCzkI=
github.com/issue9/identicon/v2 v2.1.2/go.mod h1:h5JXMtcgkqxltElhpF7PPicNyvFDWzi8VCSHdNjG7KY=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
@ -205,8 +215,6 @@ github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5 h1:kCvm3G3u+eTRbjfLPyfsfznJtraYEfZer/UvQ6CaQhI=
github.com/jakecoffman/cron v0.0.0-20190106200828-7e2009c226a5/go.mod h1:6DM2KNNK69jRu0lAHmYK9LYxmqpNjYHOaNp/ZxttD4U=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -254,6 +262,7 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -261,6 +270,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
@ -326,6 +337,10 @@ github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.194/go.mod h1:yrBKWhChnDqNz1xuXdSbWXG56XawEq0G5j1lg4VwBD4=
github.com/tencentyun/cos-go-sdk-v5 v0.7.37 h1:yMRtmXadV/Timk130OUoWDSMQEo5656kaoBfv8VcurM=
github.com/tencentyun/cos-go-sdk-v5 v0.7.37/go.mod h1:4dCEtLHGh8QPxHEkgq+nFaky7yZxQuYwgSJM87icDaw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=

BIN
internal/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,7 +1,7 @@
package alert
import (
"mini-chat/internal/proposal"
"bindbox-game/internal/proposal"
)
func NotifyHandler() func(msg *proposal.AlertMessage) {

View File

@ -0,0 +1,138 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listActivitiesRequest struct {
Name string `form:"name"`
CategoryID int64 `form:"category_id"`
IsBoss int32 `form:"is_boss"`
Status int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type activityItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Banner string `json:"banner"`
ActivityCategoryID int64 `json:"activity_category_id"`
CategoryName string `json:"category_name"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
}
type listActivitiesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []activityItem `json:"list"`
}
// ListActivities 活动列表
// @Summary 活动列表
// @Description 获取活动列表支持分类、Boss、状态过滤与分页
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param name query string false "活动名称(模糊)"
// @Param category_id query int false "活动分类ID"
// @Param is_boss query int false "是否Boss(0/1)"
// @Param status query int false "状态(1进行中 2下线)"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listActivitiesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities [get]
func (h *handler) ListActivities() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listActivitiesRequest)
res := new(listActivitiesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var isBossPtr *int32
if req.IsBoss == 0 || req.IsBoss == 1 {
isBossPtr = &req.IsBoss
}
var statusPtr *int32
if req.Status == 1 || req.Status == 2 {
statusPtr = &req.Status
}
items, total, err := h.activity.ListActivities(ctx.RequestContext(), struct {
Name string
CategoryID int64
IsBoss *int32
Status *int32
Page int
PageSize int
}{Name: req.Name, CategoryID: req.CategoryID, IsBoss: isBossPtr, Status: statusPtr, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]activityItem, len(items))
// collect category ids
var catIDs []int64
catSet := make(map[int64]struct{})
for _, v := range items {
if v.ActivityCategoryID != 0 {
if _, ok := catSet[v.ActivityCategoryID]; !ok {
catSet[v.ActivityCategoryID] = struct{}{}
catIDs = append(catIDs, v.ActivityCategoryID)
}
}
}
nameMap, _ := h.activity.GetCategoryNames(ctx.RequestContext(), catIDs)
for i, v := range items {
res.List[i] = activityItem{
ID: v.ID,
Name: v.Name,
Banner: v.Banner,
ActivityCategoryID: v.ActivityCategoryID,
CategoryName: nameMap[v.ActivityCategoryID],
Status: v.Status,
PriceDraw: v.PriceDraw,
IsBoss: v.IsBoss,
}
}
ctx.Payload(res)
}
}
// GetActivityDetail 活动详情
// @Summary 活动详情
// @Description 获取指定活动的详细信息
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} model.Activities
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id} [get]
func (h *handler) GetActivityDetail() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
item, err := h.activity.GetActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error()))
return
}
ctx.Payload(item)
}
}

View File

@ -0,0 +1,24 @@
package app
import (
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
activitysvc "bindbox-game/internal/service/activity"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
activity activitysvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
activity: activitysvc.New(logger, db),
}
}

View File

@ -0,0 +1,84 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listDrawLogsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type drawLogItem struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
IssueID int64 `json:"issue_id"`
OrderID int64 `json:"order_id"`
RewardID int64 `json:"reward_id"`
IsWinner int32 `json:"is_winner"`
Level int32 `json:"level"`
CurrentLevel int32 `json:"current_level"`
}
type listDrawLogsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []drawLogItem `json:"list"`
}
// ListDrawLogs 抽奖记录列表
// @Summary 抽奖记录列表
// @Description 查看指定活动期数的抽奖记录,支持分页
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listDrawLogsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id}/issues/{issue_id}/draw_logs [get]
func (h *handler) ListDrawLogs() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listDrawLogsRequest)
res := new(listDrawLogsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]drawLogItem, len(items))
for i, v := range items {
res.List[i] = drawLogItem{
ID: v.ID,
UserID: v.UserID,
IssueID: v.IssueID,
OrderID: v.OrderID,
RewardID: v.RewardID,
IsWinner: v.IsWinner,
Level: v.Level,
CurrentLevel: v.CurrentLevel,
}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,70 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listIssuesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type issueItem struct {
ID int64 `json:"id"`
IssueNumber string `json:"issue_number"`
Status int32 `json:"status"`
Sort int32 `json:"sort"`
}
type listIssuesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []issueItem `json:"list"`
}
// ListActivityIssues 活动期列表
// @Summary 活动期列表
// @Description 获取指定活动的期列表,支持分页
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listIssuesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id}/issues [get]
func (h *handler) ListActivityIssues() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listIssuesRequest)
res := new(listIssuesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
items, total, err := h.activity.ListIssues(ctx.RequestContext(), activityID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivityIssuesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]issueItem, len(items))
for i, v := range items {
res.List[i] = issueItem{ID: v.ID, IssueNumber: v.IssueNumber, Status: v.Status, Sort: v.Sort}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,71 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type rewardItem struct {
ProductID int64 `json:"product_id"`
Name string `json:"name"`
Weight int32 `json:"weight"`
Quantity int64 `json:"quantity"`
OriginalQty int64 `json:"original_qty"`
Level int32 `json:"level"`
Sort int32 `json:"sort"`
IsBoss int32 `json:"is_boss"`
}
type listRewardsResponse struct {
List []rewardItem `json:"list"`
}
// ListIssueRewards 奖励配置列表
// @Summary 奖励配置列表
// @Description 获取指定期的奖励配置列表
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Success 200 {object} listRewardsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/activities/{activity_id}/issues/{issue_id}/rewards [get]
func (h *handler) ListIssueRewards() core.HandlerFunc {
return func(ctx core.Context) {
issueIDStr := ctx.Param("issue_id")
if issueIDStr == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
if _, err := strconv.ParseInt(issueIDStr, 10, 64); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
issueID, _ := strconv.ParseInt(issueIDStr, 10, 64)
items, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListIssueRewardsError, err.Error()))
return
}
res := new(listRewardsResponse)
res.List = make([]rewardItem, len(items))
for i, v := range items {
res.List[i] = rewardItem{
ProductID: v.ProductID,
Name: v.Name,
Weight: v.Weight,
Quantity: v.Quantity,
OriginalQty: v.OriginalQty,
Level: v.Level,
Sort: v.Sort,
IsBoss: v.IsBoss,
}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,230 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
activitysvc "bindbox-game/internal/service/activity"
)
type createActivityRequest struct {
Name string `json:"name" binding:"required"`
Banner string `json:"banner"`
ActivityCategoryID int64 `json:"activity_category_id" binding:"required"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
type createActivityResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateActivity 创建活动
// @Summary 创建活动
// @Description 创建活动配置基本信息与分类、Boss标签
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param RequestBody body createActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities [post]
// @Security LoginVerifyToken
func (h *handler) CreateActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createActivityRequest)
res := new(createActivityResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.ActivityCategoryID == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动分类ID不能为空"))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, "禁止操作"))
return
}
var st, et *time.Time
if req.StartTime != "" {
if t, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
st = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
et = &t
}
}
item, err := h.activity.CreateActivity(ctx.RequestContext(), activitysvc.CreateActivityInput{
Name: req.Name,
Banner: req.Banner,
ActivityCategoryID: req.ActivityCategoryID,
Status: req.Status,
PriceDraw: req.PriceDraw,
IsBoss: req.IsBoss,
StartTime: st,
EndTime: et,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyActivityRequest struct {
Name string `json:"name"`
Banner string `json:"banner"`
ActivityCategoryID int64 `json:"activity_category_id"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
// ModifyActivity 修改活动
// @Summary 修改活动
// @Description 修改活动基本信息、分类、Boss标签等
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param RequestBody body modifyActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyActivityRequest)
res := new(simpleMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
var st, et *time.Time
if req.StartTime != "" {
if t, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
st = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
et = &t
}
}
if err := h.activity.ModifyActivity(ctx.RequestContext(), id, activitysvc.ModifyActivityInput{
Name: req.Name,
Banner: req.Banner,
ActivityCategoryID: req.ActivityCategoryID,
Status: req.Status,
PriceDraw: req.PriceDraw,
IsBoss: req.IsBoss,
StartTime: st,
EndTime: et,
}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// DeleteActivity 删除活动
// @Summary 删除活动
// @Description 删除指定活动
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteActivity() core.HandlerFunc {
return func(ctx core.Context) {
res := new(simpleMessageResponse)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
if err := h.activity.DeleteActivity(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// GetActivityDetail 查看活动详情
// @Summary 查看活动详情
// @Description 查看指定活动的详细信息
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} model.Activities
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [get]
// @Security LoginVerifyToken
func (h *handler) GetActivityDetail() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
item, err := h.activity.GetActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error()))
return
}
ctx.Payload(item)
}
}

View File

@ -0,0 +1,43 @@
package admin
import (
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
type categoryItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type listCategoriesResponse struct {
List []categoryItem `json:"list"`
}
// ListActivityCategories 活动分类列表
// @Summary 活动分类列表
// @Description 获取启用状态的活动分类列表
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Success 200 {object} listCategoriesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activity_categories [get]
// @Security LoginVerifyToken
func (h *handler) ListActivityCategories() core.HandlerFunc {
return func(ctx core.Context) {
items, err := h.readDB.ActivityCategories.Where(h.readDB.ActivityCategories.Status.Eq(1)).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
res := new(listCategoriesResponse)
res.List = make([]categoryItem, len(items))
for i, v := range items {
res.List[i] = categoryItem{ID: v.ID, Name: v.Name}
}
ctx.Payload(res)
}
}

View File

@ -1,15 +1,27 @@
package admin
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
activitysvc "bindbox-game/internal/service/activity"
adminsvc "bindbox-game/internal/service/admin"
guildsvc "bindbox-game/internal/service/guild"
productsvc "bindbox-game/internal/service/product"
bannersvc "bindbox-game/internal/service/banner"
usersvc "bindbox-game/internal/service/user"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
svc adminsvc.Service
activity activitysvc.Service
guild guildsvc.Service
product productsvc.Service
user usersvc.Service
banner bannersvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
@ -17,5 +29,11 @@ func New(logger logger.CustomLogger, db mysql.Repo) *handler {
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
svc: adminsvc.New(logger, db),
activity: activitysvc.New(logger, db),
guild: guildsvc.New(logger, db),
product: productsvc.New(logger, db),
user: usersvc.New(logger, db),
banner: bannersvc.New(logger, db),
}
}

View File

@ -3,15 +3,11 @@ package admin
import (
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/utils"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"gorm.io/gorm"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
adminsvc "bindbox-game/internal/service/admin"
)
type createAdminRequest struct {
@ -59,48 +55,14 @@ func (h *handler) CreateAdmin() core.HandlerFunc {
return
}
info, err := h.readDB.Admin.WithContext(ctx.RequestContext()).
Where(h.readDB.Admin.Username.Eq(req.UserName)).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), err.Error())),
)
return
}
if info != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "该账号已存在")),
)
return
}
hashedPassword, err := utils.GenerateAdminHashedPassword(req.Password)
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), err.Error())),
)
return
}
Admin := new(model.Admin)
Admin.Username = req.UserName
Admin.Nickname = req.NickName
Admin.Mobile = req.Mobile
Admin.Password = hashedPassword
Admin.Avatar = req.Avatar
Admin.LoginStatus = 1
Admin.IsSuper = 0
Admin.CreatedUser = ctx.SessionUserInfo().UserName
Admin.CreatedAt = time.Now()
if err := h.writeDB.Admin.WithContext(ctx.RequestContext()).Create(Admin); err != nil {
if err := h.svc.Create(ctx.RequestContext(), adminsvc.CreateInput{
Username: req.UserName,
Nickname: req.NickName,
Mobile: req.Mobile,
Password: req.Password,
Avatar: req.Avatar,
CreatedBy: ctx.SessionUserInfo().UserName,
}); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,

View File

@ -6,10 +6,9 @@ import (
"strconv"
"strings"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type deleteAdminRequest struct {
@ -92,10 +91,7 @@ func (h *handler) DeleteAdmin() core.HandlerFunc {
return
}
if _, err := h.writeDB.Admin.WithContext(ctx.RequestContext()).
Where(h.writeDB.Admin.IsSuper.Eq(0)).
Where(h.writeDB.Admin.ID.In(ids...)).
Delete(&model.Admin{}); err != nil {
if err := h.svc.Delete(ctx.RequestContext(), ids); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteAdminError,

View File

@ -4,12 +4,11 @@ import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/timeutil"
"bindbox-game/internal/pkg/validation"
adminsvc "bindbox-game/internal/service/admin"
)
type listRequest struct {
@ -81,8 +80,6 @@ func (h *handler) PageList() core.HandlerFunc {
return
}
query := h.readDB.Admin.WithContext(ctx.RequestContext())
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
@ -92,31 +89,12 @@ func (h *handler) PageList() core.HandlerFunc {
return
}
if req.Username != "" {
query = query.Where(h.readDB.Admin.Username.Like(fmt.Sprintf("%%%s%%", req.Username)))
}
if req.Nickname != "" {
query = query.Where(h.readDB.Admin.Nickname.Like(fmt.Sprintf("%%%s%%", req.Nickname)))
}
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.Admin.ID.Desc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAdminError,
fmt.Sprintf("%s%s", code.Text(code.ListAdminError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
items, total, err := h.svc.List(ctx.RequestContext(), adminsvc.ListInput{
Username: req.Username,
Nickname: req.Nickname,
Page: req.Page,
PageSize: req.PageSize,
})
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
@ -128,10 +106,10 @@ func (h *handler) PageList() core.HandlerFunc {
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]listData, len(resultData))
res.Total = total
res.List = make([]listData, len(items))
for k, v := range resultData {
for k, v := range items {
res.List[k] = listData{
ID: v.ID,
UserName: v.Username,

View File

@ -4,14 +4,11 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/utils"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
adminsvc "bindbox-game/internal/service/admin"
)
type modifyAdminRequest struct {
@ -79,10 +76,14 @@ func (h *handler) ModifyAdmin() core.HandlerFunc {
return
}
checkIdInfo, err := h.readDB.Admin.WithContext(ctx.RequestContext()).
Where(h.readDB.Admin.ID.Eq(int32(id))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
if err := h.svc.Modify(ctx.RequestContext(), id, adminsvc.ModifyInput{
Username: req.UserName,
Nickname: req.NickName,
Mobile: req.Mobile,
Password: req.Password,
Avatar: req.Avatar,
UpdatedBy: ctx.SessionUserInfo().UserName,
}); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
@ -91,65 +92,6 @@ func (h *handler) ModifyAdmin() core.HandlerFunc {
return
}
if checkIdInfo == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), "该账号不存在")),
)
return
}
checkUserNameInfo, err := h.readDB.Admin.WithContext(ctx.RequestContext()).
Where(h.readDB.Admin.ID.Neq(int32(id))).
Where(h.readDB.Admin.Username.Eq(req.UserName)).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), err.Error())),
)
return
}
if checkUserNameInfo != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), "该账号已存在")),
)
return
}
if req.Password != "" {
hashedPassword, err := utils.GenerateAdminHashedPassword(req.Password)
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAdminError), err.Error())),
)
return
}
checkIdInfo.Password = hashedPassword
}
checkIdInfo.Username = req.UserName
checkIdInfo.Nickname = req.NickName
checkIdInfo.Mobile = req.Mobile
checkIdInfo.Avatar = req.Avatar
checkIdInfo.UpdatedUser = ctx.SessionUserInfo().UserName
checkIdInfo.UpdatedAt = time.Now()
if err := h.writeDB.Admin.WithContext(ctx.RequestContext()).Save(checkIdInfo); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAdminError,
fmt.Sprintf("%s%s", code.Text(code.ModifyAdminError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}

View File

@ -1,172 +0,0 @@
package admin
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type relAppRequest struct {
Ids string `json:"ids" binding:"required"` // 小程序编号(多个用,分割)
}
type relAppResponse struct {
Message string `json:"message"` // 提示信息
}
// RelApp 客服关联小程序
// @Summary 客服关联小程序
// @Description 客服关联小程序
// @Tags 管理端.客服管理
// @Accept json
// @Produce json
// @Param id path string true "客服编号ID"
// @Param RequestBody body relAppRequest true "请求参数"
// @Success 200 {object} relAppResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/rel_app/{id} [put]
// @Security LoginVerifyToken
func (h *handler) RelApp() core.HandlerFunc {
return func(ctx core.Context) {
req := new(relAppRequest)
res := new(relAppResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s: %s", code.Text(code.RelAppError), "禁止操作")),
)
return
}
idList := strings.Split(req.Ids, ",")
if len(idList) == 0 || (len(idList) == 1 && idList[0] == "") {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"编号不能为空"),
)
return
}
var ids []int32
for _, strID := range idList {
if strID == "" {
continue
}
id, err := strconv.Atoi(strID)
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
fmt.Sprintf("无效的编号: %s", strID)),
)
return
}
ids = append(ids, int32(id))
}
if len(ids) == 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"编号不能为空"),
)
return
}
adminID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递客服编号ID"),
)
return
}
checkInfo, err := h.readDB.Admin.WithContext(ctx.RequestContext()).
Where(h.readDB.Admin.ID.Eq(int32(adminID))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s: %s", code.Text(code.RelAppError), err.Error())),
)
return
}
if checkInfo == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s: %s", code.Text(code.RelAppError), "该客服不存在")),
)
return
}
if checkInfo.IsSuper == 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s", "作为超级管理员,您拥有无需关联即可查看的权限。")),
)
return
}
// 删除原来已经关联过的
if _, err := h.writeDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.writeDB.MiniProgram.AdminID.Eq(int32(adminID))).
Updates(map[string]interface{}{
"admin_id": 0,
"updated_user": ctx.SessionUserInfo().UserName,
"updated_at": time.Now(),
}); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s: %s", code.Text(code.RelAppError), err.Error())),
)
return
}
if _, err := h.writeDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.writeDB.MiniProgram.ID.In(ids...)).
Updates(map[string]interface{}{
"admin_id": adminID,
"updated_user": ctx.SessionUserInfo().UserName,
"updated_at": time.Now(),
}); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.RelAppError,
fmt.Sprintf("%s: %s", code.Text(code.RelAppError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,181 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
bannersvc "bindbox-game/internal/service/banner"
)
type createBannerRequest struct {
Title string `json:"title" binding:"required"`
ImageURL string `json:"image_url" binding:"required"`
LinkURL string `json:"link_url"`
Sort int32 `json:"sort"`
Status int32 `json:"status"`
}
type createBannerResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateBanner 创建轮播图
// @Summary 创建轮播图
// @Tags 管理端.运营
// @Accept json
// @Produce json
// @Param RequestBody body createBannerRequest true "请求参数"
// @Success 200 {object} createBannerResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/banners [post]
// @Security LoginVerifyToken
func (h *handler) CreateBanner() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createBannerRequest)
res := new(createBannerResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
item, err := h.banner.Create(ctx.RequestContext(), bannersvc.CreateInput{Title: req.Title, ImageURL: req.ImageURL, LinkURL: req.LinkURL, Sort: req.Sort, Status: req.Status})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyBannerRequest struct {
Title *string `json:"title"`
ImageURL *string `json:"image_url"`
LinkURL *string `json:"link_url"`
Sort *int32 `json:"sort"`
Status *int32 `json:"status"`
}
// ModifyBanner 修改轮播图
// @Summary 修改轮播图
// @Tags 管理端.运营
// @Accept json
// @Produce json
// @Param banner_id path string true "轮播图ID"
// @Param RequestBody body modifyBannerRequest true "请求参数"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/banners/{banner_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyBanner() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyBannerRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("banner_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.banner.Modify(ctx.RequestContext(), id, bannersvc.ModifyInput{Title: req.Title, ImageURL: req.ImageURL, LinkURL: req.LinkURL, Sort: req.Sort, Status: req.Status}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
// DeleteBanner 删除轮播图
// @Summary 删除轮播图
// @Tags 管理端.运营
// @Accept json
// @Produce json
// @Param banner_id path string true "轮播图ID"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/banners/{banner_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteBanner() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("banner_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.banner.Delete(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
type listBannersRequest struct {
Status *int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type bannerItem struct {
ID int64 `json:"id"`
Title string `json:"title"`
ImageURL string `json:"image_url"`
LinkURL string `json:"link_url"`
Sort int32 `json:"sort"`
Status int32 `json:"status"`
}
type listBannersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []bannerItem `json:"list"`
}
// ListBanners 查看轮播图列表
// @Summary 查看轮播图列表
// @Tags 管理端.运营
// @Accept json
// @Produce json
// @Param status query int false "状态"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量" default(20)
// @Success 200 {object} listBannersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/banners [get]
// @Security LoginVerifyToken
func (h *handler) ListBanners() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listBannersRequest)
res := new(listBannersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
items, total, err := h.banner.List(ctx.RequestContext(), bannersvc.ListInput{Status: req.Status, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]bannerItem, len(items))
for i, it := range items {
res.List[i] = bannerItem{ID: it.ID, Title: it.Title, ImageURL: it.ImageURL, LinkURL: it.LinkURL, Sort: it.Sort, Status: it.Status}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,60 @@
package admin
import (
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
guildsvc "bindbox-game/internal/service/guild"
)
type createGuildRequest struct {
Name string `json:"name" binding:"required"`
OwnerID int64 `json:"owner_id" binding:"required"`
Description string `json:"description"`
JoinMode int32 `json:"join_mode"`
ConsumeLimit int64 `json:"consume_limit"`
AvatarURL string `json:"avatar_url"`
IsOpen int32 `json:"is_open"`
}
type createGuildResponse struct {
Message string `json:"message"`
}
// CreateGuild 创建工会
// @Summary 创建工会
// @Description 创建工会并将首位成员设为会长
// @Tags 管理端.工会
// @Accept json
// @Produce json
// @Param RequestBody body createGuildRequest true "请求参数"
// @Success 200 {object} createGuildResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/guilds [post]
// @Security LoginVerifyToken
func (h *handler) CreateGuild() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createGuildRequest)
res := new(createGuildResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateGuildError, "禁止操作"))
return
}
_, err := h.guild.CreateGuild(ctx.RequestContext(), guildsvc.CreateGuildInput{
Name: req.Name, OwnerID: req.OwnerID, Description: req.Description,
JoinMode: req.JoinMode, ConsumeLimit: req.ConsumeLimit, AvatarURL: req.AvatarURL, IsOpen: req.IsOpen,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateGuildError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,45 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
type deleteGuildResponse struct {
Message string `json:"message"`
}
// DeleteGuild 删除工会
// @Summary 删除工会
// @Description 删除指定工会
// @Tags 管理端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Success 200 {object} deleteGuildResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/guilds/{guild_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteGuild() core.HandlerFunc {
return func(ctx core.Context) {
res := new(deleteGuildResponse)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteGuildError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
if err := h.guild.DeleteGuild(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteGuildError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,36 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
// GetGuildDetail 查看工会详情
// @Summary 查看工会详情
// @Description 查看指定工会详情
// @Tags 管理端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Success 200 {object} model.Guild
// @Failure 400 {object} code.Failure
// @Router /api/admin/guilds/{guild_id} [get]
// @Security LoginVerifyToken
func (h *handler) GetGuildDetail() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
item, err := h.guild.GetGuild(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetGuildError, err.Error()))
return
}
ctx.Payload(item)
}
}

View File

@ -0,0 +1,73 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listGuildMembersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type memberItem struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Role string `json:"role"`
StartTime string `json:"start_time"`
}
type listGuildMembersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []memberItem `json:"list"`
}
// ListGuildMembers 查看工会成员
// @Summary 查看工会成员
// @Description 查看指定工会的成员列表
// @Tags 管理端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listGuildMembersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/guilds/{guild_id}/members [get]
// @Security LoginVerifyToken
func (h *handler) ListGuildMembers() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listGuildMembersRequest)
res := new(listGuildMembersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListGuildMembersError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
items, total, err := h.guild.ListMembers(ctx.RequestContext(), id, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListGuildMembersError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]memberItem, len(items))
for i, v := range items {
res.List[i] = memberItem{ID: v.ID, UserID: v.UserID, Role: v.Role, StartTime: v.StartTime.Format("2006-01-02 15:04:05")}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,65 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
guildsvc "bindbox-game/internal/service/guild"
)
type modifyGuildRequest struct {
Name string `json:"name"`
Description string `json:"description"`
JoinMode int32 `json:"join_mode"`
ConsumeLimit int64 `json:"consume_limit"`
AvatarURL string `json:"avatar_url"`
IsOpen int32 `json:"is_open"`
Status int32 `json:"status"`
}
type modifyGuildResponse struct {
Message string `json:"message"`
}
// ModifyGuild 修改工会
// @Summary 修改工会
// @Description 修改指定工会信息
// @Tags 管理端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Param RequestBody body modifyGuildRequest true "请求参数"
// @Success 200 {object} modifyGuildResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/guilds/{guild_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyGuild() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyGuildRequest)
res := new(modifyGuildResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyGuildError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
if err := h.guild.ModifyGuild(ctx.RequestContext(), id, guildsvc.ModifyGuildInput{
Name: req.Name, Description: req.Description, JoinMode: req.JoinMode, ConsumeLimit: req.ConsumeLimit, AvatarURL: req.AvatarURL, IsOpen: req.IsOpen, Status: req.Status,
}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyGuildError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,223 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
activitysvc "bindbox-game/internal/service/activity"
)
type listIssuesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listIssuesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*activitysvcIssueData `json:"list"`
}
type activitysvcIssueData struct {
ID int64 `json:"id"`
IssueNumber string `json:"issue_number"`
Status int32 `json:"status"`
Sort int32 `json:"sort"`
}
// ListActivityIssues 查看活动期数
// @Summary 查看活动期数
// @Description 获取指定活动的期数列表,支持分页
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listIssuesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues [get]
// @Security LoginVerifyToken
func (h *handler) ListActivityIssues() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listIssuesRequest)
res := new(listIssuesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
items, total, err := h.activity.ListIssues(ctx.RequestContext(), id, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivityIssuesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]*activitysvcIssueData, len(items))
for i, v := range items {
res.List[i] = &activitysvcIssueData{
ID: v.ID,
IssueNumber: v.IssueNumber,
Status: v.Status,
Sort: v.Sort,
}
}
ctx.Payload(res)
}
}
type createIssueRequest struct {
IssueNumber string `json:"issue_number" binding:"required"`
Status int32 `json:"status"`
Sort int32 `json:"sort"`
}
type createIssueResp struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
type simpleMessage struct {
Message string `json:"message"`
}
// CreateActivityIssue 创建活动期数
// @Summary 创建活动期数
// @Description 为指定活动创建一个新的期数
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param RequestBody body createIssueRequest true "请求参数"
// @Success 200 {object} simpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues [post]
// @Security LoginVerifyToken
func (h *handler) CreateActivityIssue() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createIssueRequest)
res := new(createIssueResp)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityIssueError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
item, err := h.activity.CreateIssue(ctx.RequestContext(), id, activitysvc.CreateIssueInput{
IssueNumber: req.IssueNumber,
Status: req.Status,
Sort: req.Sort,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityIssueError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyIssueRequest struct {
IssueNumber string `json:"issue_number"`
Status int32 `json:"status"`
Sort int32 `json:"sort"`
}
// ModifyActivityIssue 修改活动期数
// @Summary 修改活动期数
// @Description 修改指定期数的信息
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param RequestBody body modifyIssueRequest true "请求参数"
// @Success 200 {object} simpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyActivityIssue() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyIssueRequest)
res := new(simpleMessage)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityIssueError, "禁止操作"))
return
}
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
if err := h.activity.ModifyIssue(ctx.RequestContext(), issueID, activitysvc.ModifyIssueInput{
IssueNumber: req.IssueNumber,
Status: req.Status,
Sort: req.Sort,
}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityIssueError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// DeleteActivityIssue 删除活动期数
// @Summary 删除活动期数
// @Description 删除指定期数
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Success 200 {object} simpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteActivityIssue() core.HandlerFunc {
return func(ctx core.Context) {
res := new(simpleMessage)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityIssueError, "禁止操作"))
return
}
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
if err := h.activity.DeleteIssue(ctx.RequestContext(), issueID); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityIssueError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,377 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type simpleMessageResponse struct {
Message string `json:"message"`
}
type createItemCardRequest struct {
Name string `json:"name" binding:"required"`
Status int32 `json:"status"`
CardType int32 `json:"card_type" binding:"required"`
ScopeType int32 `json:"scope_type" binding:"required"`
ActivityCategoryID int64 `json:"activity_category_id"`
ActivityID int64 `json:"activity_id"`
IssueID int64 `json:"issue_id"`
Price int64 `json:"price" binding:"required"`
ValidStartUnix *int64 `json:"valid_start_unix"`
ValidEndUnix *int64 `json:"valid_end_unix"`
EffectType int32 `json:"effect_type" binding:"required"`
RewardMultiplierX1000 int32 `json:"reward_multiplier_x1000"`
BoostRateX1000 int32 `json:"boost_rate_x1000"`
StackingStrategy int32 `json:"stacking_strategy"`
MaxEffectValueX1000 int32 `json:"max_effect_value_x1000"`
Remark string `json:"remark"`
}
type createItemCardResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateSystemItemCard 创建道具卡
// @Summary 创建道具卡
// @Description 管理员创建新的道具卡,支持设置类型、效果、有效期等属性
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param RequestBody body createItemCardRequest true "创建道具卡请求参数"
// @Success 200 {object} createItemCardResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards [post]
func (h *handler) CreateSystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createItemCardRequest)
res := new(createItemCardResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
item := &model.SystemItemCards{
Name: req.Name,
Status: func() int32 {
if req.Status == 0 {
return 1
}
return req.Status
}(),
CardType: req.CardType,
ScopeType: req.ScopeType,
ActivityCategoryID: req.ActivityCategoryID,
ActivityID: req.ActivityID,
IssueID: req.IssueID,
Price: req.Price,
EffectType: req.EffectType,
RewardMultiplierX1000: req.RewardMultiplierX1000,
BoostRateX1000: req.BoostRateX1000,
StackingStrategy: func() int32 {
if req.StackingStrategy == 0 {
return 1
}
return req.StackingStrategy
}(),
MaxEffectValueX1000: req.MaxEffectValueX1000,
Remark: req.Remark,
}
do := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext())
if req.ValidStartUnix != nil {
item.ValidStart = time.Unix(*req.ValidStartUnix, 0)
}
if req.ValidEndUnix != nil {
item.ValidEnd = time.Unix(*req.ValidEndUnix, 0)
} else {
do = do.Omit(h.writeDB.SystemItemCards.ValidEnd)
}
if err := do.Create(item); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyItemCardRequest struct {
Name *string `json:"name"`
Status *int32 `json:"status"`
CardType *int32 `json:"card_type"`
ScopeType *int32 `json:"scope_type"`
ActivityCategoryID *int64 `json:"activity_category_id"`
ActivityID *int64 `json:"activity_id"`
IssueID *int64 `json:"issue_id"`
Price *int64 `json:"price"`
ValidStartUnix *int64 `json:"valid_start_unix"`
ValidEndUnix *int64 `json:"valid_end_unix"`
EffectType *int32 `json:"effect_type"`
RewardMultiplierX1000 *int32 `json:"reward_multiplier_x1000"`
BoostRateX1000 *int32 `json:"boost_rate_x1000"`
StackingStrategy *int32 `json:"stacking_strategy"`
MaxEffectValueX1000 *int32 `json:"max_effect_value_x1000"`
Remark *string `json:"remark"`
}
// ModifySystemItemCard 修改道具卡
// @Summary 修改道具卡
// @Description 管理员修改道具卡信息,支持修改名称、价格、有效期等属性
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param item_card_id path integer true "道具卡ID"
// @Param RequestBody body modifyItemCardRequest true "修改道具卡请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards/{item_card_id} [put]
func (h *handler) ModifySystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyItemCardRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("item_card_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
set := map[string]any{}
if req.Name != nil {
set["name"] = *req.Name
}
if req.Status != nil {
set["status"] = *req.Status
}
if req.CardType != nil {
set["card_type"] = *req.CardType
}
if req.ScopeType != nil {
set["scope_type"] = *req.ScopeType
}
if req.ActivityCategoryID != nil {
set["activity_category_id"] = *req.ActivityCategoryID
}
if req.ActivityID != nil {
set["activity_id"] = *req.ActivityID
}
if req.IssueID != nil {
set["issue_id"] = *req.IssueID
}
if req.Price != nil {
set["price"] = *req.Price
}
if req.EffectType != nil {
set["effect_type"] = *req.EffectType
}
if req.RewardMultiplierX1000 != nil {
set["reward_multiplier_x1000"] = *req.RewardMultiplierX1000
}
if req.BoostRateX1000 != nil {
set["boost_rate_x1000"] = *req.BoostRateX1000
}
if req.StackingStrategy != nil {
set["stacking_strategy"] = *req.StackingStrategy
}
if req.MaxEffectValueX1000 != nil {
set["max_effect_value_x1000"] = *req.MaxEffectValueX1000
}
if req.Remark != nil {
set["remark"] = *req.Remark
}
if req.ValidStartUnix != nil {
set["valid_start"] = time.Unix(*req.ValidStartUnix, 0)
}
if req.ValidEndUnix != nil {
set["valid_end"] = time.Unix(*req.ValidEndUnix, 0)
}
if len(set) == 0 {
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
return
}
if _, err := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemItemCards.ID.Eq(id)).Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}
// DeleteSystemItemCard 删除道具卡
// @Summary 删除道具卡
// @Description 管理员删除指定的道具卡
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param item_card_id path integer true "道具卡ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards/{item_card_id} [delete]
func (h *handler) DeleteSystemItemCard() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("item_card_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if _, err := h.writeDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemItemCards.ID.Eq(id)).Delete(); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}
type listItemCardsRequest struct {
Name string `form:"name"`
Status int32 `form:"status"`
CardType int32 `form:"card_type"`
ScopeType int32 `form:"scope_type"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type itemCardListItem struct {
*model.SystemItemCards
}
type listItemCardsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []itemCardListItem `json:"list"`
}
// ListSystemItemCards 获取道具卡列表
// @Summary 获取道具卡列表
// @Description 管理员获取道具卡列表,支持按名称、状态、类型等条件筛选
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param name query string false "道具卡名称"
// @Param status query integer false "状态1启用 2禁用"
// @Param card_type query integer false "道具卡类型1抽奖卡 2加成卡 3保底卡"
// @Param scope_type query integer false "适用范围1全局 2活动分类 3活动 4期次"
// @Param page query integer false "页码默认1"
// @Param page_size query integer false "每页条数默认10"
// @Success 200 {object} listItemCardsResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/system_item_cards [get]
func (h *handler) ListSystemItemCards() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listItemCardsRequest)
res := new(listItemCardsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
q := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).ReadDB()
if req.Name != "" {
q = q.Where(h.readDB.SystemItemCards.Name.Like("%" + req.Name + "%"))
}
if req.Status != 0 {
q = q.Where(h.readDB.SystemItemCards.Status.Eq(req.Status))
}
if req.CardType != 0 {
q = q.Where(h.readDB.SystemItemCards.CardType.Eq(req.CardType))
}
if req.ScopeType != 0 {
q = q.Where(h.readDB.SystemItemCards.ScopeType.Eq(req.ScopeType))
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
rows, err := q.Order(h.readDB.SystemItemCards.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]itemCardListItem, len(rows))
for i, r := range rows {
res.List[i] = itemCardListItem{SystemItemCards: r}
}
ctx.Payload(res)
}
}
type assignItemCardRequest struct {
CardID int64 `json:"card_id" binding:"required"`
Quantity int `json:"quantity"`
}
// AssignUserItemCard 给用户分配道具卡
// @Summary 给用户分配道具卡
// @Description 管理员给指定用户分配道具卡,可指定数量
// @Tags 管理端.运营管理
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body assignItemCardRequest true "分配道具卡请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 403 {object} code.Failure "无权限,仅超管可操作"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/admin/users/{user_id}/item_cards [post]
func (h *handler) AssignUserItemCard() core.HandlerFunc {
return func(ctx core.Context) {
req := new(assignItemCardRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.user.AddItemCard(ctx.RequestContext(), userID, req.CardID, req.Quantity); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(simpleMessageResponse{Message: "操作成功"})
}
}

View File

@ -3,17 +3,12 @@ package admin
import (
"fmt"
"net/http"
"time"
"mini-chat/configs"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/jwtoken"
"mini-chat/internal/pkg/utils"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/proposal"
"gorm.io/gorm"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/utils"
"bindbox-game/internal/pkg/validation"
adminsvc "bindbox-game/internal/service/admin"
)
type loginRequest struct {
@ -49,70 +44,12 @@ func (h *handler) Login() core.HandlerFunc {
return
}
// 验证用户是否存在
info, err := h.readDB.Admin.WithContext(ctx.RequestContext()).Where(h.readDB.Admin.Username.Eq(req.Username)).First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), err.Error())),
)
return
}
if err == gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), "账号不存在,请联系管理员。")),
)
return
}
// 验证登录状态
if info.LoginStatus != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s账号已被禁用", code.Text(code.AdminLoginError))),
)
return
}
// 验证密码
if !utils.VerifyAdminHashedPassword(info.Password, req.Password) {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s用户名或密码错误请联系管理员。", code.Text(code.AdminLoginError))),
)
return
}
// 设置 Session 信息
sessionUserInfo := proposal.SessionUserInfo{
Id: info.ID,
UserName: info.Username,
NickName: info.Nickname,
IsSuper: info.IsSuper,
Platform: "管理端",
}
// 设置载荷数据、有效期,生成 JWT Token String
tokenString, err := jwtoken.New(configs.Get().JWT.AdminSecret).Sign(sessionUserInfo, 3*24*time.Hour)
result, err := h.svc.Login(ctx.RequestContext(), adminsvc.LoginInput{
Username: req.Username,
Password: req.Password,
IP: utils.GetIP(ctx.Request()),
})
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%stoken 生成失败(%s)", code.Text(code.AdminLoginError), err.Error())),
)
return
}
info.LastLoginTime = time.Now()
info.LastLoginIP = utils.GetIP(ctx.Request())
info.LastLoginHash = utils.MD5(tokenString)
if err := h.writeDB.Admin.WithContext(ctx.RequestContext()).Save(info); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
@ -121,8 +58,8 @@ func (h *handler) Login() core.HandlerFunc {
return
}
res.Token = tokenString
res.IsSuper = info.IsSuper
res.Token = result.Token
res.IsSuper = result.IsSuper
ctx.Payload(res)
}
}

View File

@ -0,0 +1,182 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
prodsvc "bindbox-game/internal/service/product"
)
type createProductCategoryRequest struct {
Name string `json:"name" binding:"required"`
ParentID int64 `json:"parent_id"`
Status int32 `json:"status"`
}
type createProductCategoryResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateProductCategory 创建商品分类
// @Summary 创建商品分类
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param RequestBody body createProductCategoryRequest true "请求参数"
// @Success 200 {object} createProductCategoryResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/product_categories [post]
// @Security LoginVerifyToken
func (h *handler) CreateProductCategory() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createProductCategoryRequest)
res := new(createProductCategoryResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
item, err := h.product.CreateCategory(ctx.RequestContext(), prodsvc.CreateCategoryInput{
Name: req.Name, ParentID: req.ParentID, Status: req.Status,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyProductCategoryRequest struct {
Name *string `json:"name"`
ParentID *int64 `json:"parent_id"`
Status *int32 `json:"status"`
}
type pcSimpleMessage struct {
Message string `json:"message"`
}
// ModifyProductCategory 修改商品分类
// @Summary 修改商品分类
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param category_id path string true "分类ID"
// @Param RequestBody body modifyProductCategoryRequest true "请求参数"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/product_categories/{category_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyProductCategory() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyProductCategoryRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("category_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.product.ModifyCategory(ctx.RequestContext(), id, prodsvc.ModifyCategoryInput{Name: req.Name, ParentID: req.ParentID, Status: req.Status}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
// DeleteProductCategory 删除商品分类
// @Summary 删除商品分类
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param category_id path string true "分类ID"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/product_categories/{category_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteProductCategory() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("category_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.product.DeleteCategory(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
type listProductCategoriesRequest struct {
Name string `form:"name"`
Status *int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type productCategoryListItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
ParentID int64 `json:"parent_id"`
Status int32 `json:"status"`
}
type listProductCategoriesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []productCategoryListItem `json:"list"`
}
// ListProductCategories 查看商品分类列表
// @Summary 查看商品分类列表
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param name query string false "名称"
// @Param status query int false "状态"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量" default(20)
// @Success 200 {object} listProductCategoriesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/product_categories [get]
// @Security LoginVerifyToken
func (h *handler) ListProductCategories() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listProductCategoriesRequest)
res := new(listProductCategoriesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
items, total, err := h.product.ListCategories(ctx.RequestContext(), prodsvc.ListCategoriesInput{Name: req.Name, Status: req.Status, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]productCategoryListItem, len(items))
for i, it := range items {
res.List[i] = productCategoryListItem{ID: it.ID, Name: it.Name, ParentID: it.ParentID, Status: it.Status}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,188 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
prodsvc "bindbox-game/internal/service/product"
)
type createProductRequest struct {
Name string `json:"name" binding:"required"`
CategoryID int64 `json:"category_id" binding:"required"`
ImagesJSON string `json:"images_json"`
Price int64 `json:"price" binding:"required"`
Stock int64 `json:"stock" binding:"required"`
Status int32 `json:"status"`
}
type createProductResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
// CreateProduct 创建商品
// @Summary 创建商品
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param RequestBody body createProductRequest true "请求参数"
// @Success 200 {object} createProductResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/products [post]
// @Security LoginVerifyToken
func (h *handler) CreateProduct() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createProductRequest)
res := new(createProductResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
item, err := h.product.CreateProduct(ctx.RequestContext(), prodsvc.CreateProductInput{Name: req.Name, CategoryID: req.CategoryID, ImagesJSON: req.ImagesJSON, Price: req.Price, Stock: req.Stock, Status: req.Status})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyProductRequest struct {
Name *string `json:"name"`
CategoryID *int64 `json:"category_id"`
ImagesJSON *string `json:"images_json"`
Price *int64 `json:"price"`
Stock *int64 `json:"stock"`
Status *int32 `json:"status"`
}
// ModifyProduct 修改商品
// @Summary 修改商品
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param product_id path string true "商品ID"
// @Param RequestBody body modifyProductRequest true "请求参数"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/products/{product_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyProduct() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyProductRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("product_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.product.ModifyProduct(ctx.RequestContext(), id, prodsvc.ModifyProductInput{Name: req.Name, CategoryID: req.CategoryID, ImagesJSON: req.ImagesJSON, Price: req.Price, Stock: req.Stock, Status: req.Status}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
// DeleteProduct 删除商品
// @Summary 删除商品
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param product_id path string true "商品ID"
// @Success 200 {object} pcSimpleMessage
// @Failure 400 {object} code.Failure
// @Router /api/admin/products/{product_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteProduct() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("product_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if err := h.product.DeleteProduct(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
type listProductsRequest struct {
Name string `form:"name"`
CategoryID *int64 `form:"category_id"`
Status *int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type productItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
CategoryID int64 `json:"category_id"`
ImagesJSON string `json:"images_json"`
Price int64 `json:"price"`
Stock int64 `json:"stock"`
Sales int64 `json:"sales"`
Status int32 `json:"status"`
}
type listProductsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []productItem `json:"list"`
}
// ListProducts 查看商品列表
// @Summary 查看商品列表
// @Tags 管理端.商品
// @Accept json
// @Produce json
// @Param name query string false "名称"
// @Param category_id query int false "分类ID"
// @Param status query int false "状态"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量" default(20)
// @Success 200 {object} listProductsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/products [get]
// @Security LoginVerifyToken
func (h *handler) ListProducts() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listProductsRequest)
res := new(listProductsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
items, total, err := h.product.ListProducts(ctx.RequestContext(), prodsvc.ListProductsInput{Name: req.Name, CategoryID: req.CategoryID, Status: req.Status, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]productItem, len(items))
for i, it := range items {
res.List[i] = productItem{ID: it.ID, Name: it.Name, CategoryID: it.CategoryID, ImagesJSON: it.ImagesJSON, Price: it.Price, Stock: it.Stock, Sales: it.Sales, Status: it.Status}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,224 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
activitysvc "bindbox-game/internal/service/activity"
)
type rewardItem struct {
ID int64 `json:"id"`
ProductID int64 `json:"product_id"`
Name string `json:"name" binding:"required"`
Weight int32 `json:"weight" binding:"required"`
Quantity int64 `json:"quantity" binding:"required"`
OriginalQty int64 `json:"original_qty" binding:"required"`
Level int32 `json:"level" binding:"required"`
Sort int32 `json:"sort"`
IsBoss int32 `json:"is_boss"`
}
type createRewardsRequest struct {
Rewards []rewardItem `json:"rewards" binding:"required"`
}
type listRewardsResponse struct {
List []rewardItem `json:"list"`
}
// CreateIssueRewards 创建期数奖品
// @Summary 创建期数奖品
// @Description 为指定期数批量创建奖励配置
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param RequestBody body createRewardsRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id}/rewards [post]
// @Security LoginVerifyToken
func (h *handler) CreateIssueRewards() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createRewardsRequest)
res := new(simpleMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, "禁止操作"))
return
}
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
var rewards []activitysvc.CreateRewardInput
for _, r := range req.Rewards {
rewards = append(rewards, activitysvc.CreateRewardInput{
ProductID: r.ProductID,
Name: r.Name,
Weight: r.Weight,
Quantity: r.Quantity,
OriginalQty: r.OriginalQty,
Level: r.Level,
Sort: r.Sort,
IsBoss: r.IsBoss,
})
}
if err := h.activity.CreateIssueRewards(ctx.RequestContext(), issueID, rewards); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// ListIssueRewards 查看期数奖品
// @Summary 查看期数奖品
// @Description 查看指定期数的奖励配置列表
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Success 200 {object} listRewardsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id}/rewards [get]
// @Security LoginVerifyToken
func (h *handler) ListIssueRewards() core.HandlerFunc {
return func(ctx core.Context) {
issueID, err := strconv.ParseInt(ctx.Param("issue_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
items, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListIssueRewardsError, err.Error()))
return
}
res := new(listRewardsResponse)
res.List = make([]rewardItem, len(items))
for i, v := range items {
res.List[i] = rewardItem{
ID: v.ID,
ProductID: v.ProductID,
Name: v.Name,
Weight: v.Weight,
Quantity: v.Quantity,
OriginalQty: v.OriginalQty,
Level: v.Level,
Sort: v.Sort,
IsBoss: v.IsBoss,
}
}
ctx.Payload(res)
}
}
type modifyRewardRequest struct {
ProductID *int64 `json:"product_id"`
Name string `json:"name"`
Weight *int32 `json:"weight"`
Quantity *int64 `json:"quantity"`
OriginalQty *int64 `json:"original_qty"`
Level *int32 `json:"level"`
Sort *int32 `json:"sort"`
IsBoss *int32 `json:"is_boss"`
}
// ModifyIssueReward 更新期数奖励
// @Summary 更新期数奖励
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param reward_id path integer true "奖励ID"
// @Param RequestBody body modifyRewardRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id}/rewards/{reward_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyIssueReward() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyRewardRequest)
res := new(simpleMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, "禁止操作"))
return
}
rewardID, err := strconv.ParseInt(ctx.Param("reward_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递奖励ID"))
return
}
in := activitysvc.ModifyRewardInput{
ProductID: req.ProductID,
Name: req.Name,
Weight: req.Weight,
Quantity: req.Quantity,
OriginalQty: req.OriginalQty,
Level: req.Level,
Sort: req.Sort,
IsBoss: req.IsBoss,
}
if err := h.activity.ModifyIssueReward(ctx.RequestContext(), rewardID, in); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// DeleteIssueReward 删除期数奖励
// @Summary 删除期数奖励
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param reward_id path integer true "奖励ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/issues/{issue_id}/rewards/{reward_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteIssueReward() core.HandlerFunc {
return func(ctx core.Context) {
res := new(simpleMessageResponse)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, "禁止操作"))
return
}
rewardID, err := strconv.ParseInt(ctx.Param("reward_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递奖励ID"))
return
}
if err := h.activity.DeleteIssueReward(ctx.RequestContext(), rewardID); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateIssueRewardsError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -0,0 +1,265 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type createSystemCouponRequest struct {
Name string `json:"name" binding:"required"`
Status *int32 `json:"status"`
CouponType *int32 `json:"coupon_type"`
DiscountType *int32 `json:"discount_type"`
DiscountValue *int64 `json:"discount_value"`
MinAmount *int64 `json:"min_amount"`
ValidDays *int `json:"valid_days"`
TotalQuantity *int64 `json:"total_quantity"`
}
type createSystemCouponResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
func (h *handler) CreateSystemCoupon() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createSystemCouponRequest)
res := new(createSystemCouponResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
now := time.Now()
m := &model.SystemCoupons{
Name: req.Name,
ScopeType: getInt32OrDefault(req.CouponType, 1),
DiscountType: getInt32OrDefault(req.DiscountType, 1),
DiscountValue: getInt64OrDefault(req.DiscountValue, 0),
MinSpend: getInt64OrDefault(req.MinAmount, 0),
Status: getInt32OrDefault(req.Status, 1),
}
if req.ValidDays != nil && *req.ValidDays > 0 {
m.ValidStart = now
m.ValidEnd = now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
}
if err := h.writeDB.SystemCoupons.WithContext(ctx.RequestContext()).Create(m); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
if req.TotalQuantity != nil {
if _, err := h.writeDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemCoupons.ID.Eq(m.ID)).Updates(map[string]any{"total_quantity": *req.TotalQuantity}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
}
res.ID = m.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifySystemCouponRequest struct {
Name *string `json:"name"`
Status *int32 `json:"status"`
CouponType *int32 `json:"coupon_type"`
DiscountType *int32 `json:"discount_type"`
DiscountValue *int64 `json:"discount_value"`
MinAmount *int64 `json:"min_amount"`
ValidDays *int `json:"valid_days"`
TotalQuantity *int64 `json:"total_quantity"`
}
func (h *handler) ModifySystemCoupon() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifySystemCouponRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
idStr := ctx.Param("coupon_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
set := map[string]any{}
if req.Name != nil {
set["name"] = *req.Name
}
if req.Status != nil {
set["status"] = *req.Status
}
if req.CouponType != nil {
set["scope_type"] = *req.CouponType
}
if req.DiscountType != nil {
set["discount_type"] = *req.DiscountType
}
if req.DiscountValue != nil {
set["discount_value"] = *req.DiscountValue
}
if req.MinAmount != nil {
set["min_spend"] = *req.MinAmount
}
if req.ValidDays != nil && *req.ValidDays > 0 {
now := time.Now()
set["valid_start"] = now
set["valid_end"] = now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
}
if req.TotalQuantity != nil {
set["total_quantity"] = *req.TotalQuantity
}
if len(set) == 0 {
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
return
}
if _, err := h.writeDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemCoupons.ID.Eq(id)).Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
func (h *handler) DeleteSystemCoupon() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("coupon_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
return
}
if _, err := h.writeDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemCoupons.ID.Eq(id)).Delete(); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
return
}
ctx.Payload(pcSimpleMessage{Message: "操作成功"})
}
}
type listSystemCouponsRequest struct {
Name string `form:"name"`
Status *int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type systemCouponItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status int32 `json:"status"`
CouponType int32 `json:"coupon_type"`
DiscountType int32 `json:"discount_type"`
DiscountValue int64 `json:"discount_value"`
MinAmount int64 `json:"min_amount"`
MaxDiscount int64 `json:"max_discount"`
ValidDays int `json:"valid_days"`
TotalQuantity int64 `json:"total_quantity"`
UsedQuantity int64 `json:"used_quantity"`
CreatedAt string `json:"created_at"`
}
type listSystemCouponsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []systemCouponItem `json:"list"`
}
func (h *handler) ListSystemCoupons() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listSystemCouponsRequest)
res := new(listSystemCouponsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
q := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).ReadDB()
if req.Name != "" {
q = q.Where(h.readDB.SystemCoupons.Name.Like("%" + req.Name + "%"))
}
if req.Status != nil {
q = q.Where(h.readDB.SystemCoupons.Status.Eq(*req.Status))
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
type dbRow struct {
ID int64
Name string
Status int32
ScopeType int32
DiscountType int32
DiscountValue int64
MinSpend int64
ValidStart time.Time
ValidEnd time.Time
CreatedAt time.Time
TotalQuantity int64
}
var rows []dbRow
if err := q.Order(h.readDB.SystemCoupons.ID.Desc()).Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize).Scan(&rows); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]systemCouponItem, len(rows))
for i, v := range rows {
validDays := 0
if !v.ValidStart.IsZero() && !v.ValidEnd.IsZero() {
diff := v.ValidEnd.Sub(v.ValidStart)
validDays = int(diff.Hours() / 24)
}
// 统计已发放数量(用户持券数)
usedCnt, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.CouponID.Eq(v.ID)).Count()
res.List[i] = systemCouponItem{
ID: v.ID,
Name: v.Name,
Status: v.Status,
CouponType: v.ScopeType,
DiscountType: v.DiscountType,
DiscountValue: v.DiscountValue,
MinAmount: v.MinSpend,
MaxDiscount: 0,
ValidDays: validDays,
TotalQuantity: v.TotalQuantity,
UsedQuantity: usedCnt,
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
ctx.Payload(res)
}
}
func getInt32OrDefault(v *int32, d int32) int32 {
if v != nil {
return *v
}
return d
}
func getInt64OrDefault(v *int64, d int64) int64 {
if v != nil {
return *v
}
return d
}

View File

@ -0,0 +1,272 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/repository/mysql/model"
)
type simpleRoute struct {
Path string `json:"path"`
Name string `json:"name"`
Component string `json:"component"`
Meta map[string]any `json:"meta"`
Children []simpleRoute `json:"children,omitempty"`
}
// ListSimpleMenus 返回前端所需的简单路由结构
// @Router /api/v3/system/menus/simple [get]
func (h *handler) ListSimpleMenus() core.HandlerFunc {
return func(ctx core.Context) {
// 读取启用的菜单,构造成前端路由结构
rows, err := h.readDB.Menus.Where(h.readDB.Menus.Status.Is(true)).Order(h.readDB.Menus.Sort).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10021, err.Error()))
return
}
// 构建 parent_id => children
nodeMap := make(map[uint64]*simpleRoute)
var roots []simpleRoute
for _, m := range rows {
r := &simpleRoute{
Path: m.Path,
Name: m.Name,
Component: m.Component,
Meta: map[string]any{
"title": m.Name,
"keepAlive": m.KeepAlive,
"isHide": m.IsHide,
"isHideTab": m.IsHideTab,
"icon": m.Icon,
},
}
nodeMap[uint64(m.ID)] = r
}
// 第二遍挂载到父节点
for _, m := range rows {
id := uint64(m.ID)
pid := uint64(m.ParentID)
if pid == 0 {
roots = append(roots, *nodeMap[id])
continue
}
parent := nodeMap[pid]
if parent != nil {
parent.Children = append(parent.Children, *nodeMap[id])
} else {
// 无父节点,作为根返回
roots = append(roots, *nodeMap[id])
}
}
ctx.Payload(roots)
}
}
type createMenuRequest struct {
ParentID uint64 `json:"parent_id"`
Path string `json:"path"`
Name string `json:"name"`
Component string `json:"component"`
Icon string `json:"icon"`
Sort int `json:"sort"`
Status bool `json:"status"`
KeepAlive bool `json:"keep_alive"`
IsHide bool `json:"is_hide"`
IsHideTab bool `json:"is_hide_tab"`
}
func (h *handler) CreateMenu() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createMenuRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10031, err.Error()))
return
}
err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Create(&model.Menus{ParentID: int64(req.ParentID), Path: req.Path, Name: req.Name, Component: req.Component, Icon: req.Icon, Sort: int32(req.Sort), Status: req.Status, KeepAlive: req.KeepAlive, IsHide: req.IsHide, IsHideTab: req.IsHideTab})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10032, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
type modifyMenuRequest struct {
ParentID *uint64 `json:"parent_id"`
Path *string `json:"path"`
Name *string `json:"name"`
Component *string `json:"component"`
Icon *string `json:"icon"`
Sort *int `json:"sort"`
Status *bool `json:"status"`
KeepAlive *bool `json:"keep_alive"`
IsHide *bool `json:"is_hide"`
IsHideTab *bool `json:"is_hide_tab"`
}
func (h *handler) ModifyMenu() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("menu_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
req := new(modifyMenuRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10033, err.Error()))
return
}
set := map[string]any{}
if req.ParentID != nil {
set["parent_id"] = int64(*req.ParentID)
}
if req.Path != nil {
set["path"] = *req.Path
}
if req.Name != nil {
set["name"] = *req.Name
}
if req.Component != nil {
set["component"] = *req.Component
}
if req.Icon != nil {
set["icon"] = *req.Icon
}
if req.Sort != nil {
set["sort"] = *req.Sort
}
if req.Status != nil {
set["status"] = *req.Status
}
if req.KeepAlive != nil {
set["keep_alive"] = *req.KeepAlive
}
if req.IsHide != nil {
set["is_hide"] = *req.IsHide
}
if req.IsHideTab != nil {
set["is_hide_tab"] = *req.IsHideTab
}
if _, err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Where(h.writeDB.Menus.ID.Eq(id)).Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10034, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) DeleteMenu() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("menu_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if _, err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Where(h.writeDB.Menus.ID.Eq(id)).Delete(); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10035, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) ListMenuActions() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("menu_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
rows, err := h.readDB.MenuActions.WithContext(ctx.RequestContext()).Where(h.readDB.MenuActions.MenuID.Eq(id)).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10036, err.Error()))
return
}
ctx.Payload(rows)
}
}
type createActionsRequest struct {
Actions []struct {
ActionMark string `json:"action_mark"`
ActionName string `json:"action_name"`
Status bool `json:"status"`
} `json:"actions"`
}
func (h *handler) CreateMenuActions() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("menu_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
req := new(createActionsRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10037, err.Error()))
return
}
tx := h.writeDB.MenuActions.WithContext(ctx.RequestContext())
for _, a := range req.Actions {
err := tx.Create(&model.MenuActions{MenuID: id, ActionMark: a.ActionMark, ActionName: a.ActionName, Status: a.Status})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10038, err.Error()))
return
}
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) DeleteMenuAction() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("menu_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
aidStr := ctx.Param("action_id")
aid, _ := strconv.ParseInt(aidStr, 10, 64)
if _, err := h.writeDB.MenuActions.WithContext(ctx.RequestContext()).Where(h.writeDB.MenuActions.ID.Eq(aid), h.writeDB.MenuActions.MenuID.Eq(id)).Delete(); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10039, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
type assignMenusRequest struct {
MenuIDs []int64 `json:"menu_ids"`
}
type assignActionsRequest struct {
ActionIDs []int64 `json:"action_ids"`
}
func (h *handler) AssignRoleMenus() core.HandlerFunc {
return func(ctx core.Context) {
ridStr := ctx.Param("role_id")
rid, _ := strconv.ParseInt(ridStr, 10, 64)
req := new(assignMenusRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10041, err.Error()))
return
}
tx := h.writeDB.RoleMenus.WithContext(ctx.RequestContext())
for _, mid := range req.MenuIDs {
err := tx.Create(&model.RoleMenus{RoleID: rid, MenuID: mid})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10042, err.Error()))
return
}
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) AssignRoleActions() core.HandlerFunc {
return func(ctx core.Context) {
ridStr := ctx.Param("role_id")
rid, _ := strconv.ParseInt(ridStr, 10, 64)
req := new(assignActionsRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10043, err.Error()))
return
}
tx := h.writeDB.RoleActions.WithContext(ctx.RequestContext())
for _, aid := range req.ActionIDs {
err := tx.Create(&model.RoleActions{RoleID: rid, ActionID: aid})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10044, err.Error()))
return
}
}
ctx.Payload(map[string]string{"message": "ok"})
}
}

View File

@ -0,0 +1,227 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type roleListRequest struct {
RoleId int64 `form:"roleId"`
RoleName string `form:"roleName"`
RoleCode string `form:"roleCode"`
Description string `form:"description"`
Enabled *bool `form:"enabled"`
Current int `form:"current"`
Size int `form:"size"`
}
type roleItem struct {
RoleId int64 `json:"roleId"`
RoleName string `json:"roleName"`
RoleCode string `json:"roleCode"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
CreateTime string `json:"createTime"`
}
type roleListResponse struct {
Records []roleItem `json:"records"`
Current int `json:"current"`
Size int `json:"size"`
Total int64 `json:"total"`
}
// ListRoles 角色列表
// @Summary 角色列表
// @Tags 管理端.系统
// @Accept json
// @Produce json
// @Param current query int true "页码" default(1)
// @Param size query int true "每页数量" default(20)
// @Param roleName query string false "角色名称"
// @Param roleCode query string false "角色编码"
// @Param enabled query bool false "是否启用"
// @Success 200 {object} roleListResponse
// @Router /api/role/list [get]
// @Security LoginVerifyToken
func (h *handler) ListRoles() core.HandlerFunc {
return func(ctx core.Context) {
req := new(roleListRequest)
res := new(roleListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10011, validation.Error(err)))
return
}
if req.Current <= 0 {
req.Current = 1
}
if req.Size <= 0 {
req.Size = 20
}
q := h.readDB.Roles.WithContext(ctx.RequestContext()).ReadDB()
if req.RoleName != "" {
q = q.Where(h.readDB.Roles.RoleName.Like("%" + req.RoleName + "%"))
}
if req.RoleCode != "" {
q = q.Where(h.readDB.Roles.RoleCode.Like("%" + req.RoleCode + "%"))
}
if req.Enabled != nil {
q = q.Where(h.readDB.Roles.Enabled.Is(*req.Enabled))
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10012, err.Error()))
return
}
rows, err := q.Offset((req.Current - 1) * req.Size).Limit(req.Size).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10013, err.Error()))
return
}
res.Records = make([]roleItem, len(rows))
for i, r := range rows {
res.Records[i] = roleItem{
RoleId: r.ID,
RoleName: r.RoleName,
RoleCode: r.RoleCode,
Description: r.Description,
Enabled: r.Enabled,
CreateTime: r.CreatedAt.Format("2006-01-02 15:04:05"),
}
}
res.Current = req.Current
res.Size = req.Size
res.Total = total
ctx.Payload(res)
}
}
type createRoleRequest struct {
RoleName string `json:"roleName" binding:"required"`
RoleCode string `json:"roleCode" binding:"required"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
}
func (h *handler) CreateRole() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createRoleRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10014, validation.Error(err)))
return
}
err := h.writeDB.Roles.WithContext(ctx.RequestContext()).Create(&model.Roles{RoleName: req.RoleName, RoleCode: req.RoleCode, Description: req.Description, Enabled: req.Enabled})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10015, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
type modifyRoleRequest struct {
RoleName *string `json:"roleName"`
Description *string `json:"description"`
Enabled *bool `json:"enabled"`
}
func (h *handler) ModifyRole() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyRoleRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10016, validation.Error(err)))
return
}
idStr := ctx.Param("role_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
updater := h.writeDB.Roles.WithContext(ctx.RequestContext()).Where(h.writeDB.Roles.ID.Eq(id))
set := map[string]any{}
if req.RoleName != nil {
set["role_name"] = *req.RoleName
}
if req.Description != nil {
set["description"] = *req.Description
}
if req.Enabled != nil {
set["enabled"] = *req.Enabled
}
if len(set) == 0 {
ctx.Payload(map[string]string{"message": "ok"})
return
}
if _, err := updater.Updates(set); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10017, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) DeleteRole() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("role_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
if _, err := h.writeDB.Roles.WithContext(ctx.RequestContext()).Where(h.writeDB.Roles.ID.Eq(id)).Delete(); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10018, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
type assignUsersRequest struct {
AdminIDs []int64 `json:"admin_ids" binding:"required"`
}
func (h *handler) AssignRoleUsers() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("role_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
req := new(assignUsersRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10019, validation.Error(err)))
return
}
tx := h.writeDB.RoleUsers.WithContext(ctx.RequestContext())
for _, uid := range req.AdminIDs {
err := tx.Create(&model.RoleUsers{RoleID: id, AdminID: int32(uid)})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10020, err.Error()))
return
}
}
ctx.Payload(map[string]string{"message": "ok"})
}
}
func (h *handler) ListRoleUsers() core.HandlerFunc {
return func(ctx core.Context) {
idStr := ctx.Param("role_id")
id, _ := strconv.ParseInt(idStr, 10, 64)
rows, err := h.readDB.RoleUsers.WithContext(ctx.RequestContext()).Where(h.readDB.RoleUsers.RoleID.Eq(id)).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10022, err.Error()))
return
}
ctx.Payload(rows)
}
}
func (h *handler) RemoveRoleUser() core.HandlerFunc {
return func(ctx core.Context) {
roleIDStr := ctx.Param("role_id")
adminIDStr := ctx.Param("admin_id")
roleID, _ := strconv.ParseInt(roleIDStr, 10, 64)
adminID, _ := strconv.ParseInt(adminIDStr, 10, 64)
_, err := h.writeDB.RoleUsers.WithContext(ctx.RequestContext()).Where(h.writeDB.RoleUsers.RoleID.Eq(roleID), h.writeDB.RoleUsers.AdminID.Eq(int32(adminID))).Delete()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error()))
return
}
ctx.Payload(map[string]string{"message": "ok"})
}
}

View File

@ -0,0 +1,138 @@
package admin
import (
"net/http"
"time"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type userListRequest struct {
UserName string `form:"userName"`
UserGender string `form:"userGender"`
UserPhone string `form:"userPhone"`
UserEmail string `form:"userEmail"`
Status string `form:"status"`
Current int `form:"current"`
Size int `form:"size"`
}
type userListItem struct {
ID int32 `json:"id"`
Avatar string `json:"avatar"`
Status string `json:"status"`
UserName string `json:"userName"`
UserGender string `json:"userGender"`
NickName string `json:"nickName"`
UserPhone string `json:"userPhone"`
UserEmail string `json:"userEmail"`
UserRoles []string `json:"userRoles"`
CreateBy string `json:"createBy"`
CreateTime string `json:"createTime"`
UpdateBy string `json:"updateBy"`
UpdateTime string `json:"updateTime"`
}
type userListResponse struct {
Records []userListItem `json:"records"`
Current int `json:"current"`
Size int `json:"size"`
Total int64 `json:"total"`
}
// ListUsers 系统用户列表(映射到 Admin 表)
// @Summary 系统用户列表
// @Description 返回系统用户分页数据
// @Tags 管理端.系统
// @Accept json
// @Produce json
// @Param current query int true "页码" default(1)
// @Param size query int true "每页数量" default(20)
// @Param userName query string false "用户名"
// @Param userEmail query string false "邮箱"
// @Param userPhone query string false "手机号"
// @Param status query string false "状态"
// @Success 200 {object} userListResponse
// @Router /api/user/list [get]
// @Security LoginVerifyToken
func (h *handler) ListUsers() core.HandlerFunc {
return func(ctx core.Context) {
req := new(userListRequest)
res := new(userListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, validation.Error(err)))
return
}
if req.Current <= 0 {
req.Current = 1
}
if req.Size <= 0 {
req.Size = 20
}
q := h.readDB.Admin.WithContext(ctx.RequestContext()).ReadDB()
if req.UserName != "" {
q = q.Where(h.readDB.Admin.Username.Like("%" + req.UserName + "%"))
}
if req.UserEmail != "" {
q = q.Where(h.readDB.Admin.Nickname.Like("%" + req.UserEmail + "%"))
}
if req.UserPhone != "" {
q = q.Where(h.readDB.Admin.Mobile.Like("%" + req.UserPhone + "%"))
}
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, err.Error()))
return
}
admins, err := q.Offset((req.Current - 1) * req.Size).Limit(req.Size).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
return
}
res.Records = make([]userListItem, len(admins))
for i, a := range admins {
res.Records[i] = userListItem{
ID: a.ID,
Avatar: a.Avatar,
Status: mapLoginStatus(a.LoginStatus),
UserName: a.Username,
UserGender: "未知",
NickName: a.Nickname,
UserPhone: a.Mobile,
UserEmail: "",
UserRoles: rolesFromAdmin(a.IsSuper),
CreateBy: a.CreatedUser,
CreateTime: a.CreatedAt.Format(time.RFC3339),
UpdateBy: a.UpdatedUser,
UpdateTime: a.UpdatedAt.Format(time.RFC3339),
}
}
res.Current = req.Current
res.Size = req.Size
res.Total = total
ctx.Payload(res)
}
}
func mapLoginStatus(s int32) string {
switch s {
case 1:
return "1"
case 0:
return "4"
default:
return "2"
}
}
func rolesFromAdmin(isSuper int32) []string {
if isSuper == 1 {
return []string{"R_SUPER"}
}
return []string{"R_ADMIN"}
}

View File

@ -0,0 +1,536 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
"bindbox-game/internal/service/user"
)
type listUsersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listUsersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserItem `json:"list"`
}
// ListAppUsers 管理端用户列表
// @Summary 管理端用户列表
// @Description 查看APP端用户分页列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listUsersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users [get]
// @Security LoginVerifyToken
func (h *handler) ListAppUsers() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listUsersRequest)
rsp := new(listUsersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
req.PageSize = 100
}
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB()
total, err := q.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20101, err.Error()))
return
}
rows, err := q.Order(h.readDB.Users.ID.Desc()).Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20102, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserItem{ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05")}
}
ctx.Payload(rsp)
}
}
type listInvitesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listInvitesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserItem `json:"list"`
}
// ListUserInvites 查看用户邀请列表
// @Summary 查看用户邀请列表
// @Description 查看指定用户邀请的用户列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listInvitesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/invites [get]
// @Security LoginVerifyToken
func (h *handler) ListUserInvites() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInvitesRequest)
rsp := new(listInvitesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
rows, total, err := h.user.ListInvites(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20103, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserItem{ID: v.ID, Nickname: v.Nickname, Avatar: v.Avatar, InviteCode: v.InviteCode, InviterID: v.InviterID, CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05")}
}
ctx.Payload(rsp)
}
}
type listOrdersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listOrdersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*user.OrderWithItems `json:"list"`
}
type listInventoryRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listInventoryResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*user.InventoryWithProduct `json:"list"`
}
// ListUserOrders 查看用户订单列表
// @Summary 查看用户订单列表
// @Description 查看指定用户的订单记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listOrdersResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/orders [get]
// @Security LoginVerifyToken
func (h *handler) ListUserOrders() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listOrdersRequest)
rsp := new(listOrdersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListOrdersWithItems(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20104, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
// 查看用户资产列表
// @Summary 查看用户资产列表
// @Description 查看指定用户的资产记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listInventoryResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/inventory [get]
// @Security LoginVerifyToken
func (h *handler) ListUserInventory() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInventoryRequest)
rsp := new(listInventoryResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
rows, total, err := h.user.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = rows
ctx.Payload(rsp)
}
}
type listCouponsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type adminUserCouponItem struct {
ID int64 `json:"id"`
CouponID int64 `json:"coupon_id"`
Status int32 `json:"status"`
UsedOrderID int64 `json:"used_order_id"`
UsedAt string `json:"used_at"`
ValidStart string `json:"valid_start"`
ValidEnd string `json:"valid_end"`
Name string `json:"name"`
ScopeType int32 `json:"scope_type"`
DiscountType int32 `json:"discount_type"`
DiscountValue int64 `json:"discount_value"`
MinSpend int64 `json:"min_spend"`
}
type listCouponsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []adminUserCouponItem `json:"list"`
}
// ListUserCoupons 查看用户优惠券列表
// @Summary 查看用户优惠券列表
// @Description 查看指定用户持有的优惠券列表
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listCouponsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/coupons [get]
// @Security LoginVerifyToken
func (h *handler) ListUserCoupons() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listCouponsRequest)
rsp := new(listCouponsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
// 统计总数
base := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.UserCoupons.UserID.Eq(userID))
total, err := base.Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
// 联表查询 system_coupons 获取优惠券模板信息
type row struct {
ID int64
CouponID int64
Status int32
UsedOrderID int64
UsedAt *string
ValidStart *string
ValidEnd *string
Name string
ScopeType int32
DiscountType int32
DiscountValue int64
MinSpend int64
}
q := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)).
Select(
h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status,
h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt, h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd,
h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType, h.readDB.SystemCoupons.DiscountType,
h.readDB.SystemCoupons.DiscountValue, h.readDB.SystemCoupons.MinSpend,
).
Where(h.readDB.UserCoupons.UserID.Eq(userID)).
Order(h.readDB.UserCoupons.ID.Desc()).
Limit(req.PageSize).Offset((req.Page-1)*req.PageSize)
var rows []row
if err := q.Scan(&rows); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = make([]adminUserCouponItem, len(rows))
for i, v := range rows {
rsp.List[i] = adminUserCouponItem{
ID: v.ID,
CouponID: v.CouponID,
Status: v.Status,
UsedOrderID: v.UsedOrderID,
UsedAt: nullableToString(v.UsedAt),
ValidStart: nullableToString(v.ValidStart),
ValidEnd: nullableToString(v.ValidEnd),
Name: v.Name,
ScopeType: v.ScopeType,
DiscountType: v.DiscountType,
DiscountValue: v.DiscountValue,
MinSpend: v.MinSpend,
}
}
ctx.Payload(rsp)
}
}
func nullableToString(s *string) string {
if s == nil {
return ""
}
return *s
}
type listPointsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listPointsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.UserPointsLedger `json:"list"`
}
// ListUserPoints 查看用户积分记录
// @Summary 查看用户积分记录
// @Description 查看指定用户的积分流水记录
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listPointsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points [get]
// @Security LoginVerifyToken
func (h *handler) ListUserPoints() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listPointsRequest)
rsp := new(listPointsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListPointsLedger(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20106, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
type pointsBalanceResponse struct {
Balance int64 `json:"balance"`
}
type adminUserItem struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
InviteCode string `json:"invite_code"`
InviterID int64 `json:"inviter_id"`
CreatedAt string `json:"created_at"`
}
// GetUserPointsBalance 查看用户积分余额
// @Summary 查看用户积分余额
// @Description 查看指定用户当前积分余额(过滤过期)
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Success 200 {object} pointsBalanceResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points/balance [get]
// @Security LoginVerifyToken
func (h *handler) GetUserPointsBalance() core.HandlerFunc {
return func(ctx core.Context) {
rsp := new(pointsBalanceResponse)
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
total, err := h.user.GetPointsBalance(ctx.RequestContext(), userID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
return
}
rsp.Balance = total
ctx.Payload(rsp)
}
}
type addPointsRequest struct {
Points int64 `json:"points" binding:"required"`
Kind string `json:"kind"`
Remark string `json:"remark"`
ValidDays *int `json:"valid_days"`
}
type addPointsResponse struct {
Success bool `json:"success"`
}
// AddUserPoints 给用户添加积分
// @Summary 给用户添加积分
// @Description 管理端为指定用户发放积分,支持设置有效期
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body addPointsRequest true "请求参数"
// @Success 200 {object} addPointsResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/points/add [post]
// @Security LoginVerifyToken
func (h *handler) AddUserPoints() core.HandlerFunc {
return func(ctx core.Context) {
req := new(addPointsRequest)
rsp := new(addPointsResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
var validStart *time.Time
var validEnd *time.Time
now := time.Now()
validStart = &now
if req.ValidDays != nil && *req.ValidDays > 0 {
ve := now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
validEnd = &ve
}
if err := h.user.AddPoints(ctx.RequestContext(), userID, req.Points, req.Kind, req.Remark, validStart, validEnd); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
return
}
rsp.Success = true
ctx.Payload(rsp)
}
}
type addCouponRequest struct {
CouponID int64 `json:"coupon_id" binding:"required"`
}
type addCouponResponse struct {
Success bool `json:"success"`
}
// AddUserCoupon 给用户添加优惠券
// @Summary 给用户添加优惠券
// @Description 管理端为指定用户发放优惠券
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body addCouponRequest true "请求参数"
// @Success 200 {object} addCouponResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/users/{user_id}/coupons/add [post]
// @Security LoginVerifyToken
func (h *handler) AddUserCoupon() core.HandlerFunc {
return func(ctx core.Context) {
req := new(addCouponRequest)
rsp := new(addCouponResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error()))
return
}
rsp.Success = true
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,75 @@
package admin
import (
"strconv"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/service/user"
"go.uber.org/zap"
)
// GrantRewardRequest 奖励发放请求复用service层的结构体
type GrantRewardRequest = user.GrantRewardRequest
// GrantRewardResponse 奖励发放响应复用service层的结构体
type GrantRewardResponse = user.GrantRewardResponse
// GrantReward 给用户发放奖励
// @Summary 给用户发放奖励
// @Description 管理员给用户发放奖励,支持实物和虚拟奖品,可选择关联活动和奖励配置
// @Tags 管理端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body GrantRewardRequest true "请求参数"
// @Success 200 {object} GrantRewardResponse
// @Failure 400 {object} code.Failure
// @Failure 401 {object} code.Failure
// @Failure 403 {object} code.Failure
// @Failure 500 {object} code.Failure
// @Router /api/admin/users/{user_id}/rewards/grant [post]
func (h *handler) GrantReward() core.HandlerFunc {
return func(ctx core.Context) {
userIDStr := ctx.Param("user_id")
if userIDStr == "" {
ctx.AbortWithError(core.Error(400, 40001, "用户ID不能为空"))
return
}
// 解析用户ID
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(400, 40001, "用户ID格式错误"))
return
}
// 权限检查 - 仅超管可以发放奖励
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(403, 40301, "无权限发放奖励"))
return
}
// 解析请求参数
var req GrantRewardRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
zap.L().Error("解析奖励发放请求失败", zap.Error(err))
ctx.AbortWithError(core.Error(400, 40001, "参数格式错误"))
return
}
// 调用服务层发放奖励
resp, err := h.user.GrantReward(ctx.RequestContext(), userID, user.GrantRewardRequest(req))
if err != nil {
zap.L().Error("发放奖励失败",
zap.Int64("user_id", userID),
zap.Error(err),
)
ctx.AbortWithError(core.Error(500, 50001, err.Error()))
return
}
// 返回成功响应
ctx.Payload(resp)
}
}

View File

@ -1,23 +0,0 @@
package app
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
db mysql.Repo
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
db: db,
}
}

View File

@ -1,114 +0,0 @@
package app
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/httpclient"
"mini-chat/internal/pkg/validation"
"go.uber.org/zap"
)
// checkAppStatusRequest 请求参数
// 功能描述用于检查指定小程序app_id的运行状态
// 参数说明:
// - AppID(string): 小程序ID必填通过query参数传入form:"app_id"
// 返回值该函数返回一个core.HandlerFunc处理器在HTTP请求完成后通过ctx.Payload返回JSON数据
type checkAppStatusRequest struct {
AppID string `form:"app_id" binding:"required"` // 小程序ID
}
// checkAppStatusResponse 响应数据结构
// 功能描述:返回外部验证服务的状态码与状态文本,并提供统一的文字描述
// 字段说明:
// - AppID(string): 小程序ID
// - Code(int): 外部接口返回的状态码1表示正常0表示封禁其它表示未知
// - Status(string): 外部接口返回的原始状态描述
// - CheckStatusText(string): 根据Code映射得到的中文状态文字正常/封禁/未知)
type checkAppStatusResponse struct {
AppID string `json:"app_id"` // 小程序ID
Code int `json:"code"` // 外部接口返回的状态码
CheckStatusText string `json:"check_status_text"` // 状态文字描述
}
// CheckAppStatus 检查小程序状态
// @Summary 检查小程序状态
// @Description 管理端根据 app_id 调用外部验证服务获取状态(正常/封禁/未知)
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param app_id query string true "小程序ID"
// @Success 200 {object} checkAppStatusResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/check_status [get]
// @Security LoginVerifyToken
func (h *handler) CheckAppStatus() core.HandlerFunc {
return func(ctx core.Context) {
// 绑定请求参数
req := new(checkAppStatusRequest)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
// 调用外部验证服务
// 外部接口: https://api.wxapi.work/xcx/checkxcx.php?appid=<AppID>
// 期望返回: {"code":1, "appid":"xxx", "status":"ok"}
type externalCheckResp struct {
Code int `json:"code"`
Appid string `json:"appid"`
Status string `json:"status"`
}
checkRes := new(externalCheckResp)
response, err := httpclient.GetHttpClientWithContext(ctx.RequestContext()).R().
SetQueryParams(map[string]string{
"appid": req.AppID,
}).
SetResult(checkRes).
Get("https://api.wxapi.work/xcx/checkxcx.php")
if err != nil {
// 记录请求错误
h.logger.Error("请求APP验证服务失败", zap.Error(err))
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppError,
fmt.Sprintf("%s%s", code.Text(code.ListAppError), err.Error()),
))
return
}
// 检查响应状态码
if response.IsError() {
h.logger.Error(fmt.Sprintf("请求APP验证服务异常(%d)", response.StatusCode()))
}
// 将外部返回的code映射为中文状态
statusText := "未知"
switch checkRes.Code {
case 1:
statusText = "正常"
case 0:
statusText = "封禁"
default:
statusText = "未知"
}
// 组织响应数据并返回
res := &checkAppStatusResponse{
AppID: req.AppID,
Code: checkRes.Code,
CheckStatusText: statusText,
}
ctx.Payload(res)
}
}

View File

@ -1,104 +0,0 @@
package app
import (
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"gorm.io/gorm"
)
type createAppRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
AppSecret string `json:"app_secret" binding:"required"` // 小程序密钥
Name string `json:"name" binding:"required"` // 名称
Description string `json:"description"` // 描述
Avatar string `json:"avatar"` // 头像
TemplateID string `json:"template_id"` // 模版ID
}
type createAppResponse struct {
Message string `json:"message"` // 提示信息
}
// CreateApp 新增小程序
// @Summary 新增小程序
// @Description 新增小程序
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param RequestBody body createAppRequest true "请求参数"
// @Success 200 {object} createAppResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/create [post]
// @Security LoginVerifyToken
func (h *handler) CreateApp() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createAppRequest)
res := new(createAppResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), "禁止操作")),
)
return
}
info, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), err.Error())),
)
return
}
if info != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), "该小程序已存在")),
)
return
}
App := new(model.MiniProgram)
App.AppID = req.AppID
App.AppSecret = req.AppSecret
App.Name = req.Name
App.Description = req.Description
App.Avatar = req.Avatar
App.TemplateID = req.TemplateID
App.CreatedUser = ctx.SessionUserInfo().UserName
App.CreatedAt = time.Now()
if err := h.writeDB.MiniProgram.WithContext(ctx.RequestContext()).Create(App); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,108 +0,0 @@
package app
import (
"fmt"
"net/http"
"strconv"
"strings"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
)
type deleteAppRequest struct {
Ids string `json:"ids" binding:"required"` // 小程序编号(多个用,分割)
}
type deleteAppResponse struct {
Message string `json:"message"` // 提示信息
}
// DeleteApp 删除小程序
// @Summary 删除小程序
// @Description 删除小程序
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param RequestBody body deleteAppRequest true "请求参数"
// @Success 200 {object} deleteAppResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/delete [post]
// @Security LoginVerifyToken
func (h *handler) DeleteApp() core.HandlerFunc {
return func(ctx core.Context) {
req := new(deleteAppRequest)
res := new(deleteAppResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), "禁止操作")),
)
return
}
idList := strings.Split(req.Ids, ",")
if len(idList) == 0 || (len(idList) == 1 && idList[0] == "") {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"小程序编号不能为空"),
)
return
}
var ids []int32
for _, strID := range idList {
if strID == "" {
continue
}
id, err := strconv.Atoi(strID)
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
fmt.Sprintf("无效的小程序编号: %s", strID)),
)
return
}
ids = append(ids, int32(id))
}
if len(ids) == 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"小程序编号不能为空"),
)
return
}
if _, err := h.writeDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.writeDB.MiniProgram.ID.In(ids...)).
Delete(&model.MiniProgram{}); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteAppError,
fmt.Sprintf("%s: %s", code.Text(code.DeleteAppError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,163 +0,0 @@
package app
import (
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
)
type latestMessageByAppIdRequest struct {
AppID string `form:"app_id" binding:"required"` // 小程序ID
Page int `form:"page"` // 当前页码默认1
PageSize int `form:"page_size"` // 每页返回的数据量默认20
}
type latestMessageData struct {
SendTime string `json:"send_time"` // 发送时间
SenderID string `json:"sender_id"` // 发送人ID
SenderName string `json:"sender_name"` // 发送人昵称
SenderAvatar string `json:"sender_avatar"` // 发送人头像
UnreadCount int `json:"unread_count"` // 未读消息数量
Content string `json:"content"` // 消息内容
MsgType int32 `json:"msg_type"` // 消息类型(1:文本 2:图片)
}
type latestMessageByAppIdResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []latestMessageData `json:"list"`
}
// LatestMessageByAppId 根据appid获取最新消息记录
// @Summary 根据appid获取最新消息记录
// @Description 管理端根据appid获取最新消息记录包含已读未读状态访问时自动标记为已读
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param app_id query string true "小程序ID"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} latestMessageByAppIdResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/messages/latest [get]
// @Security LoginVerifyToken
func (h *handler) LatestMessageByAppId() core.HandlerFunc {
return func(ctx core.Context) {
req := new(latestMessageByAppIdRequest)
res := new(latestMessageByAppIdResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListMessageError)),
))
return
}
type unreadMessageResult struct {
SenderID string `json:"sender_id"`
SenderName string `json:"sender_name"`
SendTime time.Time `json:"send_time"`
AvatarURL string `json:"avatar_url"`
UnreadCount int `json:"unread_count"`
Content string `json:"content"` // 消息内容
MsgType int32 `json:"msg_type"` // 消息类型(1:文本 2:图片)
}
var results []unreadMessageResult
var total int64
countErr := h.db.GetDbR().Table("app_message_log m").
Select("m.send_time, m.sender_id, m.sender_name, u.user_avatar as avatar_url").
Joins("LEFT JOIN app_user u ON m.sender_id = u.user_id").
Where("m.app_id = ? AND m.sender_id != ?", req.AppID, "888888").
Group("m.sender_id").
Count(&total).
Error
if countErr != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), countErr.Error())),
)
return
}
subQuery := h.db.GetDbR().Table("app_message_log").
Select("sender_id, MAX(send_time) as latest_time").
Where("app_id = ? AND sender_id != ?", req.AppID, "888888").
Group("sender_id")
resultErr := h.db.GetDbR().Table("app_message_log m").
Select("m.send_time, m.content, m.msg_type, m.sender_id, m.sender_name, u.user_avatar as avatar_url, (SELECT COUNT(*) FROM app_message_log WHERE sender_id = m.sender_id AND is_read = 0) as unread_count").
Joins("LEFT JOIN app_user u ON m.sender_id = u.user_id").
Joins("JOIN (?) as latest ON m.sender_id = latest.sender_id AND m.send_time = latest.latest_time", subQuery).
Where("m.app_id = ? AND m.sender_id != ?", req.AppID, "888888").
Group("m.sender_id").
Order("unread_count DESC, send_time DESC").
Offset((req.Page - 1) * req.PageSize).
Limit(req.PageSize).
Find(&results).
Error
if resultErr != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), resultErr.Error())),
)
return
}
// 自动标记该appid下的所有消息为已读管理端访问时
//_, err := h.writeDB.AppMessageLog.WithContext(ctx.RequestContext()).
// Where(h.writeDB.AppMessageLog.AppID.Eq(req.AppID)).
// Where(h.writeDB.AppMessageLog.IsRead.Eq(0)).
// Update(h.writeDB.AppMessageLog.IsRead, 1)
//if err != nil {
// // 记录错误但不影响查询结果
// // TODO: 可以添加日志记录
//}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]latestMessageData, len(results))
for k, v := range results {
res.List[k] = latestMessageData{
SendTime: timeutil.FriendlyTime(v.SendTime),
SenderID: v.SenderID,
SenderName: v.SenderName,
SenderAvatar: v.AvatarURL,
UnreadCount: v.UnreadCount,
Content: v.Content,
MsgType: v.MsgType,
}
}
ctx.Payload(res)
}
}

View File

@ -1,168 +0,0 @@
package app
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type listRequest struct {
AppID string `form:"app_id"` // 小程序ID
AdminID int32 `form:"admin_id"` // 客服编号
Name string `form:"name"` // 小程序名称
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type listData struct {
ID int32 `json:"id"` // 小程序编号
AppID string `json:"app_id"` // 小程序ID
AppSecret string `json:"app_secret"` // 小程序密钥
Name string `json:"name"` // 小程序名称
Description string `json:"description"` // 小程序描述
Avatar string `json:"avatar"` // 小程序头像
TemplateID string `json:"template_id"` // 模版ID
CreatedAt string `json:"created_at"` // 创建时间
UpdatedAt string `json:"updated_at"` // 更新时间
MessageTotal int64 `json:"message_total"` // 消息总数
CheckStatusText string `json:"check_status_text"` // 状态文字描述
}
type listResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []listData `json:"list"`
}
// PageList 小程序列表
// @Summary 小程序列表
// @Description 小程序列表
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param name query string false "小程序名称"
// @Param app_id query string false "小程序ID"
// @Param admin_id query int false "客服编号"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} listResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/apps [get]
// @Security LoginVerifyToken
func (h *handler) PageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listRequest)
res := new(listResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListAppError)),
))
return
}
query := h.readDB.MiniProgram.WithContext(ctx.RequestContext())
if ctx.SessionUserInfo().IsSuper != 1 {
query = query.Where(h.readDB.MiniProgram.AdminID.Eq(ctx.SessionUserInfo().Id))
} else {
if req.AdminID != 0 {
query = query.Where(h.readDB.MiniProgram.AdminID.Eq(req.AdminID))
}
}
if req.AppID != "" {
query = query.Where(h.readDB.MiniProgram.AppID.Eq(req.AppID))
}
if req.Name != "" {
query = query.Where(h.readDB.MiniProgram.Name.Like(fmt.Sprintf("%%%s%%", req.Name)))
}
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.MiniProgram.ID.Desc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppError,
fmt.Sprintf("%s%s", code.Text(code.ListAppError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppError,
fmt.Sprintf("%s%s", code.Text(code.ListAppError), err.Error())),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]listData, len(resultData))
for k, v := range resultData {
userTotalCount, _ := h.readDB.AppUser.WithContext(ctx.RequestContext()).
Where(h.readDB.AppUser.AppID.Eq(v.AppID)).
Count()
checkStatusText := "未知"
if v.Status == 1 {
checkStatusText = "正常"
} else if v.Status == -1 {
checkStatusText = "封禁"
}
res.List[k] = listData{
ID: v.ID,
AppID: v.AppID,
AppSecret: v.AppSecret,
Name: v.Name,
Description: v.Description,
Avatar: v.Avatar,
TemplateID: v.TemplateID,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
UpdatedAt: timeutil.FriendlyTime(v.UpdatedAt),
MessageTotal: userTotalCount,
CheckStatusText: checkStatusText,
}
}
ctx.Payload(res)
}
}

View File

@ -1,143 +0,0 @@
package app
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type appMessagePageListRequest struct {
AppID string `form:"app_id" binding:"required"` // 小程序ID
UserID string `form:"user_id" binding:"required"` // 用户ID
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type listMessageData struct {
ID string `json:"id"` // 消息ID
SendTime string `json:"send_time"` // 发送时间
SenderID string `json:"sender_id"` // 发送人ID
SenderName string `json:"sender_name"` // 发送人昵称
ReceiverID string `json:"receiver_id"` // 接收人ID
Content string `json:"content"` // 消息内容
MsgType int32 `json:"msg_type"` // 消息类型(1:文本 2:图片)
}
type appMessagePageListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []listMessageData `json:"list"`
}
// AppMessagePageList 获取消息日志
// @Summary 获取消息日志
// @Description 获取消息日志
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param app_id query string true "小程序ID"
// @Param user_id query string true "用户ID"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} appMessagePageListResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/messages [get]
// @Security LoginVerifyToken
func (h *handler) AppMessagePageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(appMessagePageListRequest)
res := new(appMessagePageListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListMessageError)),
))
return
}
query := h.readDB.AppMessageLog.WithContext(ctx.RequestContext()).
Where(h.readDB.AppMessageLog.AppID.Eq(req.AppID)).
Where(h.readDB.AppMessageLog.SenderID.Eq(req.UserID)).Or(h.readDB.AppMessageLog.ReceiverID.Eq(req.UserID))
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.AppMessageLog.SendTime.Desc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).
Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), err.Error())),
)
return
}
go func() {
_, err = h.writeDB.AppMessageLog.
Where(h.writeDB.AppMessageLog.AppID.Eq(req.AppID)).
Where(h.writeDB.AppMessageLog.SenderID.Eq(req.UserID)).Or(h.readDB.AppMessageLog.ReceiverID.Eq(req.UserID)).
Where(h.writeDB.AppMessageLog.IsRead.Eq(0)).
Update(h.writeDB.AppMessageLog.IsRead, 1)
if err != nil {
h.logger.Error(fmt.Sprintf("[AppMessagePageList] update app message log error: %s", err.Error()))
}
}()
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]listMessageData, len(resultData))
for k, v := range resultData {
res.List[k] = listMessageData{
ID: fmt.Sprint(v.ID),
SendTime: v.SendTime.Format("2006-01-02 15:04:05"),
SenderID: v.SenderID,
SenderName: v.SenderName,
ReceiverID: v.ReceiverID,
Content: v.Content,
MsgType: v.MsgType,
}
}
ctx.Payload(res)
}
}

View File

@ -1,69 +0,0 @@
package app
import (
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
)
type adminSendMessageRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
ToUserID string `json:"to_user_id" binding:"required"` // 接收用户ID
MsgType int32 `json:"msg_type" binding:"required"` // 消息类型(1:文本 2:图片)
Content string `json:"content" binding:"required"` // 内容
}
type adminSendMessageResponse struct {
Message string `json:"message"` // 提示信息
}
// AdminSendMessage 管理员发送消息
// @Summary 管理员发送消息
// @Description 管理员发送消息
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param RequestBody body adminSendMessageRequest true "请求参数"
// @Success 200 {object} adminSendMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/send_message [post]
// @Security LoginVerifyToken
func (h *handler) AdminSendMessage() core.HandlerFunc {
return func(ctx core.Context) {
req := new(adminSendMessageRequest)
res := new(adminSendMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
createData := new(model.AppMessageLog)
createData.AppID = req.AppID
createData.SenderID = "888888"
createData.SenderName = "平台"
createData.Content = req.Content
createData.ReceiverID = req.ToUserID
createData.MsgType = req.MsgType
createData.SendTime = time.Now()
createData.CreatedAt = time.Now()
if err := h.writeDB.AppMessageLog.WithContext(ctx.RequestContext()).Create(createData); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.SendMessageError,
fmt.Sprintf("%s: %s", code.Text(code.SendMessageError), err.Error()),
))
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,145 +0,0 @@
package app
import (
"fmt"
"net/http"
"strconv"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type modifyAppRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
AppSecret string `json:"app_secret" binding:"required"` // 小程序密钥
Name string `json:"name" binding:"required"` // 名称
Description string `json:"description"` // 描述
Avatar string `json:"avatar"` // 头像
TemplateID string `json:"template_id"` // 模版ID
}
type modifyAppResponse struct {
Message string `json:"message"` // 提示信息
}
// ModifyApp 编辑小程序
// @Summary 编辑小程序
// @Description 编辑小程序
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param id path string true "编号ID"
// @Param RequestBody body modifyAppRequest true "请求参数"
// @Success 200 {object} modifyAppResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/{id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyApp() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyAppRequest)
res := new(modifyAppResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppError), "禁止操作")),
)
return
}
if req.AppID == "" && req.Name == "" {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"小程序ID和标题不能为空"),
)
return
}
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID"),
)
return
}
checkIdInfo, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.readDB.MiniProgram.ID.Eq(int32(id))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAppError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAppError), err.Error())),
)
return
}
if checkIdInfo == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAppError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAppError), "该小程序不存在")),
)
return
}
checkAppIDInfo, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
Where(h.readDB.MiniProgram.ID.Neq(int32(id))).
Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAppError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAppError), err.Error())),
)
return
}
if checkAppIDInfo != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAppError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyAppError), "该小程序ID已存在")),
)
return
}
checkIdInfo.AppID = req.AppID
checkIdInfo.AppSecret = req.AppSecret
checkIdInfo.Name = req.Name
checkIdInfo.Description = req.Description
checkIdInfo.Avatar = req.Avatar
checkIdInfo.TemplateID = req.TemplateID
checkIdInfo.UpdatedUser = ctx.SessionUserInfo().UserName
checkIdInfo.UpdatedAt = time.Now()
if err := h.writeDB.MiniProgram.WithContext(ctx.RequestContext()).Save(checkIdInfo); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyAppError,
fmt.Sprintf("%s%s", code.Text(code.ModifyAppError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,85 +0,0 @@
package app
import (
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"gorm.io/gorm"
)
type createAppUserRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
UserID string `json:"user_id" binding:"required"` // 用户ID
UserName string `json:"user_name" binding:"required"` // 用户昵称
UserMobile string `json:"user_mobile"` // 用户手机号
UserAvatar string `json:"user_avatar"` // 用户头像
}
type createAppUserResponse struct {
Message string `json:"message"` // 提示信息
}
// CreateAppUser 新增小程序用户
// @Summary 新增小程序用户
// @Description 新增小程序用户
// @Tags 用户端
// @Accept json
// @Produce json
// @Param RequestBody body createAppUserRequest true "请求参数"
// @Success 200 {object} createAppUserResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/user/create [post]
func (h *handler) CreateAppUser() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createAppUserRequest)
res := new(createAppUserResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
info, err := h.readDB.AppUser.WithContext(ctx.RequestContext()).
Where(h.readDB.AppUser.AppID.Eq(req.AppID)).
Where(h.readDB.AppUser.UserID.Eq(req.UserID)).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppUserError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppUserError), err.Error())),
)
return
}
if info == nil {
AppUser := new(model.AppUser)
AppUser.AppID = req.AppID
AppUser.UserID = req.UserID
AppUser.UserName = req.UserName
AppUser.UserMobile = req.UserMobile
AppUser.UserAvatar = req.UserAvatar
AppUser.CreatedAt = time.Now()
if err := h.writeDB.AppUser.WithContext(ctx.RequestContext()).Create(AppUser); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAppUserError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAppUserError), err.Error())),
)
return
}
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,137 +0,0 @@
package app
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type userListRequest struct {
AppID string `form:"app_id"` // 小程序ID
UserID string `form:"user_id"` // 用户ID
UserName string `form:"user_name"` // 用户昵称
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type userListData struct {
UserID string `json:"user_id"` // 用户ID
UserName string `json:"user_name"` // 用户昵称
UserMobile string `json:"user_mobile"` // 用户手机号
UserAvatar string `json:"user_avatar"` // 用户头像
CreatedAt string `json:"created_at"` // 创建时间
}
type userListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []userListData `json:"list"`
}
// UserPageList 小程序用户列表
// @Summary 小程序用户列表
// @Description 小程序用户列表
// @Tags 管理端.小程序
// @Accept json
// @Produce json
// @Param app_id query string true "小程序ID"
// @Param user_name query string false "用户昵称"
// @Param user_id query string false "用户ID"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} userListResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/users [get]
// @Security LoginVerifyToken
func (h *handler) UserPageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(userListRequest)
res := new(userListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppUserError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListAppUserError)),
))
return
}
query := h.readDB.AppUser.WithContext(ctx.RequestContext()).Where(h.readDB.AppUser.AppID.Eq(req.AppID))
if req.UserID != "" {
query = query.Where(h.readDB.AppUser.UserID.Eq(req.UserID))
}
if req.UserName != "" {
query = query.Where(h.readDB.AppUser.UserName.Like(fmt.Sprintf("%%%s%%", req.UserName)))
}
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.AppUser.ID.Desc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppUserError,
fmt.Sprintf("%s%s", code.Text(code.ListAppUserError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListAppUserError,
fmt.Sprintf("%s%s", code.Text(code.ListAppUserError), err.Error())),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]userListData, len(resultData))
for k, v := range resultData {
res.List[k] = userListData{
UserID: v.UserID,
UserName: v.UserName,
UserMobile: v.UserMobile,
UserAvatar: v.UserAvatar,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,59 @@
package app
import (
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
bannersvc "bindbox-game/internal/service/banner"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
)
type bannerHandler struct {
logger logger.CustomLogger
readDB *dao.Query
banner bannersvc.Service
}
func NewBanner(logger logger.CustomLogger, db mysql.Repo) *bannerHandler {
return &bannerHandler{logger: logger, readDB: dao.Use(db.GetDbR()), banner: bannersvc.New(logger, db)}
}
type listAppBannersResponse struct {
List []appBannerItem `json:"list"`
}
type appBannerItem struct {
ID int64 `json:"id"`
Title string `json:"title"`
ImageURL string `json:"image_url"`
LinkURL string `json:"link_url"`
Sort int32 `json:"sort"`
}
// ListBannersForApp APP端轮播图列表
// @Summary APP端轮播图列表
// @Tags APP端.运营
// @Accept json
// @Produce json
// @Success 200 {object} listAppBannersResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/banners [get]
func (h *bannerHandler) ListBannersForApp() core.HandlerFunc {
return func(ctx core.Context) {
res := new(listAppBannersResponse)
items, err := h.banner.ListEnabled(ctx.RequestContext())
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err)))
return
}
res.List = make([]appBannerItem, len(items))
for i, it := range items {
res.List[i] = appBannerItem{ID: it.ID, Title: it.Title, ImageURL: it.ImageURL, LinkURL: it.LinkURL, Sort: it.Sort}
}
ctx.Payload(res)
}
}

View File

@ -0,0 +1,24 @@
package common
import (
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
commonsvc "bindbox-game/internal/service/common"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
svc commonsvc.Service
}
func New(l logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: l,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
svc: commonsvc.New(),
}
}

View File

@ -0,0 +1,44 @@
package common
import (
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
func (h *handler) UploadWangEditorImage() core.HandlerFunc {
return func(ctx core.Context) {
fh, err := ctx.FormFile("file")
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "缺少文件"))
return
}
f, err := fh.Open()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.UploadError, err.Error()))
return
}
defer f.Close()
ct := fh.Header.Get("Content-Type")
if ct == "" {
ct = "application/octet-stream"
}
url, err := h.svc.UploadImage(ctx.RequestContext(), fh.Filename, f, ct)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.UploadError, err.Error()))
return
}
resp := map[string]any{
"errno": 0,
"data": map[string]string{
"url": url,
},
}
ctx.Payload(resp)
}
}

19
internal/api/guild/app.go Normal file
View File

@ -0,0 +1,19 @@
package app
import (
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
guildsvc "bindbox-game/internal/service/guild"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
guild guildsvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{logger: logger, writeDB: dao.Use(db.GetDbW()), readDB: dao.Use(db.GetDbR()), guild: guildsvc.New(logger, db)}
}

View File

@ -0,0 +1,120 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listGuildsRequest struct {
Name string `form:"name"`
IsOpen int32 `form:"is_open"`
Status int32 `form:"status"`
JoinMode int32 `form:"join_mode"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type guildItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
OwnerID int64 `json:"owner_id"`
Description string `json:"description"`
JoinMode int32 `json:"join_mode"`
ConsumeLimit int64 `json:"consume_limit"`
AvatarURL string `json:"avatar_url"`
IsOpen int32 `json:"is_open"`
Status int32 `json:"status"`
}
type listGuildsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []guildItem `json:"list"`
}
// ListGuilds 工会列表
// @Summary 浏览工会列表
// @Description 获取工会列表,支持公开与状态过滤以及分页
// @Tags APP端.工会
// @Accept json
// @Produce json
// @Param name query string false "工会名称(模糊)"
// @Param is_open query int false "是否公开(1公开 2私有)"
// @Param status query int false "状态(1正常 2解散)"
// @Param join_mode query int false "加入方式(1审核 2自动 3消费流水)"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listGuildsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/guilds [get]
func (h *handler) ListGuilds() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listGuildsRequest)
res := new(listGuildsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var isOpenPtr, statusPtr, joinModePtr *int32
if req.IsOpen == 1 || req.IsOpen == 2 {
isOpenPtr = &req.IsOpen
}
if req.Status == 1 || req.Status == 2 {
statusPtr = &req.Status
}
if req.JoinMode == 1 || req.JoinMode == 2 || req.JoinMode == 3 {
joinModePtr = &req.JoinMode
}
items, total, err := h.guild.ListGuilds(ctx.RequestContext(), struct {
Name string
IsOpen *int32
Status *int32
JoinMode *int32
Page int
PageSize int
}{Name: req.Name, IsOpen: isOpenPtr, Status: statusPtr, JoinMode: joinModePtr, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListGuildsError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]guildItem, len(items))
for i, v := range items {
res.List[i] = guildItem{ID: v.ID, Name: v.Name, OwnerID: v.OwnerID, Description: v.Description, JoinMode: v.JoinMode, ConsumeLimit: v.ConsumeLimit, AvatarURL: v.AvatarURL, IsOpen: v.IsOpen, Status: v.Status}
}
ctx.Payload(res)
}
}
// GetGuildDetail 工会详情
// @Summary 查看工会详情
// @Description 查看指定工会详情
// @Tags APP端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Success 200 {object} model.Guild
// @Failure 400 {object} code.Failure
// @Router /api/app/guilds/{guild_id} [get]
func (h *handler) GetGuildDetail() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
item, err := h.guild.GetGuild(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetGuildError, err.Error()))
return
}
ctx.Payload(item)
}
}

View File

@ -0,0 +1,141 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type joinGuildRequest struct {
UserID int64 `json:"user_id" binding:"required"`
}
type simpleMessageResponse struct {
Message string `json:"message"`
}
// JoinGuild 加入工会
// @Summary 加入工会
// @Description 用户加入指定工会
// @Tags APP端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Param RequestBody body joinGuildRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/guilds/{guild_id}/members [post]
func (h *handler) JoinGuild() core.HandlerFunc {
return func(ctx core.Context) {
req := new(joinGuildRequest)
res := new(simpleMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
guildID, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
if err := h.guild.JoinGuild(ctx.RequestContext(), guildID, struct{ UserID int64 }{UserID: req.UserID}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.JoinGuildError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// LeaveGuild 离开工会
// @Summary 离开工会
// @Description 用户离开指定工会
// @Tags APP端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Param user_id path integer true "用户ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/guilds/{guild_id}/members/{user_id} [delete]
func (h *handler) LeaveGuild() core.HandlerFunc {
return func(ctx core.Context) {
res := new(simpleMessageResponse)
guildID, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
if err := h.guild.LeaveGuild(ctx.RequestContext(), guildID, userID); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.LeaveGuildError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
type listMembersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type memberItem struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Role string `json:"role"`
StartTime string `json:"start_time"`
}
type listMembersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []memberItem `json:"list"`
}
// ListGuildMembers 工会成员列表
// @Summary 查看工会成员
// @Description 查看指定工会的成员列表
// @Tags APP端.工会
// @Accept json
// @Produce json
// @Param guild_id path integer true "工会ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listMembersResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/guilds/{guild_id}/members [get]
func (h *handler) ListGuildMembers() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listMembersRequest)
res := new(listMembersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
guildID, err := strconv.ParseInt(ctx.Param("guild_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递工会ID"))
return
}
items, total, err := h.guild.ListMembers(ctx.RequestContext(), guildID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListGuildMembersError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]memberItem, len(items))
for i, v := range items {
res.List[i] = memberItem{ID: v.ID, UserID: v.UserID, Role: v.Role, StartTime: v.StartTime.Format("2006-01-02 15:04:05")}
}
ctx.Payload(res)
}
}

View File

@ -1,21 +0,0 @@
package keyword
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
}
}

View File

@ -1,82 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strings"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
)
type createKeywordRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
Keyword string `json:"keyword" binding:"required"` // 关键字
}
type createKeywordResponse struct {
Message string `json:"message"` // 提示信息
ID int32 `json:"id"` // 关键字ID
}
// CreateKeyword 添加意图关键字
// @Summary 添加意图关键字
// @Description 添加意图关键字
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param RequestBody body createKeywordRequest true "请求参数"
// @Success 200 {object} createKeywordResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword [post]
// @Security LoginVerifyToken
func (h *handler) CreateKeyword() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createKeywordRequest)
res := new(createKeywordResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
req.Keyword = strings.TrimSpace(req.Keyword)
// 验证关键字是否已存在
if _, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeyword.AppID.Eq(req.AppID)).
Where(h.readDB.AppKeyword.Keyword.Eq(req.Keyword)).
First(); err == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordError,
fmt.Sprintf("%s: 该关键字(%s)已存在", code.Text(code.CreateKeywordError), req.Keyword)),
)
return
}
createData := new(model.AppKeyword)
createData.AppID = req.AppID
createData.Keyword = req.Keyword
createData.CreatedUser = ctx.SessionUserInfo().UserName
createData.CreatedAt = time.Now()
if err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()).Create(createData); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.CreateKeywordError), err.Error()),
))
}
res.Message = "操作成功"
res.ID = createData.ID
ctx.Payload(res)
}
}

View File

@ -1,86 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strconv"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"gorm.io/gorm"
)
type deleteKeywordResponse struct {
Message string `json:"message"` // 提示信息
}
// DeleteKeyword 删除意图关键字
// @Summary 删除意图关键字
// @Description 删除意图关键字
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param id path string true "编号ID"
// @Success 200 {object} deleteKeywordResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword/{id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteKeyword() core.HandlerFunc {
return func(ctx core.Context) {
ID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID。"),
)
return
}
info, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeyword.ID.Eq(int32(ID))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())),
)
return
}
if info == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteKeywordError,
fmt.Sprintf("%s: 编号(%d)不存在。", code.Text(code.DeleteKeywordError), ID)),
)
return
}
if _, err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.writeDB.AppKeyword.ID.Eq(int32(ID))).
Delete(); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())),
)
}
if _, err := h.writeDB.AppKeywordReply.WithContext(ctx.RequestContext()).
Where(h.writeDB.AppKeywordReply.KeywordID.Eq(int32(ID))).
Delete(); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.DeleteKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())),
)
}
res := new(deleteKeywordResponse)
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,212 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strings"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type keywordPageListRequest struct {
AppID string `form:"app_id" binding:"required"` // 小程序ID
Keyword string `form:"keyword"` // 意图关键字
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type keywordListData struct {
ID int32 `json:"id"` // 关键字编号
AppID string `json:"app_id"` // 小程序ID
Keyword string `json:"keyword"` // 意图关键字
CreatedUser string `json:"created_user"` // 创建人
CreatedAt string `json:"created_at"` // 创建时间
UpdatedUser string `json:"updated_user"` // 更新人
UpdatedAt string `json:"updated_at"` // 更新时间
MaterialTypeCount string `json:"material_type_count"` // 素材类型数量
}
type keywordPageListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []keywordListData `json:"list"`
}
// KeywordPageList 获取意图关键字列表
// @Summary 获取意图关键字列表
// @Description 获取意图关键字列表
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param app_id query int true "小程序ID"
// @Param keyword query string false "意图关键字"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} keywordPageListResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keywords [get]
// @Security LoginVerifyToken
func (h *handler) KeywordPageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(keywordPageListRequest)
res := new(keywordPageListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListKeywordError)),
))
return
}
query := h.readDB.AppKeyword.WithContext(ctx.RequestContext())
query = query.Where(h.readDB.AppKeyword.AppID.Eq(req.AppID))
if req.Keyword != "" {
query = query.Where(h.readDB.AppKeyword.Keyword.Like(fmt.Sprintf("%%%s%%", req.Keyword)))
}
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.AppKeyword.ID.Desc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).
Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordError,
fmt.Sprintf("%s%s", code.Text(code.ListKeywordError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordError,
fmt.Sprintf("%s%s", code.Text(code.ListKeywordError), err.Error())),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]keywordListData, len(resultData))
var keywordIDs []int32
for _, v := range resultData {
keywordIDs = append(keywordIDs, v.ID)
}
keywordIDMaterialTypeCountMap, err := h.contactMaterialTypeCount(keywordIDs)
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordError,
fmt.Sprintf("%s%s", code.Text(code.ListKeywordError), err.Error())),
)
return
}
for k, v := range resultData {
res.List[k] = keywordListData{
ID: v.ID,
AppID: v.AppID,
Keyword: v.Keyword,
CreatedUser: v.CreatedUser,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
UpdatedUser: v.UpdatedUser,
UpdatedAt: timeutil.FriendlyTime(v.UpdatedAt),
MaterialTypeCount: keywordIDMaterialTypeCountMap[v.ID],
}
}
ctx.Payload(res)
}
}
func (h *handler) contactMaterialTypeCount(keywordIDs []int32) (map[int32]string, error) {
var results []struct {
KeywordID int32
Type int
Count int `db:"count"`
}
if err := h.readDB.AppKeywordReply.
Select(h.readDB.AppKeywordReply.KeywordID, h.readDB.AppKeywordReply.Type, h.readDB.AppKeywordReply.ID.Count().As("count")).
Where(h.readDB.AppKeywordReply.KeywordID.In(keywordIDs...)).
Group(h.readDB.AppKeywordReply.KeywordID, h.readDB.AppKeywordReply.Type).
Scan(&results); err != nil {
return nil, err
}
keywordIDMap := make(map[int32]string)
typeMap := map[int]string{
1: "文本",
2: "图片",
3: "语音条",
4: "视频",
5: "小程序",
6: "地理位置",
7: "链接",
8: "GIF图",
9: "名片",
10: "文件",
11: "转人工",
}
for _, result := range results {
keywordID := result.KeywordID
typeDesc := fmt.Sprintf("%d条%s", result.Count, typeMap[result.Type])
if currentString, exists := keywordIDMap[keywordID]; exists {
keywordIDMap[keywordID] = currentString + "、" + typeDesc + "、"
} else {
keywordIDMap[keywordID] = "1 组话术(共" + typeDesc
}
}
for keywordID, resultString := range keywordIDMap {
// 查找最后一个逗号的位置
commaIndex := strings.LastIndex(resultString, "、")
// 如果找到逗号,替换为右括号
if commaIndex != -1 {
keywordIDMap[keywordID] = resultString[:commaIndex] + ""
} else {
keywordIDMap[keywordID] += ""
}
}
return keywordIDMap, nil
}

View File

@ -1,143 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strconv"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"gorm.io/gorm"
)
type keywordMaterial struct {
Type int32 `json:"type" binding:"required"` // 素材类型(1:文本 2:图片)
Content string `json:"content" binding:"required"` // 素材内容
IntervalSeconds int32 `json:"interval_seconds" binding:"required"` // 发送间隔时间(单位:秒)
}
type createKeywordMaterialRequest struct {
MaterialList []keywordMaterial `json:"material_list" binding:"required"` // 素材列表
}
type createKeywordMaterialResponse struct {
Message string `json:"message"` // 提示信息
}
// CreateKeywordMaterial 配置意图关键字素材
// @Summary 配置意图关键字素材
// @Description 配置意图关键字素材
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param id path string true "编号ID"
// @Param RequestBody body createKeywordMaterialRequest true "请求参数"
// @Success 200 {object} createKeywordMaterialResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword/material/{id} [post]
// @Security LoginVerifyToken
func (h *handler) CreateKeywordMaterial() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createKeywordMaterialRequest)
res := new(createKeywordMaterialResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
ID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID。"),
)
return
}
if len(req.MaterialList) == 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: 未配置素材", code.Text(code.CreateKeywordMaterialError))),
)
return
}
info, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeyword.ID.Eq(int32(ID))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: %s", code.Text(code.CreateKeywordMaterialError), err.Error())),
)
return
}
if info == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: 意图关键字编号(%d)不存在。", code.Text(code.CreateKeywordMaterialError), ID)),
)
return
}
// 先删除旧数据
if _, err := h.writeDB.AppKeywordReply.WithContext(ctx.RequestContext()).
Where(h.writeDB.AppKeywordReply.KeywordID.Eq(int32(ID))).
Delete(); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: %s", code.Text(code.CreateKeywordMaterialError), err.Error())),
)
return
}
var keywordMaterials []*model.AppKeywordReply
for i := 0; i < len(req.MaterialList); i++ {
if req.MaterialList[i].Type == 0 || req.MaterialList[i].Content == "" || req.MaterialList[i].IntervalSeconds == 0 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: 配置素材数据不完整,请检查。", code.Text(code.CreateKeywordMaterialError))),
)
return
}
keywordMaterials = append(keywordMaterials, &model.AppKeywordReply{
KeywordID: int32(ID),
AppID: info.AppID,
Type: req.MaterialList[i].Type,
Content: req.MaterialList[i].Content,
IntervalSeconds: req.MaterialList[i].IntervalSeconds,
CreatedUser: ctx.SessionUserInfo().UserName,
CreatedAt: time.Now(),
})
}
// 批量插入
if err := h.writeDB.AppKeywordReply.WithContext(ctx.RequestContext()).CreateInBatches(keywordMaterials, len(req.MaterialList)); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateKeywordMaterialError,
fmt.Sprintf("%s: %s", code.Text(code.CreateKeywordMaterialError), err.Error())),
)
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,130 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type keywordMaterialPageListRequest struct {
KeywordID int32 `form:"keyword_id" binding:"required"` // 意图关键字ID
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type keywordMaterialListData struct {
ID int32 `json:"id"` // 主键ID
KeywordID int32 `json:"keyword_id"` // 意图关键字ID
Type int32 `json:"type"` // 素材类型(1:文本 2:图片)
Content string `json:"content"` // 素材内容
IntervalSeconds int32 `json:"interval_seconds"` // 素材发送间隔时间(单位:秒)
CreatedUser string `json:"created_user"` // 创建人
CreatedAt string `json:"created_at"` // 创建时间
}
type keywordMaterialPageListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []keywordMaterialListData `json:"list"`
}
// KeywordMaterialPageList 获取意图关键字素材列表
// @Summary 获取意图关键字素材列表
// @Description 获取意图关键字素材列表
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param keyword_id query int true "意图关键字ID"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} keywordMaterialPageListResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword/materials [get]
// @Security LoginVerifyToken
func (h *handler) KeywordMaterialPageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(keywordMaterialPageListRequest)
res := new(keywordMaterialPageListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordMaterialError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListKeywordMaterialError)),
))
return
}
query := h.readDB.AppKeywordReply.WithContext(ctx.RequestContext())
query = query.Where(h.readDB.AppKeywordReply.KeywordID.Eq(req.KeywordID))
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.
Order(h.readDB.AppKeywordReply.ID.Asc()).
Limit(req.PageSize).
Offset((req.Page - 1) * req.PageSize).
Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordMaterialError,
fmt.Sprintf("%s%s", code.Text(code.ListKeywordMaterialError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListKeywordMaterialError,
fmt.Sprintf("%s%s", code.Text(code.ListKeywordMaterialError), err.Error())),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]keywordMaterialListData, len(resultData))
for k, v := range resultData {
res.List[k] = keywordMaterialListData{
ID: v.ID,
KeywordID: v.KeywordID,
Type: v.Type,
Content: v.Content,
IntervalSeconds: v.IntervalSeconds,
CreatedUser: v.CreatedUser,
CreatedAt: timeutil.FriendlyTime(v.CreatedAt),
}
}
ctx.Payload(res)
}
}

View File

@ -1,103 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strconv"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type modifyKeywordMaterialRequest struct {
Type int32 `json:"type" binding:"required"` // 素材类型(1:文本 2:图片)
Content string `json:"content" binding:"required"` // 素材内容
IntervalSeconds int32 `json:"interval_seconds" binding:"required"` // 发送间隔时间(单位:秒)
}
type modifyKeywordMaterialResponse struct {
Message string `json:"message"` // 提示信息
}
// ModifyKeywordMaterial 修改意图关键字素材
// @Summary 修改意图关键字素材
// @Description 修改意图关键字素材
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param id path string true "素材编号ID"
// @Param RequestBody body modifyKeywordMaterialRequest true "请求参数"
// @Success 200 {object} modifyKeywordMaterialResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword/material/{id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyKeywordMaterial() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyKeywordMaterialRequest)
res := new(modifyKeywordMaterialResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
ID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID。"),
)
return
}
info, err := h.readDB.AppKeywordReply.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeywordReply.ID.Eq(int32(ID))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordMaterialError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordMaterialError), err.Error())),
)
return
}
if info == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordMaterialError,
fmt.Sprintf("%s: 意图关键字素材编号(%d)不存在。", code.Text(code.ModifyKeywordMaterialError), ID)),
)
return
}
updateData := map[string]interface{}{
"type": req.Type,
"content": req.Content,
"interval_seconds": req.IntervalSeconds,
"updated_user": ctx.SessionUserInfo().UserName,
"updated_at": time.Now(),
}
if _, err := h.writeDB.AppKeywordReply.WithContext(ctx.RequestContext()).
Where(h.writeDB.AppKeywordReply.ID.Eq(int32(ID))).
Updates(updateData); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordMaterialError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordMaterialError), err.Error())),
)
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,117 +0,0 @@
package keyword
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type modifyKeywordRequest struct {
Keyword string `json:"keyword" binding:"required"` // 意图关键字
}
type modifyKeywordResponse struct {
Message string `json:"message"` // 提示信息
}
// ModifyKeyword 修改意图关键字
// @Summary 修改意图关键字
// @Description 修改意图关键字
// @Tags 管理端.意图关键字
// @Accept json
// @Produce json
// @Param id path string true "编号ID"
// @Param RequestBody body modifyKeywordRequest true "请求参数"
// @Success 200 {object} modifyKeywordResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/app/keyword/{id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyKeyword() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyKeywordRequest)
res := new(modifyKeywordResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
ID, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID。"),
)
return
}
info, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeyword.ID.Eq(int32(ID))).
First()
if err != nil && err != gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordError), err.Error())),
)
return
}
if info == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordError,
fmt.Sprintf("%s: 编号(%d)不存在。", code.Text(code.ModifyKeywordError), ID)),
)
return
}
req.Keyword = strings.TrimSpace(req.Keyword)
if info.Keyword != req.Keyword {
// 验证关键字是否已存在
if _, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.readDB.AppKeyword.AppID.Eq(info.AppID)).
Where(h.readDB.AppKeyword.Keyword.Eq(req.Keyword)).
First(); err == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordError,
fmt.Sprintf("%s: 该关键字(%s)已存在", code.Text(code.ModifyKeywordError), req.Keyword)),
)
return
}
}
updateData := map[string]interface{}{
"keyword": req.Keyword,
"updated_user": ctx.SessionUserInfo().UserName,
"updated_at": time.Now(),
}
if _, err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()).
Where(h.writeDB.AppKeyword.ID.Eq(int32(ID))).
Updates(updateData); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ModifyKeywordError,
fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordError), err.Error())),
)
}
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,21 +0,0 @@
package message
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
}
}

View File

@ -1,135 +0,0 @@
package message
import (
"fmt"
"net/http"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/timeutil"
"mini-chat/internal/pkg/validation"
"gorm.io/gorm"
)
type appMessagePageListRequest struct {
AppID string `form:"app_id" binding:"required"` // 小程序ID
UserID string `form:"user_id" binding:"required"` // 用户ID
Page int `form:"page"` // 当前页码,默认为第一页
PageSize int `form:"page_size"` // 每页返回的数据量
}
type listMessageData struct {
ID int32 `json:"id"` // 消息ID
SendTime string `json:"send_time"` // 发送时间
SenderID string `json:"sender_id"` // 发送人ID
SenderName string `json:"sender_name"` // 发送人昵称
ReceiverID string `json:"receiver_id"` // 接收人ID
Content string `json:"content"` // 消息内容
MsgType int32 `json:"msg_type"` // 消息类型(1:文本 2:图片)
}
type appMessagePageListResponse struct {
Page int `json:"page"` // 当前页码
PageSize int `json:"page_size"` // 每页返回的数据量
Total int64 `json:"total"` // 符合查询条件的总记录数
List []listMessageData `json:"list"`
}
// AppMessagePageList 获取消息日志
// @Summary 获取消息日志
// @Description 获取消息日志
// @Tags 用户端
// @Accept json
// @Produce json
// @Param app_id query string true "小程序ID"
// @Param user_id query string true "用户ID"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Success 200 {object} appMessagePageListResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/messages [get]
func (h *handler) AppMessagePageList() core.HandlerFunc {
return func(ctx core.Context) {
req := new(appMessagePageListRequest)
res := new(appMessagePageListResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 20
}
if req.PageSize > 100 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListMessageError)),
))
return
}
query := h.readDB.AppMessageLog.WithContext(ctx.RequestContext()).
Where(h.readDB.AppMessageLog.AppID.Eq(req.AppID)).
Where(h.readDB.AppMessageLog.SenderID.Eq(req.UserID)).Or(h.readDB.AppMessageLog.ReceiverID.Eq(req.UserID))
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
// 计算分页偏移量
offset := (req.Page - 1) * req.PageSize
resultData, err := listQueryDB.
Order(h.readDB.AppMessageLog.SendTime.Desc()).
Offset(offset).
Limit(req.PageSize).
Find()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), err.Error())),
)
return
}
count, err := countQueryDB.Count()
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ListMessageError,
fmt.Sprintf("%s%s", code.Text(code.ListMessageError), err.Error())),
)
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = count
res.List = make([]listMessageData, len(resultData))
for k, v := range resultData {
res.List[k] = listMessageData{
ID: v.ID,
SendTime: timeutil.FriendlyTime(v.SendTime),
SenderID: v.SenderID,
SenderName: v.SenderName,
ReceiverID: v.ReceiverID,
Content: v.Content,
MsgType: v.MsgType,
}
}
ctx.Payload(res)
}
}

View File

@ -1,128 +0,0 @@
package message
import (
"encoding/json"
"fmt"
"net/http"
"time"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/validation"
"mini-chat/internal/repository/mysql/model"
"mini-chat/internal/services"
"gorm.io/gorm"
)
type userSendMessageRequest struct {
AppID string `json:"app_id" binding:"required"` // 小程序ID
FormUserID string `json:"from_user_id" binding:"required"` // 发送用户的ID
FormUserName string `json:"from_user_name" binding:"required"` // 发送用户的昵称
MsgType int32 `json:"msg_type" binding:"required"` // 消息类型(1:文本 2:图片)
Content string `json:"content" binding:"required"` // 内容
}
type userSendMessageResponse struct {
Message string `json:"message"` // 提示信息
}
// UserSendMessage 用户发送消息
// @Summary 用户发送消息
// @Description 用户发送消息
// @Tags 用户端
// @Accept json
// @Produce json
// @Param RequestBody body userSendMessageRequest true "请求参数"
// @Success 200 {object} userSendMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/send_message [post]
func (h *handler) UserSendMessage() core.HandlerFunc {
return func(ctx core.Context) {
req := new(userSendMessageRequest)
res := new(userSendMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
createData := new(model.AppMessageLog)
createData.AppID = req.AppID
createData.SenderID = req.FormUserID
createData.SenderName = req.FormUserName
createData.Content = req.Content
createData.ReceiverID = "888888"
createData.MsgType = req.MsgType
createData.SendTime = time.Now()
createData.CreatedAt = time.Now()
if err := h.writeDB.AppMessageLog.WithContext(ctx.RequestContext()).Create(createData); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.SendMessageError,
fmt.Sprintf("%s: %s", code.Text(code.SendMessageError), err.Error()),
))
}
time.AfterFunc(1*time.Second, func() {
if req.MsgType == 1 { // 自动回复逻辑
textMsg := new(services.TextMessage)
if err := json.Unmarshal([]byte(req.Content), &textMsg); err != nil {
h.logger.Error(fmt.Sprintf("AppID(%s),用户ID(%s),发送内容(%s) JSON解析失败: %s", req.AppID, req.FormUserID, req.Content, err.Error()))
return
}
keyword, err := h.readDB.AppKeyword.
Where(h.readDB.AppKeyword.AppID.Eq(req.AppID)).
Where(h.readDB.AppKeyword.Keyword.Eq(textMsg.Message)).
Order(h.readDB.AppKeyword.ID.Asc()).
First()
if err != nil && err != gorm.ErrRecordNotFound {
h.logger.Error(fmt.Sprintf("AppID(%s),用户ID(%s),发送内容(%s) 获取意图关键字失败: %s", req.AppID, req.FormUserID, req.Content, err.Error()))
return
}
if keyword == nil {
return
}
// 获取群组关键字回复信息
reply, err := h.readDB.AppKeywordReply.
Where(h.readDB.AppKeywordReply.AppID.Eq(req.AppID)).
Where(h.readDB.AppKeywordReply.KeywordID.Eq(keyword.ID)).
Find()
if err != nil && err != gorm.ErrRecordNotFound {
h.logger.Error(fmt.Sprintf("AppID(%s),用户ID(%s),发送内容(%s) 获取群组关键字回复失败: %s", req.AppID, req.FormUserID, req.Content, err.Error()))
return
}
if len(reply) == 0 {
return
}
for _, v := range reply {
time.Sleep(time.Duration(v.IntervalSeconds) * time.Second)
replyData := new(model.AppMessageLog)
replyData.AppID = req.AppID
replyData.SenderID = "888888"
replyData.SenderName = "平台"
replyData.Content = v.Content
replyData.ReceiverID = req.FormUserID
replyData.MsgType = v.Type
replyData.SendTime = time.Now()
replyData.CreatedAt = time.Now()
if err := h.writeDB.AppMessageLog.Create(replyData); err != nil {
h.logger.Error(fmt.Sprintf("AppID(%s),用户ID(%s),发送内容(%s) 回复关键字回复失败: %s", req.AppID, req.FormUserID, req.Content, err.Error()))
}
}
}
})
res.Message = "操作成功"
ctx.Payload(res)
}
}

View File

@ -1,21 +0,0 @@
package upload
import (
"mini-chat/internal/pkg/logger"
"mini-chat/internal/repository/mysql"
"mini-chat/internal/repository/mysql/dao"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
}
}

View File

@ -1,90 +0,0 @@
package upload
import (
"fmt"
"net/http"
"strings"
"mini-chat/configs"
"mini-chat/internal/code"
"mini-chat/internal/pkg/core"
"mini-chat/internal/pkg/idgen"
)
type uploadImageResponse struct {
RealImageUrl string `json:"real_image_url"` // 真实图片地址
PreviewImageUrl string `json:"preview_image_url"` // 可预览图片地址
}
// UploadImage 上传图片
// @Summary 上传图片
// @Description 上传图片
// @Tags 通用
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "选择文件"
// @Success 200 {object} uploadImageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/upload/image [post]
func (h *handler) UploadImage() core.HandlerFunc {
return func(ctx core.Context) {
file, err := ctx.FormFile("file")
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), err.Error()),
))
return
}
if file == nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), "缺少 file 文件"),
))
return
}
// 校验文件后缀
extension := ""
if dot := strings.LastIndexByte(file.Filename, '.'); dot != -1 {
extension = file.Filename[dot+1:]
}
allowedExtensions := map[string]bool{
"jpg": true,
"jpeg": true,
"png": true,
"gif": true,
}
if !allowedExtensions[strings.ToLower(extension)] {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), "文件后缀应为 .jpg、.jpeg、.png、.gif"),
))
return
}
// 保存文件
imagePath := fmt.Sprintf("image/%s.%s", idgen.GenerateUniqueID(), strings.ToLower(extension))
filePath := fmt.Sprintf("%s/%s", configs.GetResourcesFilePath(), imagePath)
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.UploadError,
fmt.Sprintf("%s: %s", code.Text(code.UploadError), err.Error()),
))
return
}
res := new(uploadImageResponse)
res.RealImageUrl = filePath
res.PreviewImageUrl = fmt.Sprintf("resources/%s", imagePath)
ctx.Payload(res)
}
}

19
internal/api/user/app.go Normal file
View File

@ -0,0 +1,19 @@
package app
import (
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
usersvc "bindbox-game/internal/service/user"
)
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query
readDB *dao.Query
user usersvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{logger: logger, writeDB: dao.Use(db.GetDbW()), readDB: dao.Use(db.GetDbR()), user: usersvc.New(logger, db)}
}

View File

@ -0,0 +1,60 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type listCouponsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listCouponsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.UserCoupons `json:"list"`
}
// ListUserCoupons 查看用户优惠券
// @Summary 查看用户优惠券
// @Description 查看用户持有的优惠券列表
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listCouponsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/coupons [get]
func (h *handler) ListUserCoupons() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listCouponsRequest)
rsp := new(listCouponsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListCoupons(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,69 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type listInvitesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type inviteUserItem struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
InviteCode string `json:"invite_code"`
}
type listInvitesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*inviteUserItem `json:"list"`
}
// ListUserInvites 查看用户邀请记录
// @Summary 查看用户邀请记录
// @Description 查看被该用户邀请的用户列表
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listInvitesResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/invites [get]
func (h *handler) ListUserInvites() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listInvitesRequest)
rsp := new(listInvitesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListInvites(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
var list = make([]*inviteUserItem, 0, len(items))
for _, it := range items {
list = append(list, &inviteUserItem{ID: it.ID, Nickname: it.Nickname, Avatar: it.Avatar, InviteCode: it.InviteCode})
}
rsp.List = list
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,116 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type listUserItemCardsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listUserItemCardsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.UserItemCards `json:"list"`
}
// ListUserItemCards 获取用户道具卡列表
// @Summary 获取用户道具卡列表
// @Description 获取指定用户的道具卡列表,支持分页
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query integer false "页码默认1"
// @Param page_size query integer false "每页条数默认10"
// @Success 200 {object} listUserItemCardsResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/app/users/{user_id}/item_cards [get]
func (h *handler) ListUserItemCards() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listUserItemCardsRequest)
rsp := new(listUserItemCardsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListUserItemCards(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10008, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
type listUserItemCardUsesRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listUserItemCardUsesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.ActivityDrawEffects `json:"list"`
}
// ListUserItemCardUses 获取用户道具卡使用记录
// @Summary 获取用户道具卡使用记录
// @Description 获取指定用户的道具卡使用记录,支持分页
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query integer false "页码默认1"
// @Param page_size query integer false "每页条数默认10"
// @Success 200 {object} listUserItemCardUsesResponse
// @Failure 400 {object} code.Failure "参数错误"
// @Failure 401 {object} code.Failure "未授权"
// @Failure 500 {object} code.Failure "服务器内部错误"
// @Router /api/app/users/{user_id}/item_cards/uses [get]
func (h *handler) ListUserItemCardUses() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listUserItemCardUsesRequest)
rsp := new(listUserItemCardUsesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListUserItemCardUses(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10009, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,72 @@
package app
import (
"net/http"
"time"
"bindbox-game/configs"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/jwtoken"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/pkg/wechat"
"bindbox-game/internal/proposal"
usersvc "bindbox-game/internal/service/user"
)
type weixinLoginRequest struct {
Code string `json:"code"`
InviteCode string `json:"invite_code"`
}
type weixinLoginResponse struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
InviteCode string `json:"invite_code"`
Token string `json:"token"`
}
// WeixinLogin 微信登录
// @Summary 微信登录
// @Description 微信静默登录(需传递 code可选 invite_code
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param RequestBody body weixinLoginRequest true "请求参数"
// @Success 200 {object} weixinLoginResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/weixin/login [post]
func (h *handler) WeixinLogin() core.HandlerFunc {
return func(ctx core.Context) {
req := new(weixinLoginRequest)
rsp := new(weixinLoginResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
cfg := configs.Get()
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret}
c2s, err := wechat.Code2Session(ctx, wxcfg, req.Code)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
return
}
in := usersvc.LoginWeixinInput{OpenID: c2s.OpenID, UnionID: c2s.UnionID, InviteCode: req.InviteCode}
u, err := h.user.LoginWeixin(ctx.RequestContext(), in)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
return
}
rsp.UserID = u.ID
rsp.Nickname = u.Nickname
rsp.Avatar = u.Avatar
rsp.InviteCode = u.InviteCode
sessionUserInfo := proposal.SessionUserInfo{Id: int32(u.ID), UserName: u.Nickname, NickName: u.Nickname, IsSuper: 0, Platform: "APP"}
tokenString, tErr := jwtoken.New(configs.Get().JWT.PatientSecret).Sign(sessionUserInfo, 30*24*time.Hour)
if tErr == nil {
rsp.Token = tokenString
}
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,60 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type listOrdersRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listOrdersResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.Orders `json:"list"`
}
// ListUserOrders 查看用户订单记录
// @Summary 查看用户订单记录
// @Description 查看用户抽奖来源订单记录
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listOrdersResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/orders [get]
func (h *handler) ListUserOrders() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listOrdersRequest)
rsp := new(listOrdersResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListOrders(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,77 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/configs"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/miniprogram"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/pkg/wechat"
)
type bindPhoneRequest struct {
Code string `json:"code"`
}
type bindPhoneResponse struct {
Success bool `json:"success"`
Mobile string `json:"mobile"`
}
// BindPhone 绑定手机号
// @Summary 绑定手机号
// @Description 使用微信手机号 code 换取手机号并绑定到指定用户
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param RequestBody body bindPhoneRequest true "请求参数"
// @Success 200 {object} bindPhoneResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/phone/bind [post]
func (h *handler) BindPhone() core.HandlerFunc {
return func(ctx core.Context) {
req := new(bindPhoneRequest)
rsp := new(bindPhoneResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
uidStr := ctx.Param("user_id")
userID, _ := strconv.ParseInt(uidStr, 10, 64)
if userID <= 0 || req.Code == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "缺少必要参数"))
return
}
cfg := configs.Get()
var tokenRes struct {
AccessToken string `json:"access_token"`
}
if err := miniprogram.GetAccessToken(cfg.Wechat.AppID, cfg.Wechat.AppSecret, &tokenRes); err != nil || tokenRes.AccessToken == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "获取微信access_token失败"))
return
}
pn, err := wechat.GetPhoneNumber(ctx, tokenRes.AccessToken, req.Code)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
return
}
mobile := pn.PhoneInfo.PurePhoneNumber
if mobile == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "手机号为空"))
return
}
if _, err := h.writeDB.Users.WithContext(ctx.RequestContext()).Where(h.writeDB.Users.ID.Eq(userID)).Updates(map[string]any{"mobile": mobile}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
return
}
rsp.Success = true
rsp.Mobile = mobile
ctx.Payload(rsp)
}
}

View File

@ -0,0 +1,91 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type listPointsRequest struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type listPointsResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []*model.UserPointsLedger `json:"list"`
}
type pointsBalanceResponse struct {
Balance int64 `json:"balance"`
}
// ListUserPoints 查看用户积分记录
// @Summary 查看用户积分记录
// @Description 查看用户积分流水记录
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listPointsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/points [get]
func (h *handler) ListUserPoints() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listPointsRequest)
rsp := new(listPointsResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
items, total, err := h.user.ListPointsLedger(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10004, err.Error()))
return
}
rsp.Page = req.Page
rsp.PageSize = req.PageSize
rsp.Total = total
rsp.List = items
ctx.Payload(rsp)
}
}
// GetUserPointsBalance 查看用户积分余额
// @Summary 查看用户积分余额
// @Description 查看用户积分余额(过滤过期积分)
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Param user_id path integer true "用户ID"
// @Success 200 {object} pointsBalanceResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/points/balance [get]
func (h *handler) GetUserPointsBalance() core.HandlerFunc {
return func(ctx core.Context) {
rsp := new(pointsBalanceResponse)
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
return
}
total, err := h.user.GetPointsBalance(ctx.RequestContext(), userID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10005, err.Error()))
return
}
rsp.Balance = total
ctx.Payload(rsp)
}
}

Some files were not shown because too many files have changed in this diff Show More