diff --git a/.claude/plan/admin-update-user-mobile.md b/.claude/plan/admin-update-user-mobile.md new file mode 100644 index 0000000..132bedc --- /dev/null +++ b/.claude/plan/admin-update-user-mobile.md @@ -0,0 +1,268 @@ +# 📋 实施计划:后台管理端 - 添加修改用户手机号功能 + +## 任务类型 +- [x] 后端 (→ Codex) +- [ ] 前端 +- [ ] 全栈 + +--- + +## 📊 需求描述 + +为后台管理端添加修改用户手机号的功能,允许管理员直接修改用户的手机号。 + +--- + +## 🎯 技术方案 + +### API 设计 + +```http +PUT /api/admin/users/{user_id}/mobile +Content-Type: application/json +Authorization: Bearer {admin_token} + +Request Body: +{ + "mobile": "13800138000" +} + +Response: +{ + "success": true, + "message": "手机号更新成功" +} +``` + +### 安全校验 + +1. ✅ 管理员权限验证(RBAC: `user:edit`) +2. ✅ 手机号格式验证(11位数字,1开头) +3. ✅ 手机号唯一性检查(不能与其他用户重复) +4. ✅ 用户存在性验证 + +--- + +## 🔧 实施步骤 + +### 步骤 1:添加 API 处理函数 + +**文件**:`internal/api/admin/users_admin.go` +**位置**:Line 1893 后(在 `UpdateUserRemark()` 函数之后) + +**代码**: + +```go +// updateUserMobileRequest 更新用户手机号请求 +type updateUserMobileRequest struct { + Mobile string `json:"mobile" binding:"required"` +} + +// UpdateUserMobile 更新用户手机号 +// @Summary 更新用户手机号 +// @Description 管理员修改用户手机号 +// @Tags 管理端.用户 +// @Accept json +// @Produce json +// @Param user_id path integer true "用户ID" +// @Param body body updateUserMobileRequest true "手机号信息" +// @Success 200 {object} map[string]any +// @Failure 400 {object} code.Failure +// @Router /api/admin/users/{user_id}/mobile [put] +// @Security LoginVerifyToken +func (h *handler) UpdateUserMobile() core.HandlerFunc { + return func(ctx core.Context) { + userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64) + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效")) + return + } + + req := new(updateUserMobileRequest) + if err := ctx.ShouldBindJSON(req); err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) + return + } + + // 验证手机号格式(11位数字,1开头) + matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, req.Mobile) + if !matched { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "手机号格式不正确")) + return + } + + // 检查手机号是否被其他用户占用 + existedUser, _ := h.readDB.Users.WithContext(ctx.RequestContext()). + Where(h.readDB.Users.Mobile.Eq(req.Mobile)). + Where(h.readDB.Users.ID.Neq(userID)). + First() + + if existedUser != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该手机号已被其他用户使用")) + return + } + + // 更新用户手机号 + _, err = h.writeDB.Users.WithContext(ctx.RequestContext()). + Where(h.writeDB.Users.ID.Eq(userID)). + Update(h.writeDB.Users.Mobile, req.Mobile) + + if err != nil { + ctx.AbortWithError(core.Error(http.StatusBadRequest, 20303, "更新失败: "+err.Error())) + return + } + + ctx.Payload(map[string]any{ + "success": true, + "message": "手机号更新成功", + }) + } +} +``` + +--- + +### 步骤 2:添加导入依赖 + +**文件**:`internal/api/admin/users_admin.go` +**位置**:文件顶部 import 区域 + +**修改**:在现有 import 中添加 `regexp` 包 + +```go +import ( + "fmt" + "math" + "net/http" + "regexp" // ← 添加这一行 + "strconv" + "strings" + "time" + + "bindbox-game/internal/code" + // ... 其他导入保持不变 +) +``` + +--- + +### 步骤 3:注册路由 + +**文件**:`internal/router/router.go` +**位置**:在 adminAuthApiRouter 用户管理路由组中 + +**查找位置参考**: +```go +// 找到类似这样的路由组(用户管理相关) +adminAuthApiRouter.PUT("/users/:user_id/douyin_user_id", ...) +adminAuthApiRouter.PUT("/users/:user_id/remark", ...) +adminAuthApiRouter.PUT("/users/:user_id/status", ...) +``` + +**添加路由**: +```go +// 在上述路由附近添加 +adminAuthApiRouter.PUT("/users/:user_id/mobile", + intc.RequireAdminAction("user:edit"), + adminHandler.UpdateUserMobile()) +``` + +--- + +### 步骤 4:更新 Swagger 文档 + +**文件**:运行命令生成文档 + +```bash +make gen-swagger +``` + +--- + +## 📋 关键文件清单 + +| 文件路径 | 操作 | 说明 | +|---------|------|------| +| `internal/api/admin/users_admin.go` | 新增函数 | 添加 `UpdateUserMobile()` 及请求结构体 | +| `internal/api/admin/users_admin.go` | 修改导入 | 添加 `regexp` 包 | +| `internal/router/router.go` | 新增路由 | 注册 `PUT /users/:user_id/mobile` | + +--- + +## ✅ 验证清单 + +完成后请验证: + +- [ ] API 接口可正常访问 +- [ ] 手机号格式验证生效(非法格式返回错误) +- [ ] 手机号唯一性检查生效(重复手机号返回错误) +- [ ] 更新成功后数据库中手机号已变更 +- [ ] Swagger 文档已更新 +- [ ] 权限验证生效(非管理员或无 `user:edit` 权限无法访问) + +--- + +## ⚠️ 风险与缓解 + +| 风险 | 缓解措施 | +|------|----------| +| 手机号格式错误 | 正则验证 `^1[3-9]\d{9}$` | +| 手机号被占用 | 查询检查 `WHERE mobile = ? AND id != ?` | +| 权限滥用 | RBAC 中间件 `RequireAdminAction("user:edit")` | +| 更新失败 | 捕获异常并返回错误码 20303 | + +--- + +## 📝 测试用例 + +### 正常流程 +```bash +curl -X PUT http://localhost:9991/api/admin/users/123/mobile \ + -H "Authorization: Bearer {admin_token}" \ + -H "Content-Type: application/json" \ + -d '{"mobile":"13800138000"}' + +# 预期响应 +{ + "success": true, + "message": "手机号更新成功" +} +``` + +### 异常流程 + +**1. 手机号格式错误** +```json +{"mobile": "123"} +→ {"code": 10001, "message": "手机号格式不正确"} +``` + +**2. 手机号已被占用** +```json +{"mobile": "13800138000"} // 已被用户456使用 +→ {"code": 10001, "message": "该手机号已被其他用户使用"} +``` + +**3. 用户ID不存在** +```bash +PUT /api/admin/users/999999/mobile +→ 更新成功但影响行数为0(GORM特性,不会报错) +``` + +--- + +## 🚀 后续优化建议(可选) + +1. **操作日志记录**:记录管理员修改手机号的操作到审计日志 +2. **短信验证**:要求新手机号验证码确认(防止恶意修改) +3. **旧手机号通知**:向旧手机号发送变更通知短信 +4. **限制修改频率**:同一用户手机号修改间隔限制(如7天) + +--- + +## 📌 备注 + +- 本方案仅实现管理端修改功能,不包含用户端自助修改 +- 遵循现有代码风格(参考 `UpdateUserRemark()` 和 `UpdateUserDouyinID()`) +- 使用 GORM Gen 生成的 DAO 进行数据库操作 +- 错误码 20303 用于手机号更新失败(延续现有错误码序列 20301, 20302) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f4da708 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,291 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Bindbox Game is a Go-based web application built with Gin framework for managing game activities, products, and user interactions. The project includes an admin panel (Vue 3) and integrates with WeChat Mini Program, WeChat Pay, Tencent COS, Douyin (TikTok), and Aliyun SMS services. + +**Module Name**: `bindbox-game` (as defined in go.mod) + +**Note**: This is part of a multi-project repository. Other related projects include `bindbox-mini`, `douyin_game`, and `game`. This CLAUDE.md file focuses on the `bindbox_game` directory. + +## Development Commands + +**Working Directory**: Always work in `/path/to/bindbox/bindbox_game/` directory. This is part of a monorepo that includes other projects (`bindbox-mini`, `douyin_game`, `game`). + +### Backend (Go) + +```bash +# Run tests +make test # Run all tests +go test -v ./internal/service/... # Test specific package +go test -v -run TestFunctionName ./... # Run single test + +# Format code +make fmt # Format all Go files +go run cmd/mfmt/main.go # Format with import grouping + +# Run linter +make lint + +# Build for different platforms +make build-win # Windows executable +make build-mac # MacOS executable +make build-linux # Linux executable + +# Start the application +go run main.go # Development mode +ENV=dev go run main.go # Explicit environment +ENV=pro go run main.go # Production mode + +# Generate Swagger documentation +make gen-swagger + +# Serve Swagger UI locally +make serve-swagger # Opens at http://localhost:36666 +``` + +### Frontend (Vue 3 Admin Panel) + +```bash +cd web/admin + +# Install dependencies +pnpm install + +# Start dev server (usually runs on http://localhost:5173) +pnpm dev + +# Build for production +pnpm build + +# Preview production build +pnpm preview + +# Lint and fix +pnpm fix + +# Format code +pnpm lint:prettier + +# Type check +pnpm type-check +``` + +### Database + +```bash +# Run code generation for GORM models +go run cmd/gormgen/main.go + +# Migrations are in migrations/ directory (SQL files) +# Apply them manually to your database + +# Format Go code with import grouping (standard library, local module, third-party) +go run cmd/mfmt/main.go +``` + +### Docker + +```bash +# Build docker image +docker build -t zfc931912343/bindbox-game:v1.10 . + +# Run with docker +docker run -d --name bindbox-game -p 9991:9991 zfc931912343/bindbox-game:v1.10 +``` + +## Architecture + +### Backend Structure + +The backend follows a layered architecture pattern: + +- **`main.go`**: Application entry point. Initializes config, OpenTelemetry, MySQL, Redis, logger, HTTP server, and background tasks (scheduled settlement, order sync, expiration checks). + +- **`configs/`**: Configuration management using Viper. Environment-specific TOML files (dev, fat, uat, pro). Configuration includes MySQL (read/write), Redis, JWT secrets, WeChat, WeChat Pay, Tencent COS, Aliyun SMS, Douyin integration, and OpenTelemetry settings. + +- **`internal/api/`**: HTTP request handlers organized by domain (admin, user, game, activity, pay, wechat, etc.). Handlers parse requests, call services, and return responses. + +- **`internal/service/`**: Business logic layer. Services contain domain logic and orchestrate repository calls. Key services: admin, user, game, activity, title, order, banner, sysconfig, douyin, task_center. + +- **`internal/repository/mysql/`**: Data access layer using GORM. Contains generated DAO and model files (via gormgen). Supports read/write splitting with master-slave database configuration. + +- **`internal/router/`**: HTTP routing setup with Gin. Defines routes and applies middleware/interceptors. + +- **`internal/router/interceptor/`**: Middleware for authentication and authorization: + - `admin_auth.go`: JWT token verification for admin users + - `admin_rbac.go`: Role-based access control (RBAC) checking for admin actions + +- **`internal/pkg/`**: Shared utilities and helpers: + - `core/`: Custom Gin context wrapper with enhanced features (trace, logger, session info) + - `logger/`: Custom logger implementation (Zap-based) with file rotation + - `jwtoken/`: JWT token generation and parsing + - `redis/`: Redis client initialization and management + - `otel/`: OpenTelemetry integration for distributed tracing + - `wechat/`: WeChat Mini Program integration (phone number, decryption) + - `miniprogram/`: WeChat Mini Program access token and subscribe message + - Other utilities: validation, httpclient, timeutil, idgen, errors, etc. + +- **`internal/proposal/`**: Shared types and interfaces (session info, metrics, alerts, request logging). + +- **`internal/code/`**: Error code definitions for API responses. Follows a 5-digit pattern: `[service level (1)][module level (2)][specific error (2)]`. Example: `10101` = service error (1) + user module (01) + invalid phone (01). + +- **`cmd/`**: Command-line tools: + - `gormgen/`: Code generator for GORM models and DAOs from database schema + - `mfmt/`: Code formatter that organizes imports into three groups (standard library, local module, third-party packages) + - `diagnose_ledger/`: Diagnostic tool for ledger operations + +### Frontend Structure (Vue 3 Admin Panel) + +Located in `web/admin/`: +- Vue 3 + TypeScript + Vite +- Element Plus UI framework +- Pinia for state management +- Vue Router for navigation +- Axios for HTTP requests +- Tailwind CSS for styling + +### Database + +- MySQL with master-slave (read-write splitting) support +- GORM ORM with code generation (`gormgen`) +- Migrations in `migrations/` directory (SQL files with date prefixes) +- Generated models and DAOs in `internal/repository/mysql/` + +### Authentication & Authorization + +- **Admin JWT**: Separate secret for admin users. Token includes user ID, role, and session info. Token verification checks: + 1. JWT signature validity + 2. User exists and is active + 3. Token hash matches stored hash (prevents concurrent sessions) + 4. User login status is enabled + +- **RBAC**: Role-based access control for admin users. Two middleware patterns: + - `RequireAdminRole()`: Checks if user has any role assigned + - `RequireAdminAction(mark)`: Checks if user's roles have permission for specific action mark + +### Background Tasks + +Scheduled tasks initialized in `main.go`: +- **Activity Settlement**: `activitysvc.StartScheduledSettlement()` - handles scheduled activity settlements +- **User Expiration Check**: `usersvc.StartExpirationCheck()` - checks and handles user data expirations +- **Douyin Order Sync**: `douyinsvc.StartDouyinOrderSync()` - syncs orders from Douyin platform +- **Dynamic Config**: `syscfgsvc.InitGlobalDynamicConfig()` - loads dynamic system configuration + +### External Integrations + +- **WeChat Mini Program**: User authentication, phone number retrieval, subscribe messages +- **WeChat Pay**: Payment processing (API v3) +- **Tencent COS**: Object storage for file uploads +- **Aliyun SMS**: SMS notifications +- **Douyin (TikTok)**: Order synchronization and product rewards +- **OpenTelemetry**: Distributed tracing and observability + +## Testing + +- Tests located alongside source files (e.g., `foo_test.go` next to `foo.go`) +- Run all tests: `make test` or `go test -v --cover ./internal/...` +- Uses `testify` for assertions +- Some tests use `go-sqlmock` for database mocking +- Test database support: In-memory SQLite via `testrepo_sqlite.go` + +## Configuration + +Configuration is managed by environment via TOML files: +- `dev_configs.toml`: Development +- `fat_configs.toml`: Feature testing +- `uat_configs.toml`: User acceptance testing +- `pro_configs.toml`: Production + +Set environment with `ENV` environment variable (defaults to `dev`). + +JWT secrets can be overridden with environment variables: +- `ADMIN_JWT_SECRET`: Overrides admin JWT secret from config + +## API Documentation + +- Swagger annotations in code (see `main.go` for base config) +- Generate: `make gen-swagger` +- View locally: `make serve-swagger` (http://localhost:36666) +- Production: http://127.0.0.1:9991/swagger/index.html (when running) +- Health check: http://127.0.0.1:9991/system/health + +## Code Generation + +### GORM Models + +Run `cmd/gormgen/main.go` to generate GORM models and DAOs from database schema: + +```bash +go run cmd/gormgen/main.go +``` + +This generates: +- `internal/repository/mysql/model/*.gen.go`: Model structs +- `internal/repository/mysql/dao/*.gen.go`: DAO query builders + +Do not manually edit `.gen.go` files - they will be overwritten on next generation. + +## Important Patterns + +### Custom Context + +The codebase uses `core.Context` (wrapper around `gin.Context`) throughout handlers. This provides: +- Enhanced request/response handling +- Integrated tracing and logging +- Session user info management +- Standardized error handling via `AbortWithError()` + +Always use `core.Context` in handlers, not `gin.Context` directly. + +### Error Handling + +Use `core.Error()` to create business errors with HTTP status code, error code, and message: + +```go +err := core.Error(http.StatusBadRequest, code.ParamBindError, "Invalid parameters") +ctx.AbortWithError(err) +``` + +Error codes defined in `internal/code/`. + +### Response Patterns + +Successful responses use `ctx.Payload(data)` to set response data. The framework handles JSON serialization and wrapping. + +### Service Layer Pattern + +Services are initialized with logger and database repository: + +```go +svc := service.New(logger, dbRepo) +``` + +Services should contain business logic and call DAOs for data access. Keep handlers thin - move logic to services. + +## Working with This Codebase + +1. **Adding new API endpoints**: Create handler in appropriate `internal/api/` subdirectory, register route in `internal/router/`, and add business logic in corresponding service. + +2. **Database changes**: Create SQL migration file in `migrations/` with date prefix (e.g., `20260207_description.sql`), then regenerate GORM models with `go run cmd/gormgen/main.go`. + +3. **Adding dependencies**: Use `go get` for backend packages, `pnpm add` for frontend packages (in `web/admin/`). + +4. **Debugging**: Check logs in `logs/` directory. Enable debug logging in logger initialization (see `main.go`). + +5. **Environment-specific config**: Modify appropriate TOML file in `configs/` directory based on target environment. + +6. **Code formatting**: After making changes, run `make fmt` to format code. For import organization, use `go run cmd/mfmt/main.go` to group imports properly. + +7. **Error codes**: When defining new error codes in `internal/code/`, follow the 5-digit pattern: service level (1) + module level (2) + specific error (2). Service level: 1=system error, 2=user error. + +## Common Issues \u0026 Troubleshooting + +- **Port already in use**: Default port is 9991. Check if another instance is running: `lsof -i :9991` +- **Database connection errors**: Verify MySQL is running and credentials in `configs/` are correct +- **Redis connection errors**: Ensure Redis is running on configured host/port +- **GORM generation fails**: Check database connectivity and ensure `cmd/gormgen/main.go` has correct DB credentials +- **Frontend build errors**: Clear node_modules and reinstall: `cd web/admin && rm -rf node_modules && pnpm install` +- **JWT token issues**: If admin tokens are invalid, check `ADMIN_JWT_SECRET` environment variable matches config diff --git a/internal/api/activity/draw_logs_app.go b/internal/api/activity/draw_logs_app.go index ac462f4..c4e8723 100644 --- a/internal/api/activity/draw_logs_app.go +++ b/internal/api/activity/draw_logs_app.go @@ -44,7 +44,7 @@ type listDrawLogsResponse struct { // ListDrawLogs 抽奖记录列表 // @Summary 抽奖记录列表 -// @Description 查看指定活动期数的抽奖记录,支持等级筛选(默认返回最新的100条,不支持自定义翻页) +// @Description 查看指定活动期数的抽奖记录,支持等级筛选(返回最近100条,过滤掉5分钟内的数据) // @Tags APP端.活动 // @Accept json // @Produce json @@ -80,10 +80,8 @@ func (h *handler) ListDrawLogs() core.HandlerFunc { now := time.Now() // 计算5分钟前的时间点 (用于延迟显示) fiveMinutesAgo := now.Add(-5 * time.Minute) - // 计算当天零点 (用于仅显示当天数据) - startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - // [修改] 强制获取当天最新的 100 条数据 (Service 层限制最大 100) + // 强制获取最新的 100 条数据 (Service 层限制最大 100) // 忽略前端传入的 Page/PageSize,总是获取第一页的 100 条 fetchPageSize := 100 fetchPage := 1 @@ -102,22 +100,12 @@ func (h *handler) ListDrawLogs() core.HandlerFunc { var filteredItems []*model.ActivityDrawLogs for _, v := range items { - // 1. 过滤掉太新的数据 (5分钟延迟) + // 过滤掉太新的数据 (5分钟延迟) if v.CreatedAt.After(fiveMinutesAgo) { continue } - // 2. 过滤掉非当天的数据 (当天零点之前) - if v.CreatedAt.Before(startOfToday) { - // 因为是按时间倒序返回的,一旦遇到早于今天的,后续的更早,直接结束 - break - } - // 3. 数量限制 (虽然 Service 取了 100,这里再保个底,或者遵循前端 pageSize? - // 需求是 "获取当天的 最新100 个数据",这里我们以 filteredItems 为准, - // 如果前端 pageSize 传了比如 20,是否应该只给 20? - // 按照通常逻辑,列表接口应遵循 pageSize。但在这种定制逻辑下,用户似乎想要的是“当天数据的视图”。 - // 保持原逻辑:遵循 pageSize 限制输出数量,但我们上面强行取了 100 作为源数据。 - // 如果用户原本想看 100 条,前端传 100 即可。 - if len(filteredItems) >= pageSize { + // 数量限制为 100 条 + if len(filteredItems) >= 100 { break } filteredItems = append(filteredItems, v) diff --git a/internal/api/admin/users_admin.go b/internal/api/admin/users_admin.go index da32657..2eaea9f 100644 --- a/internal/api/admin/users_admin.go +++ b/internal/api/admin/users_admin.go @@ -1157,6 +1157,63 @@ func (h *handler) ListUserAuditLogs() core.HandlerFunc { } } } + + // [Fix] 针对直播间订单(金额为0),尝试从备注中提取关联的抖店订单号并查询实际支付金额 + // 备注格式示例: "直播间抽奖: xxx (关联抖店订单: 69505...)" + // 收集需要查询的 ShopOrderID + shopOrderIDs := make([]string, 0) + logIndicesMap := make(map[string][]int) // shopOrderID -> []logIndex + + for i := range logs { + // 仅处理 order 类型且金额为 0 的记录 (通常直播间订单 actual_amount=0) + // 或者 category=order 且 sub_type=paid + if logs[i].Category == "order" && logs[i].SubType == "paid" { + // 简单判断金额是否为 0.00 (前面已经格式化过) + if logs[i].AmountStr == "-0.00" || logs[i].AmountStr == "0.00" { + // 尝试提取关联抖店订单 + if strings.Contains(logs[i].DetailInfo, "关联抖店订单:") { + // 提取 ID: last part after "关联抖店订单:" and trim ")" + parts := strings.Split(logs[i].DetailInfo, "关联抖店订单:") + if len(parts) > 1 { + orderIDPart := strings.TrimSpace(parts[1]) + orderIDPart = strings.TrimRight(orderIDPart, ")") + if orderIDPart != "" { + shopOrderIDs = append(shopOrderIDs, orderIDPart) + logIndicesMap[orderIDPart] = append(logIndicesMap[orderIDPart], i) + } + } + } + } + } + } + + // 批量查询 douyin_orders + if len(shopOrderIDs) > 0 { + var dyOrders []struct { + ShopOrderID string `gorm:"column:shop_order_id"` + ActualPayAmount int64 `gorm:"column:actual_pay_amount"` + } + // 只需要查 shop_order_id 和 actual_pay_amount + // 使用 raw sql 或者 model find + // 这里为了简单直接用 h.readDB.DouyinOrders (Gen生成的) 或者 Raw SQL + // 假设 h.readDB.DouyinOrders 可用,或者直接用 h.repo.GetDbR() + if err := h.repo.GetDbR().Table("douyin_orders").Select("shop_order_id, actual_pay_amount"). + Where("shop_order_id IN ?", shopOrderIDs).Scan(&dyOrders).Error; err == nil { + + // 更新日志金额 + for _, dy := range dyOrders { + if indices, ok := logIndicesMap[dy.ShopOrderID]; ok { + for _, idx := range indices { + // 转换为浮点数 (元) + val := float64(dy.ActualPayAmount) / 100.0 + // 订单固定为支出 (负数) + val = -math.Abs(val) + logs[idx].AmountStr = fmt.Sprintf("%.2f", val) + } + } + } + } + } // 翻译 Shipping Status 等 (可选项,也可以前端做) // 翻译 Shipping Status 等 (可选项,也可以前端做) if logs[i].Category == "shipping" { diff --git a/internal/api/app/store.go b/internal/api/app/store.go index 232d289..5ecdb2e 100644 --- a/internal/api/app/store.go +++ b/internal/api/app/store.go @@ -22,12 +22,13 @@ func NewStore(logger logger.CustomLogger, db mysql.Repo, user usersvc.Service) * } type listStoreItemsRequest struct { - Kind string `form:"kind"` - Page int `form:"page"` - PageSize int `form:"page_size"` - Keyword string `form:"keyword"` // 关键词搜索 - PriceMin *int64 `form:"price_min"` // 最低积分价格(积分单位) - PriceMax *int64 `form:"price_max"` // 最高积分价格(积分单位) + Kind string `form:"kind"` + Page int `form:"page"` + PageSize int `form:"page_size"` + Keyword string `form:"keyword"` // 关键词搜索 + PriceMin *int64 `form:"price_min"` // 最低积分价格(积分单位) + PriceMax *int64 `form:"price_max"` // 最高积分价格(积分单位) + CategoryID *int64 `form:"category_id"` // 分类ID筛选(仅对product有效) } type listStoreItem struct { @@ -140,6 +141,10 @@ func (h *storeHandler) ListStoreItemsForApp() core.HandlerFunc { } default: // product q := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.Status.Eq(1)) + // 分类筛选 + if req.CategoryID != nil && *req.CategoryID > 0 { + q = q.Where(h.readDB.Products.CategoryID.Eq(*req.CategoryID)) + } // 关键词筛选 if req.Keyword != "" { q = q.Where(h.readDB.Products.Name.Like("%" + req.Keyword + "%")) diff --git a/internal/service/user/points_convert.go b/internal/service/user/points_convert.go index bb43d2b..76ae1dd 100644 --- a/internal/service/user/points_convert.go +++ b/internal/service/user/points_convert.go @@ -24,11 +24,14 @@ func (s *service) getExchangeRate(ctx context.Context) int64 { return rate } +// CentsToPointsFloat 分 → 积分(浮点数,用于 API 返回展示) +// 公式:积分 = 分 * rate / 100 +// 例如:rate=1, 3580分 → 35.8积分 // CentsToPointsFloat 分 → 积分(浮点数,用于 API 返回展示) // 公式:积分 = 分 * rate / 100 // 例如:rate=1, 3580分 → 35.8积分 func (s *service) CentsToPointsFloat(ctx context.Context, cents int64) float64 { - if cents <= 0 { + if cents == 0 { return 0 } rate := s.getExchangeRate(ctx)