Compare commits
71 Commits
9214501756
...
6c59670c69
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c59670c69 | |||
| 269bdb9fd1 | |||
| dc1b324aef | |||
| 2cf9da6143 | |||
| 635924040a | |||
| 04791789c9 | |||
| c9a83a232a | |||
| 425e64daa5 | |||
| a7a0f639e1 | |||
| 16e2ede037 | |||
| c8b04e2bc6 | |||
| e2782a69d3 | |||
| 45815bfb7d | |||
| 642b3cf7dd | |||
| 2a89a1ab9d | |||
| 6ee627139c | |||
| 87ad4177b1 | |||
| 1b5a715a22 | |||
| 8141a47690 | |||
| 81e2fb5a75 | |||
| 00452cba59 | |||
| 42e7cb5f12 | |||
| 1ab39d2f5a | |||
| b847a72a6a | |||
|
|
00f758ecba | ||
|
|
4e236e084e | ||
| 78a2b442ec | |||
| 084b802b05 | |||
| 53cb31f6ce | |||
|
|
f92cb16a58 | ||
|
|
684511d51e | ||
|
|
e2fdf47088 | ||
|
|
0ffda3fb0b | ||
|
|
110ecb2818 | ||
|
|
4001993e96 | ||
|
|
7eade510a5 | ||
|
|
2e8eb69312 | ||
|
|
23ce0e6982 | ||
|
|
34c8394e8f | ||
| 53cca2d781 | |||
| f4f8552562 | |||
| 58eac2a6c7 | |||
| 35257f2c0a | |||
| 8e7cdbad1f | |||
| c983c96449 | |||
|
|
2213817a5a | ||
|
|
3e78590fff | ||
| a910871112 | |||
| 61c517eaf7 | |||
| 59c7471cad | |||
| 5717c97e7e | |||
|
|
ce83dc1b02 | ||
|
|
b4d30ddbce | ||
|
|
de9acfa863 | ||
| d469c4a8ab | |||
| 0a3f3d3cdc | |||
| 41c0d5e1ad | |||
|
|
271c97ae6c | ||
|
|
366e9c16e0 | ||
|
|
e61a293616 | ||
|
|
e77223be17 | ||
|
|
563214b62d | ||
|
|
6357e653f8 | ||
|
|
33af2cc54a | ||
|
|
5156a25019 | ||
|
|
ba0630b2da | ||
| a6ac558680 | |||
| a4e532c6b6 | |||
| e4d4258918 | |||
| 2e86f8ae42 | |||
| 4a40520a80 |
10
.gitignore
vendored
10
.gitignore
vendored
@ -27,13 +27,3 @@ go.work.sum
|
|||||||
resources/*
|
resources/*
|
||||||
build/resources/admin/
|
build/resources/admin/
|
||||||
logs/
|
logs/
|
||||||
web/*
|
|
||||||
|
|
||||||
# 敏感配置文件
|
|
||||||
configs/*.toml
|
|
||||||
!configs/*.example.toml
|
|
||||||
|
|
||||||
# 环境变量
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM golang:1.24.5-alpine AS builder
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -62,4 +62,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:9991/system/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:9991/system/health || exit 1
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["sh", "-c", "./miniChat -env=${ACTIVE_ENV}"]
|
CMD ["./miniChat"]
|
||||||
20
cmd/create_admin/main.go
Normal file
20
cmd/create_admin/main.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bindbox-game/internal/pkg/utils"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
password := "123456"
|
||||||
|
hash, err := utils.GenerateAdminHashedPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Password hash for '123456':")
|
||||||
|
fmt.Println(hash)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("SQL to insert admin:")
|
||||||
|
fmt.Printf("INSERT INTO admin (username, nickname, password, login_status, is_super, created_user, created_at) VALUES ('CC', 'CC', '%s', 1, 0, 'system', NOW());\n", hash)
|
||||||
|
}
|
||||||
@ -34,6 +34,6 @@ eg :
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# 根目录下执行
|
# 根目录下执行
|
||||||
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,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,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels,user_game_tickets,game_ticket_logs,order_snapshots,audit_rollback_logs,user_coupon_ledger,user_game_passes,game_pass_packages,livestream_activities,livestream_prizes,livestream_draw_logs"
|
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,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,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels,user_game_tickets,game_ticket_logs,order_snapshots,audit_rollback_logs,user_coupon_ledger,user_game_passes,game_pass_packages"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
28
cmd/handlergen/README.md
Normal file
28
cmd/handlergen/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
## 自动生成数据库模型和常见的 CRUD 操作
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go run cmd/handlergen/main.go -h
|
||||||
|
|
||||||
|
Usage of ./cmd/handlergen/main.go:
|
||||||
|
-table string
|
||||||
|
enter the required data table
|
||||||
|
```
|
||||||
|
|
||||||
|
#### -table
|
||||||
|
|
||||||
|
指定要生成的表名称。
|
||||||
|
|
||||||
|
eg :
|
||||||
|
|
||||||
|
```shell
|
||||||
|
--tables="admin" # generate from `admin`
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 根目录下执行
|
||||||
|
go run cmd/handlergen/main.go -table "customer"
|
||||||
|
```
|
||||||
265
cmd/handlergen/handler_template.go.tpl
Normal file
265
cmd/handlergen/handler_template.go.tpl
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
package {{.PackageName}}
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"bindbox-game/internal/code"
|
||||||
|
"bindbox-game/internal/pkg/core"
|
||||||
|
"WeChatService/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
logger logger.CustomLoggerLogger
|
||||||
|
writeDB *dao.Query
|
||||||
|
readDB *dao.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
type genResultInfo struct {
|
||||||
|
RowsAffected int64 `json:"rows_affected"`
|
||||||
|
Error error `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
||||||
|
return &handler{
|
||||||
|
logger: logger,
|
||||||
|
writeDB: dao.Use(db.GetDbW()),
|
||||||
|
readDB: dao.Use(db.GetDbR()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 新增数据
|
||||||
|
// @Summary 新增数据
|
||||||
|
// @Description 新增数据
|
||||||
|
// @Tags API.{{.VariableName}}
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param RequestBody body model.{{.StructName}} true "请求参数"
|
||||||
|
// @Success 200 {object} model.{{.StructName}}
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Router /api/{{.VariableName}} [post]
|
||||||
|
func (h *handler) Create() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
var createData model.{{.StructName}}
|
||||||
|
if err := ctx.ShouldBindJSON(&createData); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ParamBindError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.writeDB.{{.StructName}}.WithContext(ctx.RequestContext()).Create(&createData); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Payload(createData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取列表数据
|
||||||
|
// @Summary 获取列表数据
|
||||||
|
// @Description 获取列表数据
|
||||||
|
// @Tags API.{{.VariableName}}
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.{{.StructName}}
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Router /api/{{.VariableName}}s [get]
|
||||||
|
func (h *handler) List() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
list, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Find()
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Payload(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据 ID 获取数据
|
||||||
|
// @Summary 根据 ID 获取数据
|
||||||
|
// @Description 根据 ID 获取数据
|
||||||
|
// @Tags API.{{.VariableName}}
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} model.{{.StructName}}
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Router /api/{{.VariableName}}/{id} [get]
|
||||||
|
func (h *handler) GetByID() 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,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First()
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
"record not found"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Payload(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByID 根据 ID 删除数据
|
||||||
|
// @Summary 根据 ID 删除数据
|
||||||
|
// @Description 根据 ID 删除数据
|
||||||
|
// @Tags API.{{.VariableName}}
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Success 200 {object} genResultInfo
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Router /api/{{.VariableName}}/{id} [delete]
|
||||||
|
func (h *handler) DeleteByID() 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,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First()
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
"record not found"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.writeDB.{{.StructName}}.Delete(info)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultInfo := new(genResultInfo)
|
||||||
|
resultInfo.RowsAffected = result.RowsAffected
|
||||||
|
resultInfo.Error = result.Error
|
||||||
|
|
||||||
|
ctx.Payload(resultInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateByID 根据 ID 更新数据
|
||||||
|
// @Summary 根据 ID 更新数据
|
||||||
|
// @Description 根据 ID 更新数据
|
||||||
|
// @Tags API.{{.VariableName}}
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "ID"
|
||||||
|
// @Param RequestBody body model.{{.StructName}} true "请求参数"
|
||||||
|
// @Success 200 {object} genResultInfo
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Router /api/{{.VariableName}}/{id} [put]
|
||||||
|
func (h *handler) UpdateByID() 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,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateData map[string]interface{}
|
||||||
|
if err := ctx.ShouldBindJSON(&updateData); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := h.readDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.readDB.{{.StructName}}.ID.Eq(int32(id))).First()
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
"record not found"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.writeDB.{{.StructName}}.WithContext(ctx.RequestContext()).Where(h.writeDB.{{.StructName}}.ID.Eq(info.ID)).Updates(updateData)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultInfo := new(genResultInfo)
|
||||||
|
resultInfo.RowsAffected = result.RowsAffected
|
||||||
|
resultInfo.Error = result.Error
|
||||||
|
|
||||||
|
ctx.Payload(resultInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
cmd/handlergen/main.go
Normal file
90
cmd/handlergen/main.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateData struct {
|
||||||
|
PackageName string
|
||||||
|
VariableName string
|
||||||
|
StructName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
table := flag.String("table", "", "enter the required data table")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
tableName := *table
|
||||||
|
if tableName == "" {
|
||||||
|
log.Fatal("table cannot be empty, please provide a valid table name.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前工作目录
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error getting working directory:%s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板文件路径
|
||||||
|
tmplPath := fmt.Sprintf("%s/cmd/handlergen/handler_template.go.tpl", wd)
|
||||||
|
tmpl, err := template.ParseFiles(tmplPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Template file parsed: %s", tmplPath)
|
||||||
|
|
||||||
|
// 替换的变量
|
||||||
|
data := TemplateData{
|
||||||
|
PackageName: tableName,
|
||||||
|
VariableName: tableName,
|
||||||
|
StructName: toCamelCase(tableName),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成文件的目录和文件名
|
||||||
|
outputDir := fmt.Sprintf("%s/internal/api/%s", wd, tableName)
|
||||||
|
outputFile := filepath.Join(outputDir, fmt.Sprintf("%s.gen.go", tableName))
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
err = os.MkdirAll(outputDir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件
|
||||||
|
file, err := os.Create(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
log.Printf("File created: %s", outputFile)
|
||||||
|
|
||||||
|
// 执行模板并生成文件
|
||||||
|
err = tmpl.Execute(file, data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Template execution completed successfully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将字符串转为驼峰式命名
|
||||||
|
func toCamelCase(s string) string {
|
||||||
|
// 用下划线分割字符串
|
||||||
|
parts := strings.Split(s, "_")
|
||||||
|
|
||||||
|
// 对每个部分首字母大写
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
parts[i] = strings.Title(parts[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接所有部分
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
93
cmd/matching_sim/main.go
Normal file
93
cmd/matching_sim/main.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/pkg/redis"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
activitysvc "bindbox-game/internal/service/activity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// usage: go run cmd/matching_sim/main.go -env dev -runs 10000
|
||||||
|
func main() {
|
||||||
|
runs := flag.Int("runs", 10000, "运行模拟的次数")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 1. 初始化数据库
|
||||||
|
dbRepo, err := mysql.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("数据库连接失败: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化日志 (模拟 Service 需要)
|
||||||
|
l, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 初始化 Service (完全模拟真实注入)
|
||||||
|
// 注意:这里不需要真实的 user service,传入 nil 即可
|
||||||
|
svc := activitysvc.New(l, dbRepo, nil, redis.GetClient())
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 4. 从真实数据库加载卡牌配置
|
||||||
|
fmt.Println(">>> 正在从数据库加载真实卡牌配置...")
|
||||||
|
configs, err := svc.ListMatchingCardTypes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("读取卡牌配置失败: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configs) == 0 {
|
||||||
|
fmt.Println("警告: 数据库中没有启用的卡牌配置,将使用默认配置。")
|
||||||
|
configs = []activitysvc.CardTypeConfig{
|
||||||
|
{Code: "A", Quantity: 9}, {Code: "B", Quantity: 9}, {Code: "C", Quantity: 9},
|
||||||
|
{Code: "D", Quantity: 9}, {Code: "E", Quantity: 9}, {Code: "F", Quantity: 9},
|
||||||
|
{Code: "G", Quantity: 9}, {Code: "H", Quantity: 9}, {Code: "I", Quantity: 9},
|
||||||
|
{Code: "J", Quantity: 9}, {Code: "K", Quantity: 9},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("当前生效配置:")
|
||||||
|
for _, c := range configs {
|
||||||
|
fmt.Printf(" - [%s]: %d张\n", c.Code, c.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 开始执行模拟
|
||||||
|
fmt.Printf("\n>>> 正在执行 %d 次大规模真实模拟...\n", *runs)
|
||||||
|
results := make(map[int64]int)
|
||||||
|
mseed := []byte("production_simulation_seed")
|
||||||
|
position := "B" // 默认模拟选中 B 类型
|
||||||
|
|
||||||
|
for i := 0; i < *runs; i++ {
|
||||||
|
// 调用真实业务函数创建游戏 (固定数量逻辑)
|
||||||
|
game := activitysvc.NewMatchingGameWithConfig(configs, position, mseed)
|
||||||
|
|
||||||
|
// 调用真实业务模拟函数
|
||||||
|
pairs := game.SimulateMaxPairs()
|
||||||
|
results[pairs]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 统计并输出
|
||||||
|
fmt.Println("\n对数分布统计 (100% 模拟真实生产路径):")
|
||||||
|
var pairsList []int64
|
||||||
|
for k := range results {
|
||||||
|
pairsList = append(pairsList, k)
|
||||||
|
}
|
||||||
|
sort.Slice(pairsList, func(i, j int) bool {
|
||||||
|
return pairsList[i] < pairsList[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
sumPairs := int64(0)
|
||||||
|
for _, p := range pairsList {
|
||||||
|
count := results[p]
|
||||||
|
sumPairs += p * int64(count)
|
||||||
|
fmt.Printf(" %2d 对: %5d 次 (%5.2f%%)\n", p, count, float64(count)/float64(*runs)*100)
|
||||||
|
}
|
||||||
|
fmt.Printf("\n平均对数: %.4f\n\n", float64(sumPairs)/float64(*runs))
|
||||||
|
}
|
||||||
155
cmd/migrate_configs/main.go
Normal file
155
cmd/migrate_configs/main.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
"bindbox-game/internal/service/sysconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dryRun = flag.Bool("dry-run", false, "仅打印将要写入的配置,不实际写入数据库")
|
||||||
|
force = flag.Bool("force", false, "强制覆盖已存在的配置")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
dbRepo, err := mysql.New()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("数据库连接失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 logger (简化版)
|
||||||
|
customLogger, err := logger.NewCustomLogger(dao.Use(dbRepo.GetDbW()),
|
||||||
|
logger.WithDebugLevel(),
|
||||||
|
logger.WithOutputInConsole(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Logger 初始化失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 创建动态配置服务
|
||||||
|
dynamicCfg := sysconfig.NewDynamicConfig(customLogger, dbRepo)
|
||||||
|
staticCfg := configs.Get()
|
||||||
|
|
||||||
|
// 定义要迁移的配置项
|
||||||
|
type configItem struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
Remark string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取证书文件内容并 Base64 编码
|
||||||
|
readAndEncode := func(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("警告: 读取文件 %s 失败: %v\n", path, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []configItem{
|
||||||
|
// COS 配置
|
||||||
|
{sysconfig.KeyCOSBucket, staticCfg.COS.Bucket, "COS Bucket名称"},
|
||||||
|
{sysconfig.KeyCOSRegion, staticCfg.COS.Region, "COS 地域"},
|
||||||
|
{sysconfig.KeyCOSSecretID, staticCfg.COS.SecretID, "COS SecretID (加密存储)"},
|
||||||
|
{sysconfig.KeyCOSSecretKey, staticCfg.COS.SecretKey, "COS SecretKey (加密存储)"},
|
||||||
|
{sysconfig.KeyCOSBaseURL, staticCfg.COS.BaseURL, "COS 自定义域名"},
|
||||||
|
|
||||||
|
// 微信小程序配置
|
||||||
|
{sysconfig.KeyWechatAppID, staticCfg.Wechat.AppID, "微信小程序 AppID"},
|
||||||
|
{sysconfig.KeyWechatAppSecret, staticCfg.Wechat.AppSecret, "微信小程序 AppSecret (加密存储)"},
|
||||||
|
{sysconfig.KeyWechatLotteryResultTemplateID, staticCfg.Wechat.LotteryResultTemplateID, "中奖结果订阅消息模板ID"},
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
{sysconfig.KeyWechatPayMchID, staticCfg.WechatPay.MchID, "微信支付商户号"},
|
||||||
|
{sysconfig.KeyWechatPaySerialNo, staticCfg.WechatPay.SerialNo, "微信支付证书序列号"},
|
||||||
|
{sysconfig.KeyWechatPayPrivateKey, readAndEncode(staticCfg.WechatPay.PrivateKeyPath), "微信支付私钥 (Base64编码, 加密存储)"},
|
||||||
|
{sysconfig.KeyWechatPayApiV3Key, staticCfg.WechatPay.ApiV3Key, "微信支付 API v3 密钥 (加密存储)"},
|
||||||
|
{sysconfig.KeyWechatPayNotifyURL, staticCfg.WechatPay.NotifyURL, "微信支付回调地址"},
|
||||||
|
{sysconfig.KeyWechatPayPublicKeyID, staticCfg.WechatPay.PublicKeyID, "微信支付公钥ID"},
|
||||||
|
{sysconfig.KeyWechatPayPublicKey, readAndEncode(staticCfg.WechatPay.PublicKeyPath), "微信支付公钥 (Base64编码, 加密存储)"},
|
||||||
|
|
||||||
|
// 阿里云短信配置
|
||||||
|
{sysconfig.KeyAliyunSMSAccessKeyID, staticCfg.AliyunSMS.AccessKeyID, "阿里云短信 AccessKeyID"},
|
||||||
|
{sysconfig.KeyAliyunSMSAccessKeySecret, staticCfg.AliyunSMS.AccessKeySecret, "阿里云短信 AccessKeySecret (加密存储)"},
|
||||||
|
{sysconfig.KeyAliyunSMSSignName, staticCfg.AliyunSMS.SignName, "短信签名"},
|
||||||
|
{sysconfig.KeyAliyunSMSTemplateCode, staticCfg.AliyunSMS.TemplateCode, "短信模板Code"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("========== 配置迁移工具 ==========")
|
||||||
|
fmt.Printf("环境: %s\n", configs.ProjectName)
|
||||||
|
fmt.Printf("Dry Run: %v\n", *dryRun)
|
||||||
|
fmt.Printf("Force: %v\n", *force)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
skipCount := 0
|
||||||
|
failCount := 0
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Value == "" {
|
||||||
|
fmt.Printf("[跳过] %s: 值为空\n", item.Key)
|
||||||
|
skipCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
existing := dynamicCfg.Get(ctx, item.Key)
|
||||||
|
if existing != "" && !*force {
|
||||||
|
fmt.Printf("[跳过] %s: 已存在 (使用 -force 覆盖)\n", item.Key)
|
||||||
|
skipCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脱敏显示
|
||||||
|
displayValue := item.Value
|
||||||
|
if sysconfig.IsSensitiveKey(item.Key) {
|
||||||
|
if len(displayValue) > 8 {
|
||||||
|
displayValue = displayValue[:4] + "****" + displayValue[len(displayValue)-4:]
|
||||||
|
} else {
|
||||||
|
displayValue = "****"
|
||||||
|
}
|
||||||
|
} else if len(displayValue) > 50 {
|
||||||
|
displayValue = displayValue[:50] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
if *dryRun {
|
||||||
|
fmt.Printf("[预览] %s = %s\n", item.Key, displayValue)
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
if err := dynamicCfg.Set(ctx, item.Key, item.Value, item.Remark); err != nil {
|
||||||
|
fmt.Printf("[失败] %s: %v\n", item.Key, err)
|
||||||
|
failCount++
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[成功] %s = %s\n", item.Key, displayValue)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("========== 迁移结果 ==========\n")
|
||||||
|
fmt.Printf("成功: %d, 跳过: %d, 失败: %d\n", successCount, skipCount, failCount)
|
||||||
|
|
||||||
|
if *dryRun {
|
||||||
|
fmt.Println("\n这只是预览,使用不带 -dry-run 参数执行实际迁移")
|
||||||
|
}
|
||||||
|
}
|
||||||
263
cmd/tools/task_center_test/integration.go
Normal file
263
cmd/tools/task_center_test/integration.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
|
"bindbox-game/internal/pkg/logger"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
|
tcmodel "bindbox-game/internal/repository/mysql/task_center"
|
||||||
|
tasksvc "bindbox-game/internal/service/task_center"
|
||||||
|
"bindbox-game/internal/service/title"
|
||||||
|
"bindbox-game/internal/service/user"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IntegrationTest 运行集成测试流
|
||||||
|
func IntegrationTest(repo mysql.Repo) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg := configs.Get()
|
||||||
|
|
||||||
|
// 1. 初始化日志(自定义)
|
||||||
|
l, err := logger.NewCustomLogger(dao.Use(repo.GetDbW()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("初始化日志失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化 Redis
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.Redis.Addr,
|
||||||
|
Password: cfg.Redis.Pass,
|
||||||
|
DB: cfg.Redis.DB,
|
||||||
|
})
|
||||||
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||||
|
return fmt.Errorf("连接 Redis 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 初始化依赖服务
|
||||||
|
userSvc := user.New(l, repo)
|
||||||
|
titleSvc := title.New(l, repo)
|
||||||
|
taskSvc := tasksvc.New(l, repo, rdb, userSvc, titleSvc)
|
||||||
|
|
||||||
|
// 3.5 清理缓存以确保能加载最新配置
|
||||||
|
if err := rdb.Del(ctx, "task_center:active_tasks").Err(); err != nil {
|
||||||
|
fmt.Printf("⚠️ 清理缓存失败: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 选择一个测试用户和任务
|
||||||
|
// ... (代码逻辑不变)
|
||||||
|
userID := int64(8888)
|
||||||
|
|
||||||
|
// 搜索一个首单任务(满足 lifetime 窗口,奖励为点数)
|
||||||
|
var task tcmodel.Task
|
||||||
|
db := repo.GetDbW()
|
||||||
|
if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id").
|
||||||
|
Joins("JOIN task_center_task_rewards ON task_center_task_rewards.task_id = task_center_tasks.id").
|
||||||
|
Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ? AND task_center_task_rewards.reward_type = ?", "first_order", "lifetime", "points").
|
||||||
|
First(&task).Error; err != nil {
|
||||||
|
return fmt.Errorf("未找到符合条件的集成测试任务: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("--- 开始集成测试 ---\n")
|
||||||
|
fmt.Printf("用户ID: %d, 任务ID: %d (%s)\n", userID, task.ID, task.Name)
|
||||||
|
|
||||||
|
// 5. 创建一个模拟订单
|
||||||
|
orderNo := fmt.Sprintf("TEST_ORDER_%d", time.Now().Unix())
|
||||||
|
order := &model.Orders{
|
||||||
|
UserID: userID,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
TotalAmount: 100,
|
||||||
|
ActualAmount: 100,
|
||||||
|
Status: 2, // 已支付
|
||||||
|
PaidAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := db.Omit("cancelled_at").Create(order).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建测试订单失败: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("创建测试订单: %s (ID: %d)\n", orderNo, order.ID)
|
||||||
|
|
||||||
|
// 6. 触发 OnOrderPaid
|
||||||
|
fmt.Println("触发 OnOrderPaid 事件...")
|
||||||
|
if err := taskSvc.OnOrderPaid(ctx, userID, order.ID); err != nil {
|
||||||
|
return fmt.Errorf("OnOrderPaid 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 验证结果
|
||||||
|
// A. 检查进度是否更新
|
||||||
|
var progress tcmodel.UserTaskProgress
|
||||||
|
if err := db.Where("user_id = ? AND task_id = ?", userID, task.ID).First(&progress).Error; err != nil {
|
||||||
|
fmt.Printf("❌ 进度记录未找到: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ 进度记录已更新: first_order=%d\n", progress.FirstOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. 检查奖励日志
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
var eventLog tcmodel.TaskEventLog
|
||||||
|
if err := db.Where("user_id = ? AND task_id = ?", userID, task.ID).Order("id desc").First(&eventLog).Error; err != nil {
|
||||||
|
fmt.Printf("❌ 奖励日志未找到: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ 奖励日志已找到: Status=%s, Result=%s\n", eventLog.Status, eventLog.Result)
|
||||||
|
if eventLog.Status == "granted" {
|
||||||
|
fmt.Printf("🎉 集成测试通过!奖励已成功发放。\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("⚠️ 奖励发放状态异常: %s\n", eventLog.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteAndTaskIntegrationTest 运行邀请与任务全链路集成测试
|
||||||
|
func InviteAndTaskIntegrationTest(repo mysql.Repo) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg := configs.Get()
|
||||||
|
db := repo.GetDbW()
|
||||||
|
|
||||||
|
// 1. 初始化
|
||||||
|
l, _ := logger.NewCustomLogger(dao.Use(db))
|
||||||
|
rdb := redis.NewClient(&redis.Options{Addr: cfg.Redis.Addr, Password: cfg.Redis.Pass, DB: cfg.Redis.DB})
|
||||||
|
userSvc := user.New(l, repo)
|
||||||
|
titleSvc := title.New(l, repo)
|
||||||
|
taskSvc := tasksvc.New(l, repo, rdb, userSvc, titleSvc)
|
||||||
|
|
||||||
|
// 2. 准备角色
|
||||||
|
inviterID := int64(9001)
|
||||||
|
inviteeID := int64(9002)
|
||||||
|
_ = ensureUserExists(repo, inviterID, "老司机(邀请者)")
|
||||||
|
_ = ensureUserExists(repo, inviteeID, "萌新(被邀请者)")
|
||||||
|
|
||||||
|
// 3. 建立邀请关系
|
||||||
|
if err := ensureInviteRelationship(repo, inviterID, inviteeID); err != nil {
|
||||||
|
return fmt.Errorf("建立邀请关系失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清理 Redis 缓存
|
||||||
|
_ = rdb.Del(ctx, "task_center:active_tasks").Err()
|
||||||
|
|
||||||
|
// 5. 查找测试任务
|
||||||
|
var inviteTask tcmodel.Task
|
||||||
|
if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id").
|
||||||
|
Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ?", "invite_count", "lifetime").
|
||||||
|
First(&inviteTask).Error; err != nil {
|
||||||
|
return fmt.Errorf("未找到邀请任务: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstOrderTask tcmodel.Task
|
||||||
|
if err := db.Joins("JOIN task_center_task_tiers ON task_center_task_tiers.task_id = task_center_tasks.id").
|
||||||
|
Where("task_center_task_tiers.metric = ? AND task_center_task_tiers.window = ?", "first_order", "lifetime").
|
||||||
|
First(&firstOrderTask).Error; err != nil {
|
||||||
|
return fmt.Errorf("未找到首单任务: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("--- 开始邀请全链路测试 ---\n")
|
||||||
|
fmt.Printf("邀请人: %d, 被邀请人: %d\n", inviterID, inviteeID)
|
||||||
|
|
||||||
|
// 6. 模拟邀请成功事件 (触发两次以确保达到默认阈值 2)
|
||||||
|
fmt.Println("触发 OnInviteSuccess 事件 (第1次)...")
|
||||||
|
if err := taskSvc.OnInviteSuccess(ctx, inviterID, inviteeID); err != nil {
|
||||||
|
return fmt.Errorf("OnInviteSuccess 失败: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("触发 OnInviteSuccess 事件 (第2次, 换个用户ID)...")
|
||||||
|
if err := taskSvc.OnInviteSuccess(ctx, inviterID, 9999); err != nil {
|
||||||
|
return fmt.Errorf("OnInviteSuccess 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 模拟被邀请者下单
|
||||||
|
orderNo := fmt.Sprintf("INVITE_ORDER_%d", time.Now().Unix())
|
||||||
|
order := &model.Orders{
|
||||||
|
UserID: inviteeID,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
TotalAmount: 100,
|
||||||
|
ActualAmount: 100,
|
||||||
|
Status: 2, // 已支付
|
||||||
|
PaidAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := db.Omit("cancelled_at").Create(order).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建被邀请者订单失败: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("被邀请者下单成功: %s (ID: %d)\n", orderNo, order.ID)
|
||||||
|
|
||||||
|
fmt.Println("触发 OnOrderPaid 事件 (被邀请者)...")
|
||||||
|
if err := taskSvc.OnOrderPaid(ctx, inviteeID, order.ID); err != nil {
|
||||||
|
return fmt.Errorf("OnOrderPaid 失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 验证
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
fmt.Println("\n--- 数据库进度核查 ---")
|
||||||
|
var allProgress []tcmodel.UserTaskProgress
|
||||||
|
db.Where("user_id IN (?)", []int64{inviterID, inviteeID}).Find(&allProgress)
|
||||||
|
if len(allProgress) == 0 {
|
||||||
|
fmt.Println("⚠️ 数据库中未找到任何进度记录!")
|
||||||
|
}
|
||||||
|
for _, p := range allProgress {
|
||||||
|
userLabel := "邀请人"
|
||||||
|
if p.UserID == inviteeID {
|
||||||
|
userLabel = "被邀请人"
|
||||||
|
}
|
||||||
|
fmt.Printf("[%s] 用户:%d 任务:%d | Invite=%d, OrderCount=%d, FirstOrder=%d\n",
|
||||||
|
userLabel, p.UserID, p.TaskID, p.InviteCount, p.OrderCount, p.FirstOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n--- 奖励发放核查 ---")
|
||||||
|
var logs []tcmodel.TaskEventLog
|
||||||
|
db.Where("user_id IN (?) AND status = ?", []int64{inviterID, inviteeID}, "granted").Find(&logs)
|
||||||
|
fmt.Printf("✅ 累计发放奖励次数: %d\n", len(logs))
|
||||||
|
for _, l := range logs {
|
||||||
|
fmt.Printf(" - 用户 %d 触发任务 %d 奖励 | Source:%s\n", l.UserID, l.TaskID, l.SourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logs) >= 2 {
|
||||||
|
fmt.Println("\n🎉 邀请全链路集成测试通过!邀请人和被邀请人都获得了奖励。")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n⚠️ 测试部分完成,奖励次数(%d)少于预期(2)\n", len(logs))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟创建用户的方法(如果不存在)
|
||||||
|
func ensureUserExists(repo mysql.Repo, userID int64, nickname string) error {
|
||||||
|
db := repo.GetDbW()
|
||||||
|
var user model.Users
|
||||||
|
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||||
|
user = model.Users{
|
||||||
|
ID: userID,
|
||||||
|
Nickname: nickname,
|
||||||
|
Avatar: "http://example.com/a.png",
|
||||||
|
Status: 1,
|
||||||
|
InviteCode: fmt.Sprintf("CODE%d", userID),
|
||||||
|
}
|
||||||
|
if err := db.Create(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("已确保测试用户存在: %d (%s)\n", userID, nickname)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立邀请关系
|
||||||
|
func ensureInviteRelationship(repo mysql.Repo, inviterID, inviteeID int64) error {
|
||||||
|
db := repo.GetDbW()
|
||||||
|
var rel model.UserInvites
|
||||||
|
if err := db.Where("invitee_id = ?", inviteeID).First(&rel).Error; err != nil {
|
||||||
|
rel = model.UserInvites{
|
||||||
|
InviterID: inviterID,
|
||||||
|
InviteeID: inviteeID,
|
||||||
|
InviteCode: fmt.Sprintf("CODE%d", inviterID),
|
||||||
|
}
|
||||||
|
return db.Omit("rewarded_at").Create(&rel).Error
|
||||||
|
}
|
||||||
|
// 如果已存在但邀请人不对,修正它
|
||||||
|
if rel.InviterID != inviterID {
|
||||||
|
return db.Model(&rel).Update("inviter_id", inviterID).Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
477
cmd/tools/task_center_test/main.go
Normal file
477
cmd/tools/task_center_test/main.go
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
// 任务中心配置组合测试工具
|
||||||
|
// 功能:
|
||||||
|
// 1. 生成所有有效的任务配置组合到 MySQL 数据库
|
||||||
|
// 2. 模拟用户任务进度
|
||||||
|
// 3. 验证任务功能是否正常
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
|
"bindbox-game/internal/repository/mysql"
|
||||||
|
tcmodel "bindbox-game/internal/repository/mysql/task_center"
|
||||||
|
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 常量定义
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 任务指标
|
||||||
|
MetricFirstOrder = "first_order"
|
||||||
|
MetricOrderCount = "order_count"
|
||||||
|
MetricOrderAmount = "order_amount"
|
||||||
|
MetricInviteCount = "invite_count"
|
||||||
|
|
||||||
|
// 操作符
|
||||||
|
OperatorGTE = ">="
|
||||||
|
OperatorEQ = "="
|
||||||
|
|
||||||
|
// 时间窗口
|
||||||
|
WindowDaily = "daily"
|
||||||
|
WindowWeekly = "weekly"
|
||||||
|
WindowMonthly = "monthly"
|
||||||
|
WindowLifetime = "lifetime"
|
||||||
|
|
||||||
|
// 奖励类型
|
||||||
|
RewardTypePoints = "points"
|
||||||
|
RewardTypeCoupon = "coupon"
|
||||||
|
RewardTypeItemCard = "item_card"
|
||||||
|
RewardTypeTitle = "title"
|
||||||
|
RewardTypeGameTicket = "game_ticket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskCombination 表示一种任务配置组合
|
||||||
|
type TaskCombination struct {
|
||||||
|
Name string
|
||||||
|
Metric string
|
||||||
|
Operator string
|
||||||
|
Threshold int64
|
||||||
|
Window string
|
||||||
|
RewardType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResult 测试结果
|
||||||
|
type TestResult struct {
|
||||||
|
Name string
|
||||||
|
Passed bool
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 配置组合生成器
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// GenerateAllCombinations 生成所有有效的任务配置组合
|
||||||
|
func GenerateAllCombinations() []TaskCombination {
|
||||||
|
metrics := []struct {
|
||||||
|
name string
|
||||||
|
operators []string
|
||||||
|
threshold int64
|
||||||
|
}{
|
||||||
|
{MetricFirstOrder, []string{OperatorEQ}, 1},
|
||||||
|
{MetricOrderCount, []string{OperatorGTE, OperatorEQ}, 3},
|
||||||
|
{MetricOrderAmount, []string{OperatorGTE, OperatorEQ}, 10000},
|
||||||
|
{MetricInviteCount, []string{OperatorGTE, OperatorEQ}, 2},
|
||||||
|
}
|
||||||
|
windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime}
|
||||||
|
rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket}
|
||||||
|
|
||||||
|
var combinations []TaskCombination
|
||||||
|
idx := 0
|
||||||
|
for _, m := range metrics {
|
||||||
|
for _, op := range m.operators {
|
||||||
|
for _, w := range windows {
|
||||||
|
for _, r := range rewards {
|
||||||
|
idx++
|
||||||
|
combinations = append(combinations, TaskCombination{
|
||||||
|
Name: fmt.Sprintf("测试任务%03d_%s_%s_%s", idx, m.name, w, r),
|
||||||
|
Metric: m.name,
|
||||||
|
Operator: op,
|
||||||
|
Threshold: m.threshold,
|
||||||
|
Window: w,
|
||||||
|
RewardType: r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combinations
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRewardPayload 根据奖励类型生成对应的 JSON payload
|
||||||
|
func generateRewardPayload(rewardType string) string {
|
||||||
|
switch rewardType {
|
||||||
|
case RewardTypePoints:
|
||||||
|
return `{"points": 100}`
|
||||||
|
case RewardTypeCoupon:
|
||||||
|
return `{"coupon_id": 1, "quantity": 1}`
|
||||||
|
case RewardTypeItemCard:
|
||||||
|
return `{"card_id": 1, "quantity": 1}`
|
||||||
|
case RewardTypeTitle:
|
||||||
|
return `{"title_id": 1}`
|
||||||
|
case RewardTypeGameTicket:
|
||||||
|
return `{"game_code": "minesweeper", "amount": 5}`
|
||||||
|
default:
|
||||||
|
return `{}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 数据库操作
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// SeedAllCombinations 将所有配置组合写入数据库
|
||||||
|
func SeedAllCombinations(repo mysql.Repo, dryRun bool) error {
|
||||||
|
db := repo.GetDbW()
|
||||||
|
combos := GenerateAllCombinations()
|
||||||
|
|
||||||
|
fmt.Printf("准备生成 %d 个任务配置组合\n", len(combos))
|
||||||
|
if dryRun {
|
||||||
|
fmt.Println("【试运行模式】不会实际写入数据库")
|
||||||
|
for i, c := range combos {
|
||||||
|
fmt.Printf(" %3d. %s (指标=%s, 操作符=%s, 窗口=%s, 奖励=%s)\n",
|
||||||
|
i+1, c.Name, c.Metric, c.Operator, c.Window, c.RewardType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 清理旧的测试数据
|
||||||
|
if err := tx.Where("name LIKE ?", "测试任务%").Delete(&tcmodel.Task{}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("清理旧任务失败: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("已清理旧的测试任务数据")
|
||||||
|
|
||||||
|
created := 0
|
||||||
|
for _, combo := range combos {
|
||||||
|
// 检查是否已存在
|
||||||
|
var exists tcmodel.Task
|
||||||
|
if err := tx.Where("name = ?", combo.Name).First(&exists).Error; err == nil {
|
||||||
|
fmt.Printf(" 跳过: %s (已存在)\n", combo.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入任务
|
||||||
|
task := &tcmodel.Task{
|
||||||
|
Name: combo.Name,
|
||||||
|
Description: fmt.Sprintf("测试 %s + %s + %s + %s", combo.Metric, combo.Operator, combo.Window, combo.RewardType),
|
||||||
|
Status: 1,
|
||||||
|
Visibility: 1,
|
||||||
|
}
|
||||||
|
if err := tx.Create(task).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("插入任务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入档位
|
||||||
|
tier := &tcmodel.TaskTier{
|
||||||
|
TaskID: task.ID,
|
||||||
|
Metric: combo.Metric,
|
||||||
|
Operator: combo.Operator,
|
||||||
|
Threshold: combo.Threshold,
|
||||||
|
Window: combo.Window,
|
||||||
|
Priority: 0,
|
||||||
|
}
|
||||||
|
if err := tx.Create(tier).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("插入档位失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入奖励
|
||||||
|
payload := generateRewardPayload(combo.RewardType)
|
||||||
|
reward := &tcmodel.TaskReward{
|
||||||
|
TaskID: task.ID,
|
||||||
|
TierID: tier.ID,
|
||||||
|
RewardType: combo.RewardType,
|
||||||
|
RewardPayload: datatypes.JSON(payload),
|
||||||
|
Quantity: 10,
|
||||||
|
}
|
||||||
|
if err := tx.Create(reward).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("插入奖励失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
created++
|
||||||
|
if created%10 == 0 {
|
||||||
|
fmt.Printf(" 已创建 %d 个任务...\n", created)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return fmt.Errorf("提交事务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ 成功创建 %d 个任务配置组合\n", created)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 模拟用户任务
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// SimulateUserTask 模拟用户完成任务
|
||||||
|
func SimulateUserTask(repo mysql.Repo, userID int64, taskID int64) error {
|
||||||
|
db := repo.GetDbW()
|
||||||
|
|
||||||
|
// 查询任务和档位
|
||||||
|
var task tcmodel.Task
|
||||||
|
if err := db.Where("id = ?", taskID).First(&task).Error; err != nil {
|
||||||
|
return fmt.Errorf("任务不存在: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tier tcmodel.TaskTier
|
||||||
|
if err := db.Where("task_id = ?", taskID).First(&tier).Error; err != nil {
|
||||||
|
return fmt.Errorf("档位不存在: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("模拟任务: %s (指标=%s, 阈值=%d)\n", task.Name, tier.Metric, tier.Threshold)
|
||||||
|
|
||||||
|
// 创建或更新用户进度
|
||||||
|
progress := &tcmodel.UserTaskProgress{
|
||||||
|
UserID: userID,
|
||||||
|
TaskID: taskID,
|
||||||
|
ClaimedTiers: datatypes.JSON("[]"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据指标类型设置进度
|
||||||
|
switch tier.Metric {
|
||||||
|
case MetricFirstOrder:
|
||||||
|
progress.FirstOrder = 1
|
||||||
|
progress.OrderCount = 1
|
||||||
|
progress.OrderAmount = 10000
|
||||||
|
case MetricOrderCount:
|
||||||
|
progress.OrderCount = tier.Threshold
|
||||||
|
case MetricOrderAmount:
|
||||||
|
progress.OrderAmount = tier.Threshold
|
||||||
|
progress.OrderCount = 1
|
||||||
|
case MetricInviteCount:
|
||||||
|
progress.InviteCount = tier.Threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert
|
||||||
|
if err := db.Where("user_id = ? AND task_id = ?", userID, taskID).
|
||||||
|
Assign(progress).
|
||||||
|
FirstOrCreate(progress).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建进度失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ 用户 %d 的任务进度已更新: order_count=%d, order_amount=%d, invite_count=%d, first_order=%d\n",
|
||||||
|
userID, progress.OrderCount, progress.OrderAmount, progress.InviteCount, progress.FirstOrder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 验证功能
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// VerifyAllConfigs 验证所有配置是否正确
|
||||||
|
func VerifyAllConfigs(repo mysql.Repo) []TestResult {
|
||||||
|
db := repo.GetDbR()
|
||||||
|
var results []TestResult
|
||||||
|
|
||||||
|
// 1. 检查任务数量
|
||||||
|
var taskCount int64
|
||||||
|
var sampleTasks []tcmodel.Task
|
||||||
|
db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Count(&taskCount)
|
||||||
|
db.Model(&tcmodel.Task{}).Where("name LIKE ?", "测试任务%").Limit(5).Find(&sampleTasks)
|
||||||
|
|
||||||
|
var sampleMsg string
|
||||||
|
for _, t := range sampleTasks {
|
||||||
|
sampleMsg += fmt.Sprintf("[%d:%s] ", t.ID, t.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, TestResult{
|
||||||
|
Name: "任务数量检查",
|
||||||
|
Passed: taskCount > 0,
|
||||||
|
Message: fmt.Sprintf("找到 %d 个测试任务. 样本: %s", taskCount, sampleMsg),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 检查每种指标的覆盖
|
||||||
|
metrics := []string{MetricFirstOrder, MetricOrderCount, MetricOrderAmount, MetricInviteCount}
|
||||||
|
for _, m := range metrics {
|
||||||
|
var count int64
|
||||||
|
db.Model(&tcmodel.TaskTier{}).Where("metric = ?", m).Count(&count)
|
||||||
|
results = append(results, TestResult{
|
||||||
|
Name: fmt.Sprintf("指标覆盖: %s", m),
|
||||||
|
Passed: count > 0,
|
||||||
|
Message: fmt.Sprintf("找到 %d 个档位使用此指标", count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查每种时间窗口的覆盖
|
||||||
|
windows := []string{WindowDaily, WindowWeekly, WindowMonthly, WindowLifetime}
|
||||||
|
for _, w := range windows {
|
||||||
|
var count int64
|
||||||
|
db.Model(&tcmodel.TaskTier{}).Where("window = ?", w).Count(&count)
|
||||||
|
results = append(results, TestResult{
|
||||||
|
Name: fmt.Sprintf("时间窗口覆盖: %s", w),
|
||||||
|
Passed: count > 0,
|
||||||
|
Message: fmt.Sprintf("找到 %d 个档位使用此时间窗口", count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查每种奖励类型的覆盖
|
||||||
|
rewards := []string{RewardTypePoints, RewardTypeCoupon, RewardTypeItemCard, RewardTypeTitle, RewardTypeGameTicket}
|
||||||
|
for _, r := range rewards {
|
||||||
|
var count int64
|
||||||
|
db.Model(&tcmodel.TaskReward{}).Where("reward_type = ?", r).Count(&count)
|
||||||
|
results = append(results, TestResult{
|
||||||
|
Name: fmt.Sprintf("奖励类型覆盖: %s", r),
|
||||||
|
Passed: count > 0,
|
||||||
|
Message: fmt.Sprintf("找到 %d 个奖励使用此类型", count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 检查奖励 payload 格式
|
||||||
|
var rewardList []tcmodel.TaskReward
|
||||||
|
db.Limit(20).Find(&rewardList)
|
||||||
|
for _, r := range rewardList {
|
||||||
|
var data map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(r.RewardPayload), &data)
|
||||||
|
passed := err == nil
|
||||||
|
msg := "JSON 格式正确"
|
||||||
|
if err != nil {
|
||||||
|
msg = fmt.Sprintf("JSON 解析失败: %v", err)
|
||||||
|
}
|
||||||
|
results = append(results, TestResult{
|
||||||
|
Name: fmt.Sprintf("奖励Payload格式: ID=%d, Type=%s", r.ID, r.RewardType),
|
||||||
|
Passed: passed,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintResults 打印测试结果
|
||||||
|
func PrintResults(results []TestResult) {
|
||||||
|
passed := 0
|
||||||
|
failed := 0
|
||||||
|
|
||||||
|
fmt.Println("\n========== 测试结果 ==========")
|
||||||
|
for _, r := range results {
|
||||||
|
status := "✅ PASS"
|
||||||
|
if !r.Passed {
|
||||||
|
status = "❌ FAIL"
|
||||||
|
failed++
|
||||||
|
} else {
|
||||||
|
passed++
|
||||||
|
}
|
||||||
|
fmt.Printf("%s | %s | %s\n", status, r.Name, r.Message)
|
||||||
|
}
|
||||||
|
fmt.Println("==============================")
|
||||||
|
fmt.Printf("总计: %d 通过, %d 失败\n", passed, failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// 主程序
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 命令行参数
|
||||||
|
action := flag.String("action", "help", "操作类型: seed/simulate/verify/integration/invite-test/help")
|
||||||
|
dryRun := flag.Bool("dry-run", false, "试运行模式,不实际写入数据库")
|
||||||
|
userID := flag.Int64("user", 8888, "用户ID (用于 simulate 或 integration)")
|
||||||
|
taskID := flag.Int64("task", 0, "任务ID")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 显示帮助
|
||||||
|
if *action == "help" {
|
||||||
|
fmt.Println(`
|
||||||
|
任务中心配置组合测试工具
|
||||||
|
|
||||||
|
用法:
|
||||||
|
go run main.go -action=<操作>
|
||||||
|
|
||||||
|
操作类型:
|
||||||
|
seed - 生成所有配置组合到数据库
|
||||||
|
simulate - 简单模拟用户进度 (仅修改进度表)
|
||||||
|
integration - 真实集成测试 (触发 OnOrderPaid, 验证全流程)
|
||||||
|
invite-test - 邀请全链路测试 (模拟邀请、下单、双端奖励发放)
|
||||||
|
verify - 验证配置是否正确
|
||||||
|
|
||||||
|
参数:
|
||||||
|
-dry-run - 试运行模式,不实际写入数据库
|
||||||
|
-user - 用户ID (默认: 8888)
|
||||||
|
-task - 任务ID
|
||||||
|
|
||||||
|
示例:
|
||||||
|
# 邀请全链路测试
|
||||||
|
go run main.go -action=invite-test
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据库连接
|
||||||
|
repo, err := mysql.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("连接数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := configs.Get()
|
||||||
|
fmt.Printf("已连接到数据库: %s\n", cfg.MySQL.Write.Name)
|
||||||
|
fmt.Printf("时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
// 执行操作
|
||||||
|
switch *action {
|
||||||
|
case "seed":
|
||||||
|
if err := SeedAllCombinations(repo, *dryRun); err != nil {
|
||||||
|
log.Printf("生成配置失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "simulate":
|
||||||
|
if *taskID == 0 {
|
||||||
|
fmt.Println("请指定任务ID: -task=<ID>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := SimulateUserTask(repo, *userID, *taskID); err != nil {
|
||||||
|
log.Printf("模拟失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "integration":
|
||||||
|
// 确保用户存在
|
||||||
|
if err := ensureUserExists(repo, *userID, "测试用户"); err != nil {
|
||||||
|
log.Printf("预检用户失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := IntegrationTest(repo); err != nil {
|
||||||
|
log.Printf("集成测试失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "invite-test":
|
||||||
|
if err := InviteAndTaskIntegrationTest(repo); err != nil {
|
||||||
|
log.Printf("邀请测试失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "verify":
|
||||||
|
results := VerifyAllConfigs(repo)
|
||||||
|
PrintResults(results)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("未知操作: %s\n", *action)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -89,11 +89,6 @@ type Config struct {
|
|||||||
AppSecret string `mapstructure:"app_secret" toml:"app_secret"`
|
AppSecret string `mapstructure:"app_secret" toml:"app_secret"`
|
||||||
NotifyURL string `mapstructure:"notify_url" toml:"notify_url"`
|
NotifyURL string `mapstructure:"notify_url" toml:"notify_url"`
|
||||||
} `mapstructure:"douyin" toml:"douyin"`
|
} `mapstructure:"douyin" toml:"douyin"`
|
||||||
|
|
||||||
Otel struct {
|
|
||||||
Enabled bool `mapstructure:"enabled" toml:"enabled"`
|
|
||||||
Endpoint string `mapstructure:"endpoint" toml:"endpoint"`
|
|
||||||
} `mapstructure:"otel" toml:"otel"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -110,7 +105,7 @@ var (
|
|||||||
proConfigs []byte
|
proConfigs []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func init() {
|
||||||
var r io.Reader
|
var r io.Reader
|
||||||
|
|
||||||
switch env.Active().Value() {
|
switch env.Active().Value() {
|
||||||
@ -171,38 +166,6 @@ func Init() {
|
|||||||
if v := os.Getenv("ALIYUN_SMS_TEMPLATE_CODE"); v != "" {
|
if v := os.Getenv("ALIYUN_SMS_TEMPLATE_CODE"); v != "" {
|
||||||
config.AliyunSMS.TemplateCode = v
|
config.AliyunSMS.TemplateCode = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// MySQL 配置环境变量覆盖
|
|
||||||
if v := os.Getenv("MYSQL_ADDR"); v != "" {
|
|
||||||
config.MySQL.Read.Addr = v
|
|
||||||
config.MySQL.Write.Addr = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("MYSQL_READ_ADDR"); v != "" {
|
|
||||||
config.MySQL.Read.Addr = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("MYSQL_WRITE_ADDR"); v != "" {
|
|
||||||
config.MySQL.Write.Addr = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("MYSQL_USER"); v != "" {
|
|
||||||
config.MySQL.Read.User = v
|
|
||||||
config.MySQL.Write.User = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("MYSQL_PASS"); v != "" {
|
|
||||||
config.MySQL.Read.Pass = v
|
|
||||||
config.MySQL.Write.Pass = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("MYSQL_NAME"); v != "" {
|
|
||||||
config.MySQL.Read.Name = v
|
|
||||||
config.MySQL.Write.Name = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redis 配置环境变量覆盖
|
|
||||||
if v := os.Getenv("REDIS_ADDR"); v != "" {
|
|
||||||
config.Redis.Addr = v
|
|
||||||
}
|
|
||||||
if v := os.Getenv("REDIS_PASS"); v != "" {
|
|
||||||
config.Redis.Pass = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get() Config {
|
func Get() Config {
|
||||||
|
|||||||
@ -2,28 +2,31 @@
|
|||||||
local = 'zh-cn'
|
local = 'zh-cn'
|
||||||
|
|
||||||
[mysql.read]
|
[mysql.read]
|
||||||
addr = '150.158.78.154:3306'
|
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||||
name = 'dev_game'
|
name = 'bindbox_game'
|
||||||
pass = 'bindbox2025kdy'
|
pass = 'api2api..'
|
||||||
user = 'root'
|
|
||||||
|
|
||||||
[mysql.write]
|
|
||||||
addr = '150.158.78.154:3306'
|
|
||||||
name = 'dev_game'
|
|
||||||
pass = 'bindbox2025kdy'
|
|
||||||
user = 'root'
|
user = 'root'
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
addr = "127.0.0.1:6379"
|
addr = "118.25.13.43:8379"
|
||||||
pass = ""
|
pass = "xbm#2023by1024"
|
||||||
db = 5
|
db = 5
|
||||||
|
|
||||||
|
|
||||||
|
[mysql.write]
|
||||||
|
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||||
|
name = 'bindbox_game'
|
||||||
|
pass = 'api2api..'
|
||||||
|
user = 'root'
|
||||||
|
|
||||||
[jwt]
|
[jwt]
|
||||||
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
||||||
patient_secret = "AppUserJwtSecret2025"
|
patient_secret = "AppUserJwtSecret2025"
|
||||||
|
|
||||||
|
[wechat]
|
||||||
|
app_id = "wx26ad074017e1e63f"
|
||||||
|
app_secret = "026c19ce4f3bb090c56573024c59a8be"
|
||||||
|
lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI"
|
||||||
|
|
||||||
[cos]
|
[cos]
|
||||||
bucket = "keaiya-1259195914"
|
bucket = "keaiya-1259195914"
|
||||||
@ -37,13 +40,13 @@ base_url = ""
|
|||||||
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
|
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
|
||||||
|
|
||||||
[wechatpay]
|
[wechatpay]
|
||||||
mchid = ""
|
mchid = "1610439635"
|
||||||
serial_no = ""
|
serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03"
|
||||||
private_key_path = ""
|
private_key_path = "./configs/cert/apiclient_key.pem"
|
||||||
api_v3_key = ""
|
api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05"
|
||||||
notify_url = ""
|
notify_url = "https://mini-chat.1024tool.vip/api/pay/wechat/notify"
|
||||||
public_key_id = ""
|
public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600"
|
||||||
public_key_path = ""
|
public_key_path = "./configs/cert/pub_key.pem"
|
||||||
|
|
||||||
[aliyun_sms]
|
[aliyun_sms]
|
||||||
access_key_id = ""
|
access_key_id = ""
|
||||||
@ -51,8 +54,5 @@ access_key_secret = ""
|
|||||||
sign_name = ""
|
sign_name = ""
|
||||||
template_code = ""
|
template_code = ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[internal]
|
[internal]
|
||||||
api_key = "bindbox-internal-secret-2024"
|
api_key = "bindbox-internal-secret-2024"
|
||||||
|
|||||||
@ -2,75 +2,54 @@
|
|||||||
local = 'zh-cn'
|
local = 'zh-cn'
|
||||||
|
|
||||||
[mysql.read]
|
[mysql.read]
|
||||||
<<<<<<< Updated upstream
|
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||||
addr = "mysql:3306"
|
name = 'bindbox_game'
|
||||||
user = "root"
|
pass = 'api2api..'
|
||||||
pass = "bindbox2025kdy"
|
user = 'root'
|
||||||
name = "bindbox_game"
|
|
||||||
|
|
||||||
[mysql.write]
|
|
||||||
addr = "mysql:3306"
|
|
||||||
user = "root"
|
|
||||||
pass = "bindbox2025kdy"
|
|
||||||
name = "bindbox_game"
|
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
addr = "redis:6379"
|
addr = "118.25.13.43:8379"
|
||||||
pass = ""
|
pass = "xbm#2023by1024"
|
||||||
db = 0
|
db = 5
|
||||||
|
|
||||||
|
|
||||||
|
[mysql.write]
|
||||||
|
addr = 'sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555'
|
||||||
|
name = 'bindbox_game'
|
||||||
|
pass = 'api2api..'
|
||||||
|
user = 'root'
|
||||||
|
|
||||||
[jwt]
|
[jwt]
|
||||||
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
||||||
patient_secret = "AppUserJwtSecret2025"
|
patient_secret = "AppUserJwtSecret2025"
|
||||||
|
|
||||||
[wechat]
|
[wechat]
|
||||||
app_id = ""
|
app_id = "wx26ad074017e1e63f"
|
||||||
app_secret = ""
|
app_secret = "026c19ce4f3bb090c56573024c59a8be"
|
||||||
lottery_result_template_id = ""
|
lottery_result_template_id = "O2eqJQD3pn-vQ6g2z9DWzINVwOmPoz8yW-172J_YcpI"
|
||||||
|
|
||||||
[cos]
|
[cos]
|
||||||
bucket = "keaiya-1259195914"
|
bucket = "keaiya-1259195914"
|
||||||
region = "ap-shanghai"
|
region = "ap-shanghai"
|
||||||
secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
|
secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
|
||||||
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
|
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
|
||||||
|
# 可选:如有 CDN/自定义域名则填写,否则留空
|
||||||
base_url = ""
|
base_url = ""
|
||||||
|
|
||||||
[random]
|
[random]
|
||||||
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
|
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
|
||||||
|
|
||||||
[wechatpay]
|
[wechatpay]
|
||||||
mchid = ""
|
mchid = "1610439635"
|
||||||
serial_no = ""
|
serial_no = "3AFD505D597831F8E931EBFFEEB5976B81F66F03"
|
||||||
private_key_path = ""
|
private_key_path = "./configs/cert/apiclient_key.pem"
|
||||||
api_v3_key = ""
|
api_v3_key = "3tbwEFZV3fZtOslpUJC7Sacb8qjzhm05"
|
||||||
notify_url = ""
|
notify_url = "https://mini-chat.1024tool.vip/api/pay/wechat/notify"
|
||||||
public_key_id = ""
|
public_key_id = "PUB_KEY_ID_0116104396352025041000211519001600"
|
||||||
public_key_path = ""
|
public_key_path = "./configs/cert/pub_key.pem"
|
||||||
|
|
||||||
[aliyun_sms]
|
[aliyun_sms]
|
||||||
access_key_id = ""
|
access_key_id = "LTAI5tJ55hp81F5HDa2oSYb3"
|
||||||
access_key_secret = ""
|
access_key_secret = "cUd3Ym73i7OKsDDBJre5IAkpwwTiLs"
|
||||||
sign_name = ""
|
sign_name = "沙琪玛上海信息技术"
|
||||||
template_code = ""
|
template_code = "SMS_499200896"
|
||||||
|
|
||||||
[internal]
|
|
||||||
api_key = "bindbox-internal-secret-2024"
|
|
||||||
|
|
||||||
[otel]
|
|
||||||
enabled = true
|
|
||||||
endpoint = "tempo:4318"
|
|
||||||
=======
|
|
||||||
addr = '127.0.0.1:3306'
|
|
||||||
name = 'mini_chat'
|
|
||||||
pass = 'zRXiHTHzRHTdShFZ'
|
|
||||||
user = 'mini_chat'
|
|
||||||
|
|
||||||
[mysql.write]
|
|
||||||
addr = '127.0.0.1:3306'
|
|
||||||
name = 'mini_chat'
|
|
||||||
pass = 'zRXiHTHzRHTdShFZ'
|
|
||||||
user = 'mini_chat'
|
|
||||||
|
|
||||||
[jwt]
|
|
||||||
admin_secret = "X9Rm9ycFr66w9syuYTWCc"
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
[language]
|
[mysql]
|
||||||
local = 'zh-cn'
|
|
||||||
|
|
||||||
[mysql.read]
|
[mysql.read]
|
||||||
addr = "mysql:3306"
|
addr = "127.0.0.1:3306"
|
||||||
user = "root"
|
user = "root"
|
||||||
pass = "bindbox2025kdy"
|
pass = "123456"
|
||||||
name = "bindbox_game"
|
name = "bindbox_game"
|
||||||
|
|
||||||
[mysql.write]
|
[mysql.write]
|
||||||
addr = "mysql:3306"
|
addr = "127.0.0.1:3306"
|
||||||
user = "root"
|
user = "root"
|
||||||
pass = "bindbox2025kdy"
|
pass = "123456"
|
||||||
name = "bindbox_game"
|
name = "bindbox_game"
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
addr = "redis:6379"
|
addr = "127.0.0.1:6379"
|
||||||
pass = ""
|
pass = ""
|
||||||
db = 0
|
db = 0
|
||||||
|
|
||||||
@ -22,39 +19,24 @@ db = 0
|
|||||||
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
admin_secret = "m9ycX9RTPyuYTWw9FrCc"
|
||||||
patient_secret = "AppUserJwtSecret2025"
|
patient_secret = "AppUserJwtSecret2025"
|
||||||
|
|
||||||
|
[random]
|
||||||
|
|
||||||
|
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||||
|
|
||||||
[wechat]
|
[wechat]
|
||||||
app_id = ""
|
app_id = ""
|
||||||
app_secret = ""
|
app_secret = ""
|
||||||
lottery_result_template_id = ""
|
lottery_result_template_id = ""
|
||||||
|
|
||||||
[cos]
|
|
||||||
bucket = "keaiya-1259195914"
|
|
||||||
region = "ap-shanghai"
|
|
||||||
secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
|
|
||||||
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
|
|
||||||
base_url = ""
|
|
||||||
|
|
||||||
[random]
|
|
||||||
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"
|
|
||||||
|
|
||||||
[wechatpay]
|
[wechatpay]
|
||||||
mchid = ""
|
mchid = ""
|
||||||
serial_no = ""
|
serial_no = ""
|
||||||
private_key_path = ""
|
private_key_path = ""
|
||||||
api_v3_key = ""
|
api_v3_key = ""
|
||||||
notify_url = ""
|
notify_url = "https://example.com/api/pay/wechat/notify"
|
||||||
public_key_id = ""
|
|
||||||
public_key_path = ""
|
|
||||||
|
|
||||||
[aliyun_sms]
|
[aliyun_sms]
|
||||||
access_key_id = ""
|
access_key_id = ""
|
||||||
access_key_secret = ""
|
access_key_secret = ""
|
||||||
sign_name = ""
|
sign_name = ""
|
||||||
template_code = ""
|
template_code = ""
|
||||||
|
|
||||||
[internal]
|
|
||||||
api_key = "bindbox-internal-secret-2024"
|
|
||||||
|
|
||||||
[otel]
|
|
||||||
enabled = true
|
|
||||||
endpoint = "tempo:4318"
|
|
||||||
45
go.mod
45
go.mod
@ -1,8 +1,6 @@
|
|||||||
module bindbox-game
|
module bindbox-game
|
||||||
|
|
||||||
go 1.24.0
|
go 1.19
|
||||||
|
|
||||||
toolchain go1.24.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
@ -20,7 +18,6 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.15.0
|
github.com/go-playground/validator/v10 v10.15.0
|
||||||
github.com/go-resty/resty/v2 v2.10.0
|
github.com/go-resty/resty/v2 v2.10.0
|
||||||
github.com/go-sql-driver/mysql v1.7.0
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/issue9/identicon/v2 v2.1.2
|
github.com/issue9/identicon/v2 v2.1.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@ -29,22 +26,17 @@ require (
|
|||||||
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644
|
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644
|
||||||
github.com/spf13/cast v1.5.1
|
github.com/spf13/cast v1.5.1
|
||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.2
|
github.com/swaggo/swag v1.16.2
|
||||||
github.com/tealeg/xlsx v1.0.5
|
github.com/tealeg/xlsx v1.0.5
|
||||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.37
|
github.com/tencentyun/cos-go-sdk-v5 v0.7.37
|
||||||
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
|
||||||
go.opentelemetry.io/otel v1.39.0
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0
|
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0
|
|
||||||
go.opentelemetry.io/otel/trace v1.39.0
|
|
||||||
go.uber.org/multierr v1.10.0
|
go.uber.org/multierr v1.10.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.44.0
|
golang.org/x/crypto v0.27.0
|
||||||
golang.org/x/tools v0.38.0
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c
|
gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c
|
||||||
gorm.io/driver/mysql v1.5.2
|
gorm.io/driver/mysql v1.5.2
|
||||||
@ -63,11 +55,9 @@ require (
|
|||||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||||
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
|
||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||||
github.com/alicebob/miniredis/v2 v2.36.1 // indirect
|
|
||||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clbanning/mxj v1.8.4 // indirect
|
github.com/clbanning/mxj v1.8.4 // indirect
|
||||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
@ -77,18 +67,14 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
github.com/go-openapi/spec v0.20.4 // indirect
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
github.com/go-openapi/swag v0.19.15 // indirect
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
|
||||||
github.com/google/go-querystring v1.0.0 // indirect
|
github.com/google/go-querystring v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
@ -121,21 +107,14 @@ require (
|
|||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
|
||||||
golang.org/x/arch v0.4.0 // indirect
|
golang.org/x/arch v0.4.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
111
go.sum
111
go.sum
@ -96,8 +96,6 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/
|
|||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||||
github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI=
|
|
||||||
github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
|
||||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||||
@ -106,9 +104,7 @@ github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQ
|
|||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
|
||||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
@ -116,8 +112,6 @@ github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
|
|||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
@ -154,13 +148,11 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
|
||||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
@ -171,11 +163,6 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
@ -188,7 +175,6 @@ github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyr
|
|||||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
@ -206,14 +192,10 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
|
|||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -240,8 +222,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@ -254,9 +236,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -275,16 +256,13 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
|||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/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/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.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/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/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.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@ -292,25 +270,16 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
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/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/assert/v4 v4.1.1 h1:OhPE8SB8n/qZCNGLQa+6MQtr/B3oON0JAVj68k8jJlc=
|
||||||
github.com/issue9/assert/v4 v4.1.1/go.mod h1:v7qDRXi7AsaZZNh8eAK2rkLJg5/clztqQGA1DRv9Lv4=
|
|
||||||
github.com/issue9/identicon/v2 v2.1.2 h1:tu+4vveoiJNXfmWYvl1pDcZSAHCG37+lsoEc2UfCzkI=
|
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/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/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
|
||||||
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||||
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
|
||||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||||
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
|
||||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
|
||||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
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/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
@ -336,7 +305,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@ -362,7 +330,6 @@ 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 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
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/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
||||||
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
|
|
||||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
@ -400,8 +367,7 @@ github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0
|
|||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
|
||||||
github.com/rs/cors v1.8.1 h1:OrP+y5H+5Md29ACTA9imbALaKHwOSUZkcizaG0LT5ow=
|
github.com/rs/cors v1.8.1 h1:OrP+y5H+5Md29ACTA9imbALaKHwOSUZkcizaG0LT5ow=
|
||||||
github.com/rs/cors v1.8.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.8.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644 h1:BBwREPixt0iE77C9z7DOenoeh5OGFrzyL1cWOp5oQTs=
|
github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644 h1:BBwREPixt0iE77C9z7DOenoeh5OGFrzyL1cWOp5oQTs=
|
||||||
@ -436,9 +402,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
@ -470,34 +435,13 @@ github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
@ -523,8 +467,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -564,9 +508,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
|||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -612,8 +555,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
|||||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -638,8 +581,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -692,8 +635,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@ -720,8 +663,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@ -779,15 +722,12 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
@ -850,10 +790,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
|||||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
@ -870,8 +806,6 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
|||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
@ -883,9 +817,10 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
|||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@ -913,12 +848,10 @@ gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10
|
|||||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||||
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
|
gorm.io/driver/postgres v1.4.5 h1:mTeXTTtHAgnS9PgmhN2YeUbazYpLhUI1doLnw42XUZc=
|
||||||
gorm.io/driver/postgres v1.4.5/go.mod h1:GKNQYSJ14qvWkvPwXljMGehpKrhlDNsqYRr5HnYGncg=
|
|
||||||
gorm.io/driver/sqlite v1.1.6/go.mod h1:W8LmC/6UvVbHKah0+QOC7Ja66EaZXHwUTjgXY8YNWX8=
|
gorm.io/driver/sqlite v1.1.6/go.mod h1:W8LmC/6UvVbHKah0+QOC7Ja66EaZXHwUTjgXY8YNWX8=
|
||||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||||
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
||||||
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
|
|
||||||
gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
|
gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
|
||||||
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
|
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
|
||||||
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||||
|
|||||||
@ -54,7 +54,7 @@ type activityDetailResponse struct {
|
|||||||
PlayType string `json:"play_type"`
|
PlayType string `json:"play_type"`
|
||||||
MinParticipants int64 `json:"min_participants"`
|
MinParticipants int64 `json:"min_participants"`
|
||||||
IntervalMinutes int64 `json:"interval_minutes"`
|
IntervalMinutes int64 `json:"interval_minutes"`
|
||||||
ScheduledTime *time.Time `json:"scheduled_time"`
|
ScheduledTime time.Time `json:"scheduled_time"`
|
||||||
LastSettledAt time.Time `json:"last_settled_at"`
|
LastSettledAt time.Time `json:"last_settled_at"`
|
||||||
RefundCouponID int64 `json:"refund_coupon_id"`
|
RefundCouponID int64 `json:"refund_coupon_id"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
@ -86,16 +86,6 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize <= 0 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
if req.PageSize > 100 {
|
|
||||||
req.PageSize = 100
|
|
||||||
}
|
|
||||||
var isBossPtr *int32
|
var isBossPtr *int32
|
||||||
if req.IsBoss == 0 || req.IsBoss == 1 {
|
if req.IsBoss == 0 || req.IsBoss == 1 {
|
||||||
isBossPtr = &req.IsBoss
|
isBossPtr = &req.IsBoss
|
||||||
@ -190,7 +180,7 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
|||||||
PlayType: item.PlayType,
|
PlayType: item.PlayType,
|
||||||
MinParticipants: item.MinParticipants,
|
MinParticipants: item.MinParticipants,
|
||||||
IntervalMinutes: item.IntervalMinutes,
|
IntervalMinutes: item.IntervalMinutes,
|
||||||
ScheduledTime: &item.ScheduledTime,
|
ScheduledTime: item.ScheduledTime,
|
||||||
LastSettledAt: item.LastSettledAt,
|
LastSettledAt: item.LastSettledAt,
|
||||||
RefundCouponID: item.RefundCouponID,
|
RefundCouponID: item.RefundCouponID,
|
||||||
Image: item.Image,
|
Image: item.Image,
|
||||||
@ -198,13 +188,6 @@ func (h *handler) GetActivityDetail() core.HandlerFunc {
|
|||||||
AllowItemCards: item.AllowItemCards,
|
AllowItemCards: item.AllowItemCards,
|
||||||
AllowCoupons: item.AllowCoupons,
|
AllowCoupons: item.AllowCoupons,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复一番赏:即时模式下,清空 ScheduledTime (设置为 nil) 以绕过前端下单拦截
|
|
||||||
// 如果返回零值时间,前端会解析为很早的时间从而判定已结束,必须明确返回 nil
|
|
||||||
if rsp.PlayType == "ichiban" && rsp.DrawMode == "instant" {
|
|
||||||
rsp.ScheduledTime = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,18 +77,16 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
// 计算5分钟前的时间点
|
||||||
// 计算5分钟前的时间点 (用于延迟显示)
|
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||||
fiveMinutesAgo := now.Add(-5 * time.Minute)
|
|
||||||
// 计算当天零点 (用于仅显示当天数据)
|
|
||||||
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
||||||
|
|
||||||
// [修改] 强制获取当天最新的 100 条数据 (Service 层限制最大 100)
|
// 为了保证过滤后依然有足够数据,我们多取一些
|
||||||
// 忽略前端传入的 Page/PageSize,总是获取第一页的 100 条
|
fetchPageSize := pageSize
|
||||||
fetchPageSize := 100
|
if pageSize < 100 {
|
||||||
fetchPage := 1
|
fetchPageSize = 100 // 至少取100条来过滤
|
||||||
|
}
|
||||||
|
|
||||||
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, fetchPage, fetchPageSize, req.Level)
|
items, total, err := h.activity.ListDrawLogs(ctx.RequestContext(), issueID, page, fetchPageSize, req.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListDrawLogsError, err.Error()))
|
||||||
return
|
return
|
||||||
@ -102,21 +100,10 @@ func (h *handler) ListDrawLogs() core.HandlerFunc {
|
|||||||
|
|
||||||
var filteredItems []*model.ActivityDrawLogs
|
var filteredItems []*model.ActivityDrawLogs
|
||||||
for _, v := range items {
|
for _, v := range items {
|
||||||
// 1. 过滤掉太新的数据 (5分钟延迟)
|
// 恢复 5 分钟过滤逻辑
|
||||||
if v.CreatedAt.After(fiveMinutesAgo) {
|
if v.CreatedAt.After(fiveMinutesAgo) {
|
||||||
continue
|
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 {
|
if len(filteredItems) >= pageSize {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,10 +64,6 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// DEBUG LOG: Print request params to diagnose frontend issue
|
|
||||||
reqBytes, _ := json.Marshal(req)
|
|
||||||
h.logger.Info(fmt.Sprintf("JoinLottery Request Params: UserID=%d Payload=%s", ctx.SessionUserInfo().Id, string(reqBytes)))
|
|
||||||
|
|
||||||
userID := int64(ctx.SessionUserInfo().Id)
|
userID := int64(ctx.SessionUserInfo().Id)
|
||||||
h.logger.Info(fmt.Sprintf("JoinLottery Start: UserID=%d ActivityID=%d IssueID=%d", userID, req.ActivityID, req.IssueID))
|
h.logger.Info(fmt.Sprintf("JoinLottery Start: UserID=%d ActivityID=%d IssueID=%d", userID, req.ActivityID, req.IssueID))
|
||||||
activity, err := h.activity.GetActivity(ctx.RequestContext(), req.ActivityID)
|
activity, err := h.activity.GetActivity(ctx.RequestContext(), req.ActivityID)
|
||||||
@ -136,10 +132,8 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
|
||||||
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
|
||||||
if applied > 0 {
|
|
||||||
order.CouponID = *req.CouponID
|
order.CouponID = *req.CouponID
|
||||||
}
|
applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
|
||||||
}
|
}
|
||||||
// Title Discount Logic
|
// Title Discount Logic
|
||||||
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
// 1. Fetch active effects for this user, scoped to this activity/issue/category
|
||||||
@ -269,13 +263,6 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
deducted += canDeduct
|
deducted += canDeduct
|
||||||
|
|
||||||
// Record usage for remark (Format: gp_use:ID:Count)
|
|
||||||
if order.Remark == "" {
|
|
||||||
order.Remark = fmt.Sprintf("gp_use:%d:%d", p.ID, canDeduct)
|
|
||||||
} else {
|
|
||||||
order.Remark += fmt.Sprintf("|gp_use:%d:%d", p.ID, canDeduct)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if deducted < count {
|
if deducted < count {
|
||||||
@ -286,8 +273,15 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
order.ActualAmount = 0
|
order.ActualAmount = 0
|
||||||
order.SourceType = 4 // Cleanly mark as Game Pass source
|
order.SourceType = 4 // Cleanly mark as Game Pass source
|
||||||
|
|
||||||
// Legacy marker for backward compatibility or simple check
|
// existing lottery logic sets SourceType based on "h.orderModel" which defaults to something?
|
||||||
|
// h.orderModel(..., c) implementation needs to be checked or inferred.
|
||||||
|
// Assuming orderModel sets SourceType based on activity or defaults.
|
||||||
|
// Let's explicitly mark it or rely on Remark.
|
||||||
|
if order.Remark == "" {
|
||||||
|
order.Remark = "use_game_pass"
|
||||||
|
} else {
|
||||||
order.Remark += "|use_game_pass"
|
order.Remark += "|use_game_pass"
|
||||||
|
}
|
||||||
// Note: If we change SourceType to 4, ProcessOrderLottery might skip it if checks SourceType.
|
// Note: If we change SourceType to 4, ProcessOrderLottery might skip it if checks SourceType.
|
||||||
// Lottery app usually expects SourceType=2 or similar.
|
// Lottery app usually expects SourceType=2 or similar.
|
||||||
// Let's KEEP SourceType as is (likely 2 for ichiban), but Amount=0 ensures it's treated as Paid.
|
// Let's KEEP SourceType as is (likely 2 for ichiban), but Amount=0 ensures it's treated as Paid.
|
||||||
@ -295,21 +289,24 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
|
|
||||||
if !useGamePass && req.UsePoints != nil && *req.UsePoints > 0 {
|
if !useGamePass && req.UsePoints != nil && *req.UsePoints > 0 {
|
||||||
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
||||||
// req.UsePoints 是前端传入的积分数,需要转换为分
|
usePts := *req.UsePoints
|
||||||
usePtsCents, _ := h.user.PointsToCents(ctx.RequestContext(), *req.UsePoints)
|
if bal > 0 && usePts > bal {
|
||||||
// bal 已经是分单位
|
usePts = bal
|
||||||
if bal > 0 && usePtsCents > bal {
|
|
||||||
usePtsCents = bal
|
|
||||||
}
|
}
|
||||||
// deductCents 是要从订单金额中抵扣的分数
|
ratePtsPerCent, _ := h.user.CentsToPoints(ctx.RequestContext(), 1)
|
||||||
deductCents := usePtsCents
|
if ratePtsPerCent <= 0 {
|
||||||
|
ratePtsPerCent = 1
|
||||||
|
}
|
||||||
|
deductCents := usePts / ratePtsPerCent
|
||||||
if deductCents > order.ActualAmount {
|
if deductCents > order.ActualAmount {
|
||||||
deductCents = order.ActualAmount
|
deductCents = order.ActualAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
if deductCents > 0 {
|
if deductCents > 0 {
|
||||||
// needPts 是实际需要扣除的分数
|
needPts := deductCents * ratePtsPerCent
|
||||||
needPts := deductCents
|
if needPts > usePts {
|
||||||
|
needPts = usePts
|
||||||
|
}
|
||||||
// Inline ConsumePointsFor logic using tx
|
// Inline ConsumePointsFor logic using tx
|
||||||
// Lock rows
|
// Lock rows
|
||||||
rows, errFind := tx.UserPoints.WithContext(ctx.RequestContext()).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).Order(tx.UserPoints.ValidEnd.Asc()).Find()
|
rows, errFind := tx.UserPoints.WithContext(ctx.RequestContext()).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).Order(tx.UserPoints.ValidEnd.Asc()).Find()
|
||||||
@ -397,30 +394,9 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 优惠券预扣:在事务中原子性扣减余额
|
// Inline RecordOrderCouponUsage (no logging)
|
||||||
// 如果余额不足(被其他并发订单消耗),事务回滚
|
if applied > 0 && req.CouponID != nil && *req.CouponID > 0 {
|
||||||
if applied > 0 && order.CouponID > 0 {
|
_ = tx.Orders.UnderlyingDB().Exec("INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))", order.ID, *req.CouponID, applied).Error
|
||||||
// 原子更新优惠券余额和状态
|
|
||||||
now := time.Now()
|
|
||||||
res := tx.Orders.UnderlyingDB().Exec(`
|
|
||||||
UPDATE user_coupons
|
|
||||||
SET balance_amount = balance_amount - ?,
|
|
||||||
status = CASE WHEN balance_amount - ? <= 0 THEN 4 ELSE 4 END,
|
|
||||||
used_order_id = ?,
|
|
||||||
used_at = ?
|
|
||||||
WHERE id = ? AND user_id = ? AND balance_amount >= ? AND status IN (1, 4)
|
|
||||||
`, applied, applied, order.ID, now, order.CouponID, userID, applied)
|
|
||||||
|
|
||||||
if res.Error != nil {
|
|
||||||
return fmt.Errorf("优惠券预扣失败: %w", res.Error)
|
|
||||||
}
|
|
||||||
if res.RowsAffected == 0 {
|
|
||||||
// 余额不足或状态不对,事务回滚
|
|
||||||
return errors.New("优惠券余额不足或已被使用")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录使用关系
|
|
||||||
_ = tx.Orders.UnderlyingDB().Exec("INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))", order.ID, order.CouponID, applied).Error
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -436,8 +412,16 @@ func (h *handler) JoinLottery() core.HandlerFunc {
|
|||||||
rsp.ActualAmount = order.ActualAmount
|
rsp.ActualAmount = order.ActualAmount
|
||||||
rsp.Status = order.Status
|
rsp.Status = order.Status
|
||||||
|
|
||||||
// 即时开奖触发(已支付 + 即时开奖模式)
|
// Immediate Draw Trigger if Paid (e.g. Game Pass or Free)
|
||||||
if shouldTriggerInstantDraw(order.Status, activity.DrawMode) {
|
if order.Status == 2 && activity.DrawMode == "instant" {
|
||||||
|
go func() {
|
||||||
|
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
// Immediate Draw Trigger if Paid (e.g. Game Pass or Free)
|
||||||
|
if order.Status == 2 && activity.DrawMode == "instant" {
|
||||||
|
// Trigger process asynchronously or synchronously?
|
||||||
|
// Usually WechatNotify triggers it. Since we bypass WechatNotify, we must trigger it.
|
||||||
go func() {
|
go func() {
|
||||||
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
|
||||||
}()
|
}()
|
||||||
@ -634,15 +618,12 @@ func (h *handler) validateIchibanSlots(ctx core.Context, req *joinLotteryRequest
|
|||||||
if totalSlots <= 0 {
|
if totalSlots <= 0 {
|
||||||
return core.Error(http.StatusBadRequest, 170008, "no slots")
|
return core.Error(http.StatusBadRequest, 170008, "no slots")
|
||||||
}
|
}
|
||||||
// 1. 强制校验:必须选择位置
|
if len(req.SlotIndex) > 0 {
|
||||||
if len(req.SlotIndex) == 0 {
|
|
||||||
return core.Error(http.StatusBadRequest, code.ParamBindError, "一番赏必须选择位置")
|
|
||||||
}
|
|
||||||
if req.Count <= 0 || req.Count != int64(len(req.SlotIndex)) {
|
if req.Count <= 0 || req.Count != int64(len(req.SlotIndex)) {
|
||||||
return core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误:数量与位置不匹配")
|
return core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 内存中去重和范围检查
|
// 1. 内存中去重和范围检查
|
||||||
selectedSlots := make([]int64, 0, len(req.SlotIndex))
|
selectedSlots := make([]int64, 0, len(req.SlotIndex))
|
||||||
seen := make(map[int64]struct{}, len(req.SlotIndex))
|
seen := make(map[int64]struct{}, len(req.SlotIndex))
|
||||||
for _, si := range req.SlotIndex {
|
for _, si := range req.SlotIndex {
|
||||||
@ -656,12 +637,13 @@ func (h *handler) validateIchibanSlots(ctx core.Context, req *joinLotteryRequest
|
|||||||
selectedSlots = append(selectedSlots, si-1)
|
selectedSlots = append(selectedSlots, si-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 批量查询数据库检查格位是否已被占用
|
// 2. 批量查询数据库检查格位是否已被占用
|
||||||
var occupiedCount int64
|
var occupiedCount int64
|
||||||
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index IN ?", req.IssueID, selectedSlots).Scan(&occupiedCount).Error
|
_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index IN ?", req.IssueID, selectedSlots).Scan(&occupiedCount).Error
|
||||||
if occupiedCount > 0 {
|
if occupiedCount > 0 {
|
||||||
// 即使是并发场景,这里做一个 Pre-check 也能拦截大部分冲突
|
// 如果有占用,为了告知具体是哪个位置,可以打个 log 或者简单的直接返回错误
|
||||||
return core.Error(http.StatusBadRequest, 170007, "部分位置已被占用,请刷新重试")
|
return core.Error(http.StatusBadRequest, 170007, "部分位置已被占用,请刷新重试")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,71 +1,11 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseSlotFromRemark(t *testing.T) {
|
func TestParseSlotFromRemark(t *testing.T) {
|
||||||
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
r := parseSlotFromRemark("lottery:activity:1|issue:2|count:1|slot:42")
|
||||||
if r != 42 {
|
if r != 42 { t.Fatalf("slot parse failed: %d", r) }
|
||||||
t.Fatalf("slot parse failed: %d", r)
|
|
||||||
}
|
|
||||||
r2 := parseSlotFromRemark("lottery:activity:1|issue:2|count:1")
|
r2 := parseSlotFromRemark("lottery:activity:1|issue:2|count:1")
|
||||||
if r2 != -1 {
|
if r2 != -1 { t.Fatalf("expected -1, got %d", r2) }
|
||||||
t.Fatalf("expected -1, got %d", r2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestShouldTriggerInstantDraw 验证即时开奖触发条件
|
|
||||||
func TestShouldTriggerInstantDraw(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
orderStatus int32
|
|
||||||
drawMode string
|
|
||||||
shouldTrigger bool
|
|
||||||
}{
|
|
||||||
{"已支付+即时开奖", 2, "instant", true},
|
|
||||||
{"已支付+定时开奖", 2, "scheduled", false},
|
|
||||||
{"未支付+即时开奖", 1, "instant", false},
|
|
||||||
{"未支付+定时开奖", 1, "scheduled", false},
|
|
||||||
{"已取消+即时开奖", 3, "instant", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
result := shouldTriggerInstantDraw(tc.orderStatus, tc.drawMode)
|
|
||||||
if result != tc.shouldTrigger {
|
|
||||||
t.Errorf("期望触发=%v,实际触发=%v", tc.shouldTrigger, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInstantDrawTriggerOnce 验证即时开奖只触发一次
|
|
||||||
// 这个测试模拟 JoinLottery 中的触发逻辑,确保不会重复触发
|
|
||||||
func TestInstantDrawTriggerOnce(t *testing.T) {
|
|
||||||
var callCount int32 = 0
|
|
||||||
|
|
||||||
// 模拟 ProcessOrderLottery 的调用
|
|
||||||
processOrderLottery := func() {
|
|
||||||
atomic.AddInt32(&callCount, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟订单状态
|
|
||||||
orderStatus := int32(2)
|
|
||||||
drawMode := "instant"
|
|
||||||
|
|
||||||
// 执行触发逻辑(使用辅助函数,避免重复代码)
|
|
||||||
if shouldTriggerInstantDraw(orderStatus, drawMode) {
|
|
||||||
go processOrderLottery()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待 goroutine 完成
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// 验证只调用一次
|
|
||||||
if callCount != 1 {
|
|
||||||
t.Errorf("ProcessOrderLottery 应该只被调用 1 次,实际调用了 %d 次", callCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
|||||||
sc.discount_value
|
sc.discount_value
|
||||||
FROM user_coupons uc
|
FROM user_coupons uc
|
||||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
||||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status IN (1, 4)
|
WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, userCouponID, userID).Scan(&result).Error
|
`, userCouponID, userID).Scan(&result).Error
|
||||||
|
|
||||||
@ -82,6 +82,9 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
|||||||
switch result.DiscountType {
|
switch result.DiscountType {
|
||||||
case 1: // 金额券
|
case 1: // 金额券
|
||||||
bal := result.BalanceAmount
|
bal := result.BalanceAmount
|
||||||
|
if bal <= 0 {
|
||||||
|
bal = result.DiscountValue
|
||||||
|
}
|
||||||
if bal > 0 {
|
if bal > 0 {
|
||||||
if bal > remainingCap {
|
if bal > remainingCap {
|
||||||
applied = remainingCap
|
applied = remainingCap
|
||||||
@ -122,46 +125,6 @@ func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *mode
|
|||||||
return applied
|
return applied
|
||||||
}
|
}
|
||||||
|
|
||||||
// preDeductCouponInTx 在事务中预扣优惠券余额
|
|
||||||
// 功能:原子性地扣减余额并设置 status=4(预扣中),防止并发超额使用
|
|
||||||
// 参数:
|
|
||||||
// - ctx:请求上下文
|
|
||||||
// - tx:数据库事务(必须在事务中调用)
|
|
||||||
// - userID:用户ID
|
|
||||||
// - userCouponID:用户持券ID
|
|
||||||
// - appliedAmount:要预扣的金额(分)
|
|
||||||
// - orderID:关联的订单ID
|
|
||||||
//
|
|
||||||
// 返回:是否成功预扣
|
|
||||||
func (h *handler) preDeductCouponInTx(ctx core.Context, txDB interface {
|
|
||||||
Exec(sql string, values ...interface{}) interface {
|
|
||||||
RowsAffected() int64
|
|
||||||
Error() error
|
|
||||||
}
|
|
||||||
}, userID int64, userCouponID int64, appliedAmount int64, orderID int64) bool {
|
|
||||||
if appliedAmount <= 0 || userCouponID <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// 原子更新:扣减余额 + 设置状态为预扣中(4) + 关联订单
|
|
||||||
// 条件:余额足够 且 状态为未使用(1)或使用中(4,支持同一券多订单分批扣减场景,但需余额足够)
|
|
||||||
result := txDB.Exec(`
|
|
||||||
UPDATE user_coupons
|
|
||||||
SET balance_amount = balance_amount - ?,
|
|
||||||
status = CASE WHEN balance_amount - ? <= 0 THEN 4 ELSE 4 END,
|
|
||||||
used_order_id = ?,
|
|
||||||
used_at = ?
|
|
||||||
WHERE id = ? AND user_id = ? AND balance_amount >= ? AND status IN (1, 4)
|
|
||||||
`, appliedAmount, appliedAmount, orderID, now, userCouponID, userID, appliedAmount)
|
|
||||||
|
|
||||||
if result.Error() != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.RowsAffected() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销)
|
// updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销)
|
||||||
// 功能:根据订单 remark 中记录的 applied_amount,
|
// 功能:根据订单 remark 中记录的 applied_amount,
|
||||||
//
|
//
|
||||||
@ -191,7 +154,7 @@ func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, ord
|
|||||||
sc.discount_value
|
sc.discount_value
|
||||||
FROM user_coupons uc
|
FROM user_coupons uc
|
||||||
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1
|
||||||
WHERE uc.id = ? AND uc.user_id = ? AND uc.status IN (1, 4)
|
WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, userCouponID, userID).Scan(&result).Error
|
`, userCouponID, userID).Scan(&result).Error
|
||||||
|
|
||||||
@ -311,14 +274,3 @@ func parseIssueIDFromRemark(remarkStr string) int64 {
|
|||||||
func parseCountFromRemark(remarkStr string) int64 {
|
func parseCountFromRemark(remarkStr string) int64 {
|
||||||
return remark.Parse(remarkStr).Count
|
return remark.Parse(remarkStr).Count
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldTriggerInstantDraw 判断是否应该触发即时开奖
|
|
||||||
// 功能:封装即时开奖触发条件判断,避免条件重复
|
|
||||||
// 参数:
|
|
||||||
// - orderStatus:订单状态(2=已支付)
|
|
||||||
// - drawMode:开奖模式("instant"=即时开奖)
|
|
||||||
//
|
|
||||||
// 返回:是否应该触发即时开奖
|
|
||||||
func shouldTriggerInstantDraw(orderStatus int32, drawMode string) bool {
|
|
||||||
return orderStatus == 2 && drawMode == "instant"
|
|
||||||
}
|
|
||||||
|
|||||||
@ -30,7 +30,6 @@ type matchingGamePreOrderRequest struct {
|
|||||||
CouponID *int64 `json:"coupon_id"`
|
CouponID *int64 `json:"coupon_id"`
|
||||||
ItemCardID *int64 `json:"item_card_id"`
|
ItemCardID *int64 `json:"item_card_id"`
|
||||||
UseGamePass bool `json:"use_game_pass"` // 新增:是否使用次数卡
|
UseGamePass bool `json:"use_game_pass"` // 新增:是否使用次数卡
|
||||||
Count int64 `json:"count"` // 新增:购买数量
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type matchingGamePreOrderResponse struct {
|
type matchingGamePreOrderResponse struct {
|
||||||
@ -83,12 +82,6 @@ func (h *handler) PreOrderMatchingGame() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验 Count:对对碰只能单次购买
|
|
||||||
if req.Count > 1 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170013, "对对碰游戏暂不支持批量购买,请单次支付"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Get Activity/Issue Info (Mocking price for now or fetching if available)
|
// 1. Get Activity/Issue Info (Mocking price for now or fetching if available)
|
||||||
// Assuming price is fixed or fetched. Let's fetch basic activity info if possible, or assume config.
|
// Assuming price is fixed or fetched. Let's fetch basic activity info if possible, or assume config.
|
||||||
// Since Request has IssueID, let's fetch Issue to get ActivityID and Price.
|
// Since Request has IssueID, let's fetch Issue to get ActivityID and Price.
|
||||||
@ -173,13 +166,7 @@ func (h *handler) PreOrderMatchingGame() core.HandlerFunc {
|
|||||||
ActualAmount: 0, // 次数卡抵扣,实付0元
|
ActualAmount: 0, // 次数卡抵扣,实付0元
|
||||||
DiscountAmount: activity.PriceDraw,
|
DiscountAmount: activity.PriceDraw,
|
||||||
Status: 2, // 已支付
|
Status: 2, // 已支付
|
||||||
Remark: func() string {
|
Remark: fmt.Sprintf("activity:%d|game_pass:%d|matching_game:issue:%d", activity.ID, validPass.ID, req.IssueID),
|
||||||
r := fmt.Sprintf("activity:%d|game_pass:%d|matching_game:issue:%d", activity.ID, validPass.ID, req.IssueID)
|
|
||||||
if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
|
|
||||||
r += fmt.Sprintf("|itemcard:%d", *req.ItemCardID)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}(),
|
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
PaidAt: now,
|
PaidAt: now,
|
||||||
@ -545,18 +532,14 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc {
|
|||||||
zap.Bool("is_ok", scopeOK))
|
zap.Bool("is_ok", scopeOK))
|
||||||
|
|
||||||
if scopeOK {
|
if scopeOK {
|
||||||
// Fix: Don't set cardToVoid immediately. Only set it if an effect is actually applied.
|
cardToVoid = icID
|
||||||
|
|
||||||
// Double reward
|
|
||||||
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
|
||||||
cardToVoid = icID // Mark for consumption
|
// Double reward
|
||||||
h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
||||||
finalQuantity = 2
|
finalQuantity = 2
|
||||||
finalRemark += "(倍数)"
|
finalRemark += "(倍数)"
|
||||||
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
|
||||||
// Probability boost
|
// Probability boost - try to upgrade to better reward
|
||||||
cardToVoid = icID // Mark for consumption (even if RNG fails, the card is "used")
|
|
||||||
|
|
||||||
h.logger.Debug("道具卡-CheckMatchingGame: 应用概率提升", zap.Int32("boost_rate", ic.BoostRateX1000))
|
h.logger.Debug("道具卡-CheckMatchingGame: 应用概率提升", zap.Int32("boost_rate", ic.BoostRateX1000))
|
||||||
allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
|
allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
|
||||||
h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
|
h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
|
||||||
@ -597,11 +580,6 @@ func (h *handler) CheckMatchingGame() core.HandlerFunc {
|
|||||||
} else {
|
} else {
|
||||||
h.logger.Debug("道具卡-CheckMatchingGame: 未找到更好的奖品可升级", zap.Int64("current_score", candidate.MinScore))
|
h.logger.Debug("道具卡-CheckMatchingGame: 未找到更好的奖品可升级", zap.Int64("current_score", candidate.MinScore))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Effect not recognized or params too low
|
|
||||||
h.logger.Warn("道具卡-CheckMatchingGame: 效果类型未知或参数无效,不消耗卡片",
|
|
||||||
zap.Int32("effect_type", ic.EffectType),
|
|
||||||
zap.Int32("multiplier", ic.RewardMultiplierX1000))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
|
h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
|
||||||
|
|||||||
@ -7,31 +7,31 @@ import (
|
|||||||
|
|
||||||
// TestSelectRewardExact 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore
|
// TestSelectRewardExact 测试对对碰选奖逻辑:精确匹配 TotalPairs == MinScore
|
||||||
func TestSelectRewardExact(t *testing.T) {
|
func TestSelectRewardExact(t *testing.T) {
|
||||||
// 模拟奖品设置 (使用 Level 作为标识,因为 ActivityRewardSettings 没有 Name 字段)
|
// 模拟奖品设置
|
||||||
rewards := []*model.ActivityRewardSettings{
|
rewards := []*model.ActivityRewardSettings{
|
||||||
{ID: 1, Level: 1, MinScore: 10, Quantity: 5},
|
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 5},
|
||||||
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
||||||
{ID: 3, Level: 3, MinScore: 30, Quantity: 5},
|
{ID: 3, Name: "奖品C-30对", MinScore: 30, Quantity: 5},
|
||||||
{ID: 4, Level: 4, MinScore: 40, Quantity: 5},
|
{ID: 4, Name: "奖品D-40对", MinScore: 40, Quantity: 5},
|
||||||
{ID: 5, Level: 5, MinScore: 45, Quantity: 5},
|
{ID: 5, Name: "奖品E-45对", MinScore: 45, Quantity: 5},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
totalPairs int64
|
totalPairs int64
|
||||||
expectReward *int64 // nil = 无匹配
|
expectReward *int64 // nil = 无匹配
|
||||||
expectLevel int32
|
expectName string
|
||||||
}{
|
}{
|
||||||
{"精确匹配10对", 10, ptr(int64(1)), 1},
|
{"精确匹配10对", 10, ptr(int64(1)), "奖品A-10对"},
|
||||||
{"精确匹配20对", 20, ptr(int64(2)), 2},
|
{"精确匹配20对", 20, ptr(int64(2)), "奖品B-20对"},
|
||||||
{"精确匹配30对", 30, ptr(int64(3)), 3},
|
{"精确匹配30对", 30, ptr(int64(3)), "奖品C-30对"},
|
||||||
{"精确匹配40对", 40, ptr(int64(4)), 4},
|
{"精确匹配40对", 40, ptr(int64(4)), "奖品D-40对"},
|
||||||
{"精确匹配45对", 45, ptr(int64(5)), 5},
|
{"精确匹配45对", 45, ptr(int64(5)), "奖品E-45对"},
|
||||||
{"15对-无匹配", 15, nil, 0},
|
{"15对-无匹配", 15, nil, ""},
|
||||||
{"25对-无匹配", 25, nil, 0},
|
{"25对-无匹配", 25, nil, ""},
|
||||||
{"35对-无匹配", 35, nil, 0},
|
{"35对-无匹配", 35, nil, ""},
|
||||||
{"50对-无匹配", 50, nil, 0},
|
{"50对-无匹配", 50, nil, ""},
|
||||||
{"0对-无匹配", 0, nil, 0},
|
{"0对-无匹配", 0, nil, ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -40,15 +40,15 @@ func TestSelectRewardExact(t *testing.T) {
|
|||||||
|
|
||||||
if tc.expectReward == nil {
|
if tc.expectReward == nil {
|
||||||
if candidate != nil {
|
if candidate != nil {
|
||||||
t.Errorf("期望无匹配,但得到奖品: Level=%d (ID=%d)", candidate.Level, candidate.ID)
|
t.Errorf("期望无匹配,但得到奖品: %s (ID=%d)", candidate.Name, candidate.ID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if candidate == nil {
|
if candidate == nil {
|
||||||
t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward)
|
t.Errorf("期望匹配奖品ID=%d,但无匹配", *tc.expectReward)
|
||||||
} else if candidate.ID != *tc.expectReward {
|
} else if candidate.ID != *tc.expectReward {
|
||||||
t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID)
|
t.Errorf("期望奖品ID=%d,实际=%d", *tc.expectReward, candidate.ID)
|
||||||
} else if candidate.Level != tc.expectLevel {
|
} else if candidate.Name != tc.expectName {
|
||||||
t.Errorf("期望奖品Level=%d,实际=%d", tc.expectLevel, candidate.Level)
|
t.Errorf("期望奖品名=%s,实际=%s", tc.expectName, candidate.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -58,14 +58,14 @@ func TestSelectRewardExact(t *testing.T) {
|
|||||||
// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配
|
// TestSelectRewardWithZeroQuantity 测试库存为0时不匹配
|
||||||
func TestSelectRewardWithZeroQuantity(t *testing.T) {
|
func TestSelectRewardWithZeroQuantity(t *testing.T) {
|
||||||
rewards := []*model.ActivityRewardSettings{
|
rewards := []*model.ActivityRewardSettings{
|
||||||
{ID: 1, Level: 1, MinScore: 10, Quantity: 0}, // 库存为0
|
{ID: 1, Name: "奖品A-10对", MinScore: 10, Quantity: 0}, // 库存为0
|
||||||
{ID: 2, Level: 2, MinScore: 20, Quantity: 5},
|
{ID: 2, Name: "奖品B-20对", MinScore: 20, Quantity: 5},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 即使精确匹配,库存为0也不应匹配
|
// 即使精确匹配,库存为0也不应匹配
|
||||||
candidate := selectRewardExact(rewards, 10)
|
candidate := selectRewardExact(rewards, 10)
|
||||||
if candidate != nil {
|
if candidate != nil {
|
||||||
t.Errorf("库存为0时不应匹配,但得到: Level=%d", candidate.Level)
|
t.Errorf("库存为0时不应匹配,但得到: %s", candidate.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 库存>0应正常匹配
|
// 库存>0应正常匹配
|
||||||
|
|||||||
@ -65,73 +65,10 @@ func (h *handler) startMatchingGameCleanup() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoCheckDatabaseFallback 数据库扫描兜底(防止Redis缓存过期导致漏单)
|
|
||||||
func (h *handler) autoCheckDatabaseFallback() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 1. 查询 30分钟前~24小时内 已支付 但 未开奖 的对对碰订单 (SourceType=3)
|
|
||||||
// 这个时间窗口是为了避开正常游戏中的订单 (Redis TTL 30m)
|
|
||||||
startTime := time.Now().Add(-24 * time.Hour)
|
|
||||||
endTime := time.Now().Add(-30 * time.Minute)
|
|
||||||
|
|
||||||
// 使用 left join 排除已有日志的订单
|
|
||||||
var orderNos []string
|
|
||||||
err := h.readDB.Orders.WithContext(ctx).UnderlyingDB().Raw(`
|
|
||||||
SELECT o.order_no
|
|
||||||
FROM orders o
|
|
||||||
LEFT JOIN activity_draw_logs l ON o.id = l.order_id
|
|
||||||
WHERE o.source_type = 3
|
|
||||||
AND o.status = 2
|
|
||||||
AND o.created_at BETWEEN ? AND ?
|
|
||||||
AND l.id IS NULL
|
|
||||||
`, startTime, endTime).Scan(&orderNos).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error("对对碰兜底扫描: 查询失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(orderNos) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.logger.Info("对对碰兜底扫描: 发现异常订单", zap.Int("count", len(orderNos)))
|
|
||||||
|
|
||||||
for _, orderNo := range orderNos {
|
|
||||||
// 2. 加载订单详情
|
|
||||||
order, err := h.readDB.Orders.WithContext(ctx).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
|
|
||||||
if err != nil || order == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 重构游戏状态
|
|
||||||
// 我们需要从 Seed, Position 等信息重构 Memory Graph
|
|
||||||
game, err := h.activity.ReconstructMatchingGame(ctx, orderNo)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error("对对碰兜底扫描: 游戏状态重构失败", zap.String("order_no", orderNo), zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 重构 GameID (模拟)
|
|
||||||
// 注意:原始 GameID 可能丢失,这里我们并不真的需要精确的 Request GameID,
|
|
||||||
// 因为 doAutoCheck 主要依赖 game 对象和 OrderID。
|
|
||||||
// 但为了锁的唯一性,我们使用 MG_FALLBACK_{OrderID}
|
|
||||||
fakeGameID := fmt.Sprintf("FALLBACK_%d", order.ID)
|
|
||||||
|
|
||||||
h.logger.Info("对对碰兜底扫描: 触发补单", zap.String("order_no", orderNo))
|
|
||||||
h.doAutoCheck(ctx, fakeGameID, game, order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoCheckExpiredGames 扫描超时未结算的对对碰游戏并自动开奖
|
// autoCheckExpiredGames 扫描超时未结算的对对碰游戏并自动开奖
|
||||||
func (h *handler) autoCheckExpiredGames() {
|
func (h *handler) autoCheckExpiredGames() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// 0. 执行数据库兜底扫描 (低频执行,例如每次 autoCheck 都跑,或者加计数器)
|
|
||||||
// 由于 autoCheckHelper 是每3分钟跑一次,这里直接调用损耗可控
|
|
||||||
// 且查询走了索引 (created_at)
|
|
||||||
h.autoCheckDatabaseFallback()
|
|
||||||
|
|
||||||
// 1. 扫描 Redis 中所有 matching_game key
|
// 1. 扫描 Redis 中所有 matching_game key
|
||||||
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
keys, err := h.redis.Keys(ctx, activitysvc.MatchingGameKeyPrefix+"*").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -464,7 +464,6 @@ type activityItem struct {
|
|||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
PriceDraw int64 `json:"price_draw"`
|
PriceDraw int64 `json:"price_draw"`
|
||||||
IsBoss int32 `json:"is_boss"`
|
IsBoss int32 `json:"is_boss"`
|
||||||
PlayType string `json:"play_type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type listActivitiesResponse struct {
|
type listActivitiesResponse struct {
|
||||||
@ -545,7 +544,6 @@ func (h *handler) ListActivities() core.HandlerFunc {
|
|||||||
Status: v.Status,
|
Status: v.Status,
|
||||||
PriceDraw: v.PriceDraw,
|
PriceDraw: v.PriceDraw,
|
||||||
IsBoss: v.IsBoss,
|
IsBoss: v.IsBoss,
|
||||||
PlayType: v.PlayType,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Payload(res)
|
ctx.Payload(res)
|
||||||
|
|||||||
@ -1,62 +1,33 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
activitysvc "bindbox-game/internal/service/activity"
|
activitysvc "bindbox-game/internal/service/activity"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type activityCommitGenerateResp struct {
|
type activityCommitGenerateResp struct{ SeedVersion int32 `json:"seed_version"` }
|
||||||
SeedVersion int32 `json:"seed_version"`
|
type activityCommitSummaryResp struct{ SeedVersion int32 `json:"seed_version"`; Algo string `json:"algo"`; HasSeed bool `json:"has_seed"`; LenSeedMaster int `json:"len_seed_master"`; LenSeedHash int `json:"len_seed_hash"`; LenItemsRoot int `json:"len_items_root"`; ItemsRootHex string `json:"items_root_hex"` }
|
||||||
}
|
|
||||||
type activityCommitSummaryResp struct {
|
|
||||||
SeedVersion int32 `json:"seed_version"`
|
|
||||||
Algo string `json:"algo"`
|
|
||||||
HasSeed bool `json:"has_seed"`
|
|
||||||
LenSeedMaster int `json:"len_seed_master"`
|
|
||||||
LenSeedHash int `json:"len_seed_hash"`
|
|
||||||
LenItemsRoot int `json:"len_items_root"`
|
|
||||||
ItemsRootHex string `json:"items_root_hex"`
|
|
||||||
}
|
|
||||||
type activityCredentialResp struct {
|
|
||||||
SeedMasterHex string `json:"seed_master_hex"`
|
|
||||||
SeedHashHex string `json:"seed_hash_hex"`
|
|
||||||
ItemsRootHex string `json:"items_root_hex"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) GenerateActivityCommitmentGeneral() core.HandlerFunc {
|
func (h *handler) GenerateActivityCommitmentGeneral() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
|
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64); if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID")); return }
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
|
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
|
||||||
ver, e := svc.Generate(ctx.RequestContext(), activityID)
|
ver, e := svc.Generate(ctx.RequestContext(), activityID)
|
||||||
if e != nil {
|
if e != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 170301, e.Error())); return }
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170301, e.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Payload(&activityCommitGenerateResp{SeedVersion: ver})
|
ctx.Payload(&activityCommitGenerateResp{SeedVersion: ver})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) GetActivityCommitmentSummaryGeneral() core.HandlerFunc {
|
func (h *handler) GetActivityCommitmentSummaryGeneral() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
|
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64); if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID")); return }
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
|
svc := activitysvc.NewActivityCommitmentService(dao.Use(h.repo.GetDbR()), dao.Use(h.repo.GetDbW()), h.repo)
|
||||||
sum, e := svc.Summary(ctx.RequestContext(), activityID)
|
sum, e := svc.Summary(ctx.RequestContext(), activityID)
|
||||||
if e != nil {
|
if e != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, 170302, e.Error())); return }
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 170302, e.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var lenMaster, lenHash, lenRoot *int
|
var lenMaster, lenHash, lenRoot *int
|
||||||
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&lenMaster)
|
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&lenMaster)
|
||||||
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&lenHash)
|
_ = h.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&lenHash)
|
||||||
@ -65,48 +36,12 @@ func (h *handler) GetActivityCommitmentSummaryGeneral() core.HandlerFunc {
|
|||||||
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsHex)
|
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsHex)
|
||||||
|
|
||||||
lm, lh, lr := 0, 0, 0
|
lm, lh, lr := 0, 0, 0
|
||||||
if lenMaster != nil {
|
if lenMaster != nil { lm = *lenMaster }
|
||||||
lm = *lenMaster
|
if lenHash != nil { lh = *lenHash }
|
||||||
}
|
if lenRoot != nil { lr = *lenRoot }
|
||||||
if lenHash != nil {
|
|
||||||
lh = *lenHash
|
|
||||||
}
|
|
||||||
if lenRoot != nil {
|
|
||||||
lr = *lenRoot
|
|
||||||
}
|
|
||||||
ih := ""
|
ih := ""
|
||||||
if itemsHex != nil {
|
if itemsHex != nil { ih = *itemsHex }
|
||||||
ih = *itemsHex
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&activityCommitSummaryResp{SeedVersion: sum.SeedVersion, Algo: sum.Algo, HasSeed: sum.HasSeed, LenSeedMaster: lm, LenSeedHash: lh, LenItemsRoot: lr, ItemsRootHex: ih})
|
ctx.Payload(&activityCommitSummaryResp{SeedVersion: sum.SeedVersion, Algo: sum.Algo, HasSeed: sum.HasSeed, LenSeedMaster: lm, LenSeedHash: lh, LenItemsRoot: lr, ItemsRootHex: ih})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) GetActivityCredential() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var seedMasterHex, seedHashHex, itemsRootHex *string
|
|
||||||
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&seedMasterHex)
|
|
||||||
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_seed_hash) FROM activities WHERE id=?", activityID).Scan(&seedHashHex)
|
|
||||||
_ = h.repo.GetDbR().Raw("SELECT HEX(commitment_items_root) FROM activities WHERE id=?", activityID).Scan(&itemsRootHex)
|
|
||||||
|
|
||||||
resp := &activityCredentialResp{}
|
|
||||||
if seedMasterHex != nil {
|
|
||||||
resp.SeedMasterHex = *seedMasterHex
|
|
||||||
}
|
|
||||||
if seedHashHex != nil {
|
|
||||||
resp.SeedHashHex = *seedHashHex
|
|
||||||
}
|
|
||||||
if itemsRootHex != nil {
|
|
||||||
resp.ItemsRootHex = *itemsRootHex
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -9,8 +9,6 @@ import (
|
|||||||
bannersvc "bindbox-game/internal/service/banner"
|
bannersvc "bindbox-game/internal/service/banner"
|
||||||
channelsvc "bindbox-game/internal/service/channel"
|
channelsvc "bindbox-game/internal/service/channel"
|
||||||
douyinsvc "bindbox-game/internal/service/douyin"
|
douyinsvc "bindbox-game/internal/service/douyin"
|
||||||
gamesvc "bindbox-game/internal/service/game"
|
|
||||||
livestreamsvc "bindbox-game/internal/service/livestream"
|
|
||||||
productsvc "bindbox-game/internal/service/product"
|
productsvc "bindbox-game/internal/service/product"
|
||||||
snapshotsvc "bindbox-game/internal/service/snapshot"
|
snapshotsvc "bindbox-game/internal/service/snapshot"
|
||||||
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
syscfgsvc "bindbox-game/internal/service/sysconfig"
|
||||||
@ -36,7 +34,6 @@ type handler struct {
|
|||||||
snapshotSvc snapshotsvc.Service
|
snapshotSvc snapshotsvc.Service
|
||||||
rollbackSvc snapshotsvc.RollbackService
|
rollbackSvc snapshotsvc.RollbackService
|
||||||
douyinSvc douyinsvc.Service
|
douyinSvc douyinsvc.Service
|
||||||
livestream livestreamsvc.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler {
|
||||||
@ -44,8 +41,6 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
|
|||||||
snapshotSvc := snapshotsvc.NewService(db)
|
snapshotSvc := snapshotsvc.NewService(db)
|
||||||
rollbackSvc := snapshotsvc.NewRollbackService(db, snapshotSvc)
|
rollbackSvc := snapshotsvc.NewRollbackService(db, snapshotSvc)
|
||||||
syscfgSvc := syscfgsvc.New(logger, db)
|
syscfgSvc := syscfgsvc.New(logger, db)
|
||||||
ticketSvc := gamesvc.NewTicketService(logger, db) // 游戏资格服务
|
|
||||||
titleSvc := titlesvc.New(logger, db) // 称号服务
|
|
||||||
return &handler{
|
return &handler{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
writeDB: dao.Use(db.GetDbW()),
|
writeDB: dao.Use(db.GetDbW()),
|
||||||
@ -57,11 +52,10 @@ func New(logger logger.CustomLogger, db mysql.Repo, rdb *redis.Client) *handler
|
|||||||
userSvc: userSvc,
|
userSvc: userSvc,
|
||||||
banner: bannersvc.New(logger, db),
|
banner: bannersvc.New(logger, db),
|
||||||
channel: channelsvc.New(logger, db),
|
channel: channelsvc.New(logger, db),
|
||||||
title: titleSvc,
|
title: titlesvc.New(logger, db),
|
||||||
syscfg: syscfgSvc,
|
syscfg: syscfgSvc,
|
||||||
snapshotSvc: snapshotSvc,
|
snapshotSvc: snapshotSvc,
|
||||||
rollbackSvc: rollbackSvc,
|
rollbackSvc: rollbackSvc,
|
||||||
douyinSvc: douyinsvc.New(logger, db, syscfgSvc, ticketSvc, userSvc, titleSvc),
|
douyinSvc: douyinsvc.New(logger, db, syscfgSvc),
|
||||||
livestream: livestreamsvc.New(logger, db, ticketSvc), // 传入ticketSvc
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,319 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
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 addBlacklistRequest struct {
|
|
||||||
DouyinUserID string `json:"douyin_user_id" binding:"required"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type blacklistResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
DouyinUserID string `json:"douyin_user_id"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
OperatorID int64 `json:"operator_id"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listBlacklistRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
Keyword string `form:"keyword"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listBlacklistResponse struct {
|
|
||||||
List []blacklistResponse `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBlacklist 获取黑名单列表
|
|
||||||
// @Summary 获取黑名单列表
|
|
||||||
// @Description 获取抖音用户黑名单列表,支持分页和关键词搜索
|
|
||||||
// @Tags 管理端.黑名单
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param page query int false "页码" default(1)
|
|
||||||
// @Param page_size query int false "每页数量" default(20)
|
|
||||||
// @Param keyword query string false "搜索关键词(抖音ID)"
|
|
||||||
// @Success 200 {object} listBlacklistResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/blacklist [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) ListBlacklist() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(listBlacklistRequest)
|
|
||||||
if err := ctx.ShouldBindQuery(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
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
|
||||||
Table("douyin_blacklist").
|
|
||||||
Where("status = 1")
|
|
||||||
|
|
||||||
if req.Keyword != "" {
|
|
||||||
db = db.Where("douyin_user_id LIKE ?", "%"+req.Keyword+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
if err := db.Count(&total).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var list []model.DouyinBlacklist
|
|
||||||
if err := db.Order("id DESC").
|
|
||||||
Offset((req.Page - 1) * req.PageSize).
|
|
||||||
Limit(req.PageSize).
|
|
||||||
Find(&list).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rsp := &listBlacklistResponse{
|
|
||||||
List: make([]blacklistResponse, len(list)),
|
|
||||||
Total: total,
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, item := range list {
|
|
||||||
rsp.List[i] = blacklistResponse{
|
|
||||||
ID: item.ID,
|
|
||||||
DouyinUserID: item.DouyinUserID,
|
|
||||||
Reason: item.Reason,
|
|
||||||
OperatorID: item.OperatorID,
|
|
||||||
Status: item.Status,
|
|
||||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(rsp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddBlacklist 添加黑名单
|
|
||||||
// @Summary 添加黑名单
|
|
||||||
// @Description 将抖音用户添加到黑名单
|
|
||||||
// @Tags 管理端.黑名单
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body addBlacklistRequest true "请求参数"
|
|
||||||
// @Success 200 {object} blacklistResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/blacklist [post]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) AddBlacklist() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(addBlacklistRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已在黑名单
|
|
||||||
var existCount int64
|
|
||||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
|
||||||
Table("douyin_blacklist").
|
|
||||||
Where("douyin_user_id = ? AND status = 1", req.DouyinUserID).
|
|
||||||
Count(&existCount)
|
|
||||||
|
|
||||||
if existCount > 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该用户已在黑名单中"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID := int64(0)
|
|
||||||
if ctx.SessionUserInfo().Id > 0 {
|
|
||||||
operatorID = int64(ctx.SessionUserInfo().Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
blacklist := &model.DouyinBlacklist{
|
|
||||||
DouyinUserID: req.DouyinUserID,
|
|
||||||
Reason: req.Reason,
|
|
||||||
OperatorID: operatorID,
|
|
||||||
Status: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.repo.GetDbW().WithContext(ctx.RequestContext()).Create(blacklist).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&blacklistResponse{
|
|
||||||
ID: blacklist.ID,
|
|
||||||
DouyinUserID: blacklist.DouyinUserID,
|
|
||||||
Reason: blacklist.Reason,
|
|
||||||
OperatorID: blacklist.OperatorID,
|
|
||||||
Status: blacklist.Status,
|
|
||||||
CreatedAt: blacklist.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveBlacklist 移除黑名单
|
|
||||||
// @Summary 移除黑名单
|
|
||||||
// @Description 将用户从黑名单中移除(软删除,status设为0)
|
|
||||||
// @Tags 管理端.黑名单
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "黑名单ID"
|
|
||||||
// @Success 200 {object} simpleMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/blacklist/{id} [delete]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) RemoveBlacklist() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
idStr := ctx.Param("id")
|
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := h.repo.GetDbW().WithContext(ctx.RequestContext()).
|
|
||||||
Table("douyin_blacklist").
|
|
||||||
Where("id = ?", id).
|
|
||||||
Update("status", 0)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, result.Error.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ParamBindError, "黑名单记录不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&simpleMessageResponse{Message: "移除成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckBlacklist 检查用户是否在黑名单
|
|
||||||
// @Summary 检查黑名单状态
|
|
||||||
// @Description 检查指定抖音用户是否在黑名单中
|
|
||||||
// @Tags 管理端.黑名单
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param douyin_user_id query string true "抖音用户ID"
|
|
||||||
// @Success 200 {object} map[string]bool
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/blacklist/check [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) CheckBlacklist() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
douyinUserID := ctx.RequestInputParams().Get("douyin_user_id")
|
|
||||||
if douyinUserID == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音用户ID不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
|
||||||
Table("douyin_blacklist").
|
|
||||||
Where("douyin_user_id = ? AND status = 1", douyinUserID).
|
|
||||||
Count(&count)
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{
|
|
||||||
"douyin_user_id": douyinUserID,
|
|
||||||
"is_blacklisted": count > 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchAddBlacklist 批量添加黑名单
|
|
||||||
// @Summary 批量添加黑名单
|
|
||||||
// @Description 批量将抖音用户添加到黑名单
|
|
||||||
// @Tags 管理端.黑名单
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body batchAddBlacklistRequest true "请求参数"
|
|
||||||
// @Success 200 {object} batchAddBlacklistResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/blacklist/batch [post]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) BatchAddBlacklist() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
var req struct {
|
|
||||||
DouyinUserIDs []string `json:"douyin_user_ids" binding:"required"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.DouyinUserIDs) == 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音用户ID列表不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取操作人ID
|
|
||||||
operatorID := int64(0)
|
|
||||||
if ctx.SessionUserInfo().Id > 0 {
|
|
||||||
operatorID = int64(ctx.SessionUserInfo().Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询已存在的黑名单
|
|
||||||
var existingIDs []string
|
|
||||||
h.repo.GetDbR().WithContext(ctx.RequestContext()).
|
|
||||||
Table("douyin_blacklist").
|
|
||||||
Where("douyin_user_id IN ? AND status = 1", req.DouyinUserIDs).
|
|
||||||
Pluck("douyin_user_id", &existingIDs)
|
|
||||||
|
|
||||||
existMap := make(map[string]bool)
|
|
||||||
for _, id := range existingIDs {
|
|
||||||
existMap[id] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤出需要新增的
|
|
||||||
var toAdd []model.DouyinBlacklist
|
|
||||||
for _, uid := range req.DouyinUserIDs {
|
|
||||||
if !existMap[uid] {
|
|
||||||
toAdd = append(toAdd, model.DouyinBlacklist{
|
|
||||||
DouyinUserID: uid,
|
|
||||||
Reason: req.Reason,
|
|
||||||
OperatorID: operatorID,
|
|
||||||
Status: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addedCount := 0
|
|
||||||
if len(toAdd) > 0 {
|
|
||||||
if err := h.repo.GetDbW().WithContext(ctx.RequestContext()).Create(&toAdd).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addedCount = len(toAdd)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{
|
|
||||||
"total_requested": len(req.DouyinUserIDs),
|
|
||||||
"added": addedCount,
|
|
||||||
"skipped": len(req.DouyinUserIDs) - addedCount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -45,8 +45,6 @@ func (h *handler) CreateChannel() core.HandlerFunc {
|
|||||||
|
|
||||||
type channelStatsRequest struct {
|
type channelStatsRequest struct {
|
||||||
Days int `form:"days"`
|
Days int `form:"days"`
|
||||||
StartDate string `form:"start_date"`
|
|
||||||
EndDate string `form:"end_date"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelStats 渠道数据分析
|
// ChannelStats 渠道数据分析
|
||||||
@ -60,7 +58,7 @@ func (h *handler) ChannelStats() core.HandlerFunc {
|
|||||||
idStr := ctx.Param("channel_id")
|
idStr := ctx.Param("channel_id")
|
||||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
id, _ := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
|
||||||
stats, err := h.channel.GetStats(ctx.RequestContext(), id, req.Days, req.StartDate, req.EndDate)
|
stats, err := h.channel.GetStats(ctx.RequestContext(), id, req.Days)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,681 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bindbox-game/internal/code"
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/pkg/validation"
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type activityProfitLossRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
Name string `form:"name"`
|
|
||||||
Status int32 `form:"status"` // 1进行中 2下线
|
|
||||||
SortBy string `form:"sort_by"` // profit, profit_asc, profit_rate, draw_count
|
|
||||||
}
|
|
||||||
|
|
||||||
type activityProfitLossItem struct {
|
|
||||||
ActivityID int64 `json:"activity_id"`
|
|
||||||
ActivityName string `json:"activity_name"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
DrawCount int64 `json:"draw_count"`
|
|
||||||
GamePassCount int64 `json:"game_pass_count"` // 次卡抽奖次数
|
|
||||||
PaymentCount int64 `json:"payment_count"` // 现金/优惠券抽奖次数
|
|
||||||
RefundCount int64 `json:"refund_count"` // 退款/取消抽奖次数
|
|
||||||
PlayerCount int64 `json:"player_count"`
|
|
||||||
TotalRevenue int64 `json:"total_revenue"` // 实际支付金额 (分)
|
|
||||||
TotalDiscount int64 `json:"total_discount"` // 优惠券抵扣金额 (分)
|
|
||||||
TotalGamePassValue int64 `json:"total_game_pass_value"` // 次卡价值 (分)
|
|
||||||
TotalCost int64 `json:"total_cost"` // 奖品标价总和 (分)
|
|
||||||
Profit int64 `json:"profit"` // (Revenue + Discount + GamePassValue) - Cost
|
|
||||||
ProfitRate float64 `json:"profit_rate"` // Profit / (Revenue + Discount + GamePassValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
type activityProfitLossResponse struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []activityProfitLossItem `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) DashboardActivityProfitLoss() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(activityProfitLossRequest)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
|
||||||
|
|
||||||
// 1. 获取活动列表基础信息
|
|
||||||
// 1. 获取活动列表基础信息
|
|
||||||
var activities []model.Activities
|
|
||||||
// 仅查询有完整配置(Issue->RewardSettings)且未删除的活动
|
|
||||||
// 使用 Raw SQL 避免 GORM 自动注入 ambiguous 的 deleted_at
|
|
||||||
rawSubQuery := fmt.Sprintf(`
|
|
||||||
SELECT activity_issues.activity_id
|
|
||||||
FROM %s AS activity_issues
|
|
||||||
JOIN %s AS activity_reward_settings ON activity_reward_settings.issue_id = activity_issues.id
|
|
||||||
WHERE activity_issues.deleted_at IS NULL
|
|
||||||
AND activity_reward_settings.deleted_at IS NULL
|
|
||||||
`, model.TableNameActivityIssues, model.TableNameActivityRewardSettings)
|
|
||||||
|
|
||||||
query := db.Table(model.TableNameActivities).
|
|
||||||
Where("activities.deleted_at IS NULL").
|
|
||||||
Where(fmt.Sprintf("activities.id IN (%s)", rawSubQuery))
|
|
||||||
|
|
||||||
if req.Name != "" {
|
|
||||||
query = query.Where("activities.name LIKE ?", "%"+req.Name+"%")
|
|
||||||
}
|
|
||||||
if req.Status > 0 {
|
|
||||||
query = query.Where("activities.status = ?", req.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
query.Count(&total)
|
|
||||||
|
|
||||||
// 如果有排序需求,先获取所有活动计算盈亏后排序,再分页
|
|
||||||
// 如果没有排序需求,直接数据库分页
|
|
||||||
needCustomSort := req.SortBy != ""
|
|
||||||
var limitQuery = query
|
|
||||||
if !needCustomSort {
|
|
||||||
limitQuery = query.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := limitQuery.Order("id DESC").Find(&activities).Error; err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("GetActivityProfitLoss activities error: %v", err))
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21021, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(activities) == 0 {
|
|
||||||
ctx.Payload(&activityProfitLossResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: total,
|
|
||||||
List: []activityProfitLossItem{},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activityIDs := make([]int64, len(activities))
|
|
||||||
activityMap := make(map[int64]*activityProfitLossItem)
|
|
||||||
for i, a := range activities {
|
|
||||||
activityIDs[i] = a.ID
|
|
||||||
activityMap[a.ID] = &activityProfitLossItem{
|
|
||||||
ActivityID: a.ID,
|
|
||||||
ActivityName: a.Name,
|
|
||||||
Status: a.Status,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 统计抽奖次数和人数 (通过 activity_draw_logs 关联 activity_issues 和 orders)
|
|
||||||
type drawStat struct {
|
|
||||||
ActivityID int64
|
|
||||||
TotalCount int64
|
|
||||||
GamePassCount int64
|
|
||||||
PaymentCount int64
|
|
||||||
RefundCount int64
|
|
||||||
PlayerCount int64
|
|
||||||
}
|
|
||||||
var drawStats []drawStat
|
|
||||||
db.Table(model.TableNameActivityDrawLogs).
|
|
||||||
Select(`
|
|
||||||
activity_issues.activity_id,
|
|
||||||
COUNT(activity_draw_logs.id) as total_count,
|
|
||||||
SUM(CASE WHEN orders.status = 2 AND (orders.source_type = 4 OR orders.order_no LIKE 'GP%') THEN 1 ELSE 0 END) as game_pass_count,
|
|
||||||
SUM(CASE WHEN orders.status = 2 AND NOT (orders.source_type = 4 OR orders.order_no LIKE 'GP%') THEN 1 ELSE 0 END) as payment_count,
|
|
||||||
SUM(CASE WHEN orders.status IN (3, 4) THEN 1 ELSE 0 END) as refund_count,
|
|
||||||
COUNT(DISTINCT CASE WHEN orders.status = 2 THEN activity_draw_logs.user_id END) as player_count
|
|
||||||
`).
|
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
|
||||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
|
||||||
Where("activity_issues.activity_id IN ?", activityIDs).
|
|
||||||
Group("activity_issues.activity_id").
|
|
||||||
Scan(&drawStats)
|
|
||||||
|
|
||||||
for _, s := range drawStats {
|
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
|
||||||
item.DrawCount = s.GamePassCount + s.PaymentCount // 仅统计有效抽奖(次卡+支付)
|
|
||||||
item.GamePassCount = s.GamePassCount
|
|
||||||
item.PaymentCount = s.PaymentCount
|
|
||||||
item.RefundCount = s.RefundCount
|
|
||||||
item.PlayerCount = s.PlayerCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
|
||||||
// 3. 统计营收和优惠券抵扣 (通过 orders 关联 activity_draw_logs)
|
|
||||||
// BUG修复:排除已退款订单(status=4)。
|
|
||||||
// 注意: MySQL SUM()运算涉及除法时会返回Decimal类型,需要Scan到float64
|
|
||||||
type revenueStat struct {
|
|
||||||
ActivityID int64
|
|
||||||
TotalRevenue float64
|
|
||||||
TotalDiscount float64
|
|
||||||
}
|
|
||||||
var revenueStats []revenueStat
|
|
||||||
|
|
||||||
// 修正: 按抽奖次数比例分摊订单金额 (解决多活动订单归因问题)
|
|
||||||
// 逻辑: 活动分摊收入 = 订单实际金额 * (该活动在该订单中的抽奖次数 / 该订单总抽奖次数)
|
|
||||||
var err error
|
|
||||||
err = db.Table(model.TableNameOrders).
|
|
||||||
Select(`
|
|
||||||
order_activity_draws.activity_id,
|
|
||||||
SUM(1.0 * orders.actual_amount * order_activity_draws.draw_count / order_total_draws.total_count) as total_revenue,
|
|
||||||
SUM(CASE WHEN orders.source_type = 4 OR orders.order_no LIKE 'GP%' THEN 0 ELSE 1.0 * orders.discount_amount * order_activity_draws.draw_count / order_total_draws.total_count END) as total_discount
|
|
||||||
`).
|
|
||||||
// Subquery 1: Calculate draw counts per order per activity (and link to issue->activity)
|
|
||||||
Joins(`JOIN (
|
|
||||||
SELECT activity_draw_logs.order_id, activity_issues.activity_id, COUNT(*) as draw_count
|
|
||||||
FROM activity_draw_logs
|
|
||||||
JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id
|
|
||||||
GROUP BY activity_draw_logs.order_id, activity_issues.activity_id
|
|
||||||
) as order_activity_draws ON order_activity_draws.order_id = orders.id`).
|
|
||||||
// Subquery 2: Calculate total draw counts per order
|
|
||||||
Joins(`JOIN (
|
|
||||||
SELECT order_id, COUNT(*) as total_count
|
|
||||||
FROM activity_draw_logs
|
|
||||||
GROUP BY order_id
|
|
||||||
) as order_total_draws ON order_total_draws.order_id = orders.id`).
|
|
||||||
Where("orders.status = ?", 2). // 已支付(排除待支付、取消、退款状态)
|
|
||||||
Where("order_activity_draws.activity_id IN ?", activityIDs).
|
|
||||||
Group("order_activity_draws.activity_id").
|
|
||||||
Scan(&revenueStats).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("GetActivityProfitLoss revenue stats error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range revenueStats {
|
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
|
||||||
item.TotalRevenue = int64(s.TotalRevenue)
|
|
||||||
item.TotalDiscount = int64(s.TotalDiscount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 统计成本 (通过 user_inventory 关联 products 和 orders)
|
|
||||||
// 修正:增加关联 orders 表,过滤掉已退款/取消的订单 (status!=2)
|
|
||||||
type costStat struct {
|
|
||||||
ActivityID int64
|
|
||||||
TotalCost int64
|
|
||||||
}
|
|
||||||
var costStats []costStat
|
|
||||||
db.Table(model.TableNameUserInventory).
|
|
||||||
Select("user_inventory.activity_id, SUM(products.price) as total_cost").
|
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
|
||||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
|
||||||
Where("user_inventory.activity_id IN ?", activityIDs).
|
|
||||||
Where("orders.status = ?", 2). // 仅统计已支付订单产生的成本
|
|
||||||
Group("user_inventory.activity_id").
|
|
||||||
Scan(&costStats)
|
|
||||||
|
|
||||||
for _, s := range costStats {
|
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
|
||||||
item.TotalCost = s.TotalCost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 统计次卡价值 (0元订单按活动单价计算)
|
|
||||||
// 先获取各活动的单价
|
|
||||||
activityPriceMap := make(map[int64]int64)
|
|
||||||
for _, a := range activities {
|
|
||||||
activityPriceMap[a.ID] = a.PriceDraw
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计每个活动的0元订单对应的抽奖次数 (次卡支付)
|
|
||||||
// BUG修复:之前统计的是订单数量,但一个订单可能包含多次抽奖
|
|
||||||
// 正确做法是统计抽奖次数,再乘以活动单价
|
|
||||||
type gamePassStat struct {
|
|
||||||
ActivityID int64
|
|
||||||
GamePassDraws int64 // 抽奖次数,非订单数
|
|
||||||
}
|
|
||||||
var gamePassStats []gamePassStat
|
|
||||||
db.Table(model.TableNameActivityDrawLogs).
|
|
||||||
Select("activity_issues.activity_id, COUNT(activity_draw_logs.id) as game_pass_draws").
|
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
|
||||||
Joins("JOIN orders ON orders.id = activity_draw_logs.order_id").
|
|
||||||
Where("orders.status = ? AND orders.status != ?", 2, 4). // 已支付且未退款
|
|
||||||
Where("orders.actual_amount = 0"). // 0元订单
|
|
||||||
Where("orders.source_type = 4 OR orders.order_no LIKE 'GP%'"). // 次数卡 (Lottery SourceType=4 OR Matching Game GP prefix)
|
|
||||||
Where("activity_issues.activity_id IN ?", activityIDs).
|
|
||||||
Group("activity_issues.activity_id").
|
|
||||||
Scan(&gamePassStats)
|
|
||||||
|
|
||||||
for _, s := range gamePassStats {
|
|
||||||
if item, ok := activityMap[s.ActivityID]; ok {
|
|
||||||
// 次卡价值 = 次卡抽奖次数 * 活动单价
|
|
||||||
item.TotalGamePassValue = s.GamePassDraws * activityPriceMap[s.ActivityID]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 计算盈亏和比率
|
|
||||||
// 公式: 盈亏 = (支付金额 + 优惠券抵扣 + 次卡价值) - 产品成本
|
|
||||||
finalList := make([]activityProfitLossItem, 0, len(activities))
|
|
||||||
for _, a := range activities {
|
|
||||||
item := activityMap[a.ID]
|
|
||||||
totalIncome := item.TotalRevenue + item.TotalDiscount + item.TotalGamePassValue
|
|
||||||
item.Profit = totalIncome - item.TotalCost
|
|
||||||
if totalIncome > 0 {
|
|
||||||
item.ProfitRate = float64(item.Profit) / float64(totalIncome)
|
|
||||||
}
|
|
||||||
finalList = append(finalList, *item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按请求的字段排序
|
|
||||||
if needCustomSort {
|
|
||||||
sort.Slice(finalList, func(i, j int) bool {
|
|
||||||
switch req.SortBy {
|
|
||||||
case "profit":
|
|
||||||
return finalList[i].Profit > finalList[j].Profit
|
|
||||||
case "profit_asc":
|
|
||||||
return finalList[i].Profit < finalList[j].Profit
|
|
||||||
case "profit_rate":
|
|
||||||
return finalList[i].ProfitRate > finalList[j].ProfitRate
|
|
||||||
case "draw_count":
|
|
||||||
return finalList[i].DrawCount > finalList[j].DrawCount
|
|
||||||
default:
|
|
||||||
return false // 保持原有顺序 (id DESC)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 排序后再分页
|
|
||||||
start := (req.Page - 1) * req.PageSize
|
|
||||||
end := start + req.PageSize
|
|
||||||
if start > len(finalList) {
|
|
||||||
start = len(finalList)
|
|
||||||
}
|
|
||||||
if end > len(finalList) {
|
|
||||||
end = len(finalList)
|
|
||||||
}
|
|
||||||
finalList = finalList[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&activityProfitLossResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: total,
|
|
||||||
List: finalList,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type activityLogsRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type activityLogItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
ProductID int64 `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ProductImage string `json:"product_image"`
|
|
||||||
ProductPrice int64 `json:"product_price"`
|
|
||||||
ProductQuantity int64 `json:"product_quantity"` // 奖品数量
|
|
||||||
OrderAmount int64 `json:"order_amount"`
|
|
||||||
OrderNo string `json:"order_no"` // 订单号
|
|
||||||
DiscountAmount int64 `json:"discount_amount"` // 优惠金额(分)
|
|
||||||
PayType string `json:"pay_type"` // 支付方式/类型 (现金/道具卡/次数卡)
|
|
||||||
UsedCard string `json:"used_card"` // 使用的卡券名称(兼容旧字段)
|
|
||||||
OrderStatus int32 `json:"order_status"` // 订单状态: 1待支付 2已支付 3已取消 4已退款
|
|
||||||
Profit int64 `json:"profit"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
// 新增:详细支付信息
|
|
||||||
PaymentDetails PaymentDetails `json:"payment_details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PaymentDetails 支付详细信息
|
|
||||||
type PaymentDetails struct {
|
|
||||||
CouponUsed bool `json:"coupon_used"` // 是否使用优惠券
|
|
||||||
CouponName string `json:"coupon_name"` // 优惠券名称
|
|
||||||
CouponDiscount int64 `json:"coupon_discount"` // 优惠券抵扣金额(分)
|
|
||||||
ItemCardUsed bool `json:"item_card_used"` // 是否使用道具卡
|
|
||||||
ItemCardName string `json:"item_card_name"` // 道具卡名称
|
|
||||||
GamePassUsed bool `json:"game_pass_used"` // 是否使用次数卡
|
|
||||||
GamePassInfo string `json:"game_pass_info"` // 次数卡使用信息
|
|
||||||
PointsUsed bool `json:"points_used"` // 是否使用积分
|
|
||||||
PointsDiscount int64 `json:"points_discount"` // 积分抵扣金额(分)
|
|
||||||
}
|
|
||||||
|
|
||||||
type activityLogsResponse struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []activityLogItem `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) DashboardActivityLogs() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
activityID, _ := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
|
|
||||||
if activityID <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "Invalid activity ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(activityLogsRequest)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
db.Table(model.TableNameActivityDrawLogs).
|
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
|
||||||
Where("activity_issues.activity_id = ?", activityID).
|
|
||||||
Count(&total)
|
|
||||||
|
|
||||||
var logs []struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
Nickname string
|
|
||||||
Avatar string
|
|
||||||
ProductID int64
|
|
||||||
ProductName string
|
|
||||||
ImagesJSON string
|
|
||||||
ProductPrice int64
|
|
||||||
OrderAmount int64
|
|
||||||
DiscountAmount int64
|
|
||||||
PointsAmount int64 // 积分抵扣金额
|
|
||||||
OrderStatus int32 // 订单状态
|
|
||||||
SourceType int32
|
|
||||||
CouponID int64
|
|
||||||
CouponName string
|
|
||||||
ItemCardID int64
|
|
||||||
ItemCardName string
|
|
||||||
EffectType int32
|
|
||||||
Multiplier int32
|
|
||||||
OrderRemark string // BUG修复:增加remark字段用于解析次数卡使用信息
|
|
||||||
OrderNo string // 订单号
|
|
||||||
DrawCount int64 // 该订单的总抽奖次数(用于金额分摊)
|
|
||||||
UsedDrawLogID int64 // 道具卡实际使用的日志ID
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
err := db.Table(model.TableNameActivityDrawLogs).
|
|
||||||
Select(`
|
|
||||||
activity_draw_logs.id,
|
|
||||||
activity_draw_logs.user_id,
|
|
||||||
COALESCE(users.nickname, '') as nickname,
|
|
||||||
COALESCE(users.avatar, '') as avatar,
|
|
||||||
activity_reward_settings.product_id,
|
|
||||||
COALESCE(products.name, '') as product_name,
|
|
||||||
COALESCE(products.images_json, '[]') as images_json,
|
|
||||||
COALESCE(products.price, 0) as product_price,
|
|
||||||
COALESCE(orders.actual_amount, 0) as order_amount,
|
|
||||||
COALESCE(orders.discount_amount, 0) as discount_amount,
|
|
||||||
COALESCE(orders.points_amount, 0) as points_amount,
|
|
||||||
COALESCE(orders.status, 0) as order_status,
|
|
||||||
orders.source_type,
|
|
||||||
COALESCE(orders.coupon_id, 0) as coupon_id,
|
|
||||||
COALESCE(system_coupons.name, '') as coupon_name,
|
|
||||||
COALESCE(orders.item_card_id, 0) as item_card_id,
|
|
||||||
COALESCE(system_item_cards.name, '') as item_card_name,
|
|
||||||
COALESCE(system_item_cards.effect_type, 0) as effect_type,
|
|
||||||
COALESCE(system_item_cards.reward_multiplier_x1000, 0) as multiplier,
|
|
||||||
COALESCE(orders.remark, '') as order_remark,
|
|
||||||
COALESCE(orders.order_no, '') as order_no,
|
|
||||||
COALESCE(order_draw_counts.draw_count, 1) as draw_count,
|
|
||||||
COALESCE(user_item_cards.used_draw_log_id, 0) as used_draw_log_id,
|
|
||||||
activity_draw_logs.created_at
|
|
||||||
`).
|
|
||||||
Joins("JOIN activity_issues ON activity_issues.id = activity_draw_logs.issue_id").
|
|
||||||
Joins("LEFT JOIN users ON users.id = activity_draw_logs.user_id").
|
|
||||||
Joins("LEFT JOIN activity_reward_settings ON activity_reward_settings.id = activity_draw_logs.reward_id").
|
|
||||||
Joins("LEFT JOIN products ON products.id = activity_reward_settings.product_id").
|
|
||||||
Joins("LEFT JOIN orders ON orders.id = activity_draw_logs.order_id").
|
|
||||||
Joins("LEFT JOIN user_coupons ON user_coupons.id = orders.coupon_id").
|
|
||||||
Joins("LEFT JOIN system_coupons ON system_coupons.id = user_coupons.coupon_id").
|
|
||||||
Joins("LEFT JOIN user_item_cards ON user_item_cards.id = orders.item_card_id").
|
|
||||||
Joins("LEFT JOIN system_item_cards ON system_item_cards.id = user_item_cards.card_id").
|
|
||||||
Joins("LEFT JOIN (SELECT order_id, COUNT(*) as draw_count FROM activity_draw_logs GROUP BY order_id) as order_draw_counts ON order_draw_counts.order_id = activity_draw_logs.order_id").
|
|
||||||
Where("activity_issues.activity_id = ?", activityID).
|
|
||||||
Order("activity_draw_logs.id DESC").
|
|
||||||
Offset((req.Page - 1) * req.PageSize).
|
|
||||||
Limit(req.PageSize).
|
|
||||||
Scan(&logs).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("GetActivityLogs error: %v", err))
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21022, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
list := make([]activityLogItem, len(logs))
|
|
||||||
for i, l := range logs {
|
|
||||||
var images []string
|
|
||||||
_ = json.Unmarshal([]byte(l.ImagesJSON), &images)
|
|
||||||
productImage := ""
|
|
||||||
if len(images) > 0 {
|
|
||||||
productImage = images[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default quantity is 1
|
|
||||||
quantity := int64(1)
|
|
||||||
|
|
||||||
// Determine PayType and UsedCard + PaymentDetails
|
|
||||||
payType := "现金支付"
|
|
||||||
usedCard := ""
|
|
||||||
paymentDetails := PaymentDetails{} // 金额将在 drawCount 计算后设置
|
|
||||||
|
|
||||||
// 检查是否使用了优惠券
|
|
||||||
if l.CouponID > 0 || l.CouponName != "" {
|
|
||||||
paymentDetails.CouponUsed = true
|
|
||||||
paymentDetails.CouponName = l.CouponName
|
|
||||||
if paymentDetails.CouponName == "" {
|
|
||||||
paymentDetails.CouponName = "优惠券"
|
|
||||||
}
|
|
||||||
usedCard = paymentDetails.CouponName
|
|
||||||
payType = "优惠券"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否使用了道具卡
|
|
||||||
// BUG FIX: 仅当该条日志的 ID 等于 item_card 记录的 used_draw_log_id 时,才显示道具卡信息
|
|
||||||
// 防止一个订单下的所有抽奖记录都显示 "双倍快乐水"
|
|
||||||
isCardValidForThisLog := (l.UsedDrawLogID == 0) || (l.UsedDrawLogID == l.ID)
|
|
||||||
|
|
||||||
if (l.ItemCardID > 0 || l.ItemCardName != "") && isCardValidForThisLog {
|
|
||||||
paymentDetails.ItemCardUsed = true
|
|
||||||
paymentDetails.ItemCardName = l.ItemCardName
|
|
||||||
if paymentDetails.ItemCardName == "" {
|
|
||||||
paymentDetails.ItemCardName = "道具卡"
|
|
||||||
}
|
|
||||||
if usedCard != "" {
|
|
||||||
usedCard = usedCard + " + " + paymentDetails.ItemCardName
|
|
||||||
} else {
|
|
||||||
usedCard = paymentDetails.ItemCardName
|
|
||||||
}
|
|
||||||
payType = "道具卡"
|
|
||||||
|
|
||||||
// 计算双倍/多倍卡数量
|
|
||||||
if l.EffectType == 1 && l.Multiplier > 1000 {
|
|
||||||
quantity = quantity * int64(l.Multiplier) / 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否使用了次数卡 (source_type=4 或 remark包含use_game_pass)
|
|
||||||
if l.SourceType == 4 || strings.Contains(l.OrderRemark, "use_game_pass") {
|
|
||||||
paymentDetails.GamePassUsed = true
|
|
||||||
// 解析 gp_use:ID:Count 格式获取次数卡使用信息
|
|
||||||
gamePassInfo := "次数卡"
|
|
||||||
if strings.Contains(l.OrderRemark, "gp_use:") {
|
|
||||||
// 从remark中提取次数卡信息,格式: use_game_pass;gp_use:ID:Count;gp_use:ID:Count
|
|
||||||
parts := strings.Split(l.OrderRemark, ";")
|
|
||||||
var gpParts []string
|
|
||||||
for _, p := range parts {
|
|
||||||
if strings.HasPrefix(p, "gp_use:") {
|
|
||||||
gpParts = append(gpParts, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(gpParts) > 0 {
|
|
||||||
gamePassInfo = fmt.Sprintf("使用%d种次数卡", len(gpParts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paymentDetails.GamePassInfo = gamePassInfo
|
|
||||||
if usedCard != "" {
|
|
||||||
usedCard = usedCard + " + " + gamePassInfo
|
|
||||||
} else {
|
|
||||||
usedCard = gamePassInfo
|
|
||||||
}
|
|
||||||
payType = "次数卡"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否使用了积分
|
|
||||||
if l.PointsAmount > 0 {
|
|
||||||
paymentDetails.PointsUsed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果同时使用了多种方式,标记为组合支付
|
|
||||||
usedCount := 0
|
|
||||||
if paymentDetails.CouponUsed {
|
|
||||||
usedCount++
|
|
||||||
}
|
|
||||||
if paymentDetails.ItemCardUsed {
|
|
||||||
usedCount++
|
|
||||||
}
|
|
||||||
if paymentDetails.GamePassUsed {
|
|
||||||
usedCount++
|
|
||||||
}
|
|
||||||
if usedCount > 1 {
|
|
||||||
payType = "组合支付"
|
|
||||||
} else if usedCount == 0 && l.OrderAmount > 0 {
|
|
||||||
payType = "现金支付"
|
|
||||||
} else if usedCount == 0 && l.OrderAmount == 0 {
|
|
||||||
// 0元支付默认视为次数卡使用(实际业务中几乎不存在真正免费的情况)
|
|
||||||
payType = "次数卡"
|
|
||||||
paymentDetails.GamePassUsed = true
|
|
||||||
if paymentDetails.GamePassInfo == "" {
|
|
||||||
paymentDetails.GamePassInfo = "次数卡"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算单次抽奖的分摊金额(一个订单可能包含多次抽奖)
|
|
||||||
drawCount := l.DrawCount
|
|
||||||
if drawCount <= 0 {
|
|
||||||
drawCount = 1
|
|
||||||
}
|
|
||||||
perDrawOrderAmount := l.OrderAmount / drawCount
|
|
||||||
perDrawDiscountAmount := l.DiscountAmount / drawCount
|
|
||||||
perDrawPointsAmount := l.PointsAmount / drawCount
|
|
||||||
|
|
||||||
// 设置支付详情中的分摊金额
|
|
||||||
paymentDetails.CouponDiscount = perDrawDiscountAmount
|
|
||||||
paymentDetails.PointsDiscount = perDrawPointsAmount
|
|
||||||
|
|
||||||
list[i] = activityLogItem{
|
|
||||||
ID: l.ID,
|
|
||||||
UserID: l.UserID,
|
|
||||||
Nickname: l.Nickname,
|
|
||||||
Avatar: l.Avatar,
|
|
||||||
ProductID: l.ProductID,
|
|
||||||
ProductName: l.ProductName,
|
|
||||||
ProductImage: productImage,
|
|
||||||
ProductPrice: l.ProductPrice,
|
|
||||||
ProductQuantity: quantity,
|
|
||||||
OrderAmount: perDrawOrderAmount, // 单次抽奖分摊的支付金额
|
|
||||||
OrderNo: l.OrderNo, // 订单号
|
|
||||||
DiscountAmount: perDrawDiscountAmount, // 单次抽奖分摊的优惠金额
|
|
||||||
PayType: payType,
|
|
||||||
UsedCard: usedCard,
|
|
||||||
OrderStatus: l.OrderStatus,
|
|
||||||
Profit: perDrawOrderAmount + perDrawDiscountAmount - l.ProductPrice*quantity, // 单次盈亏 = 分摊收入 - 成本*数量
|
|
||||||
CreatedAt: l.CreatedAt,
|
|
||||||
PaymentDetails: paymentDetails,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&activityLogsResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: total,
|
|
||||||
List: list,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ensureActivityProfitLossMenuResponse struct {
|
|
||||||
Ensured bool `json:"ensured"`
|
|
||||||
Parent int64 `json:"parent_id"`
|
|
||||||
MenuID int64 `json:"menu_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureActivityProfitLossMenu 确保运营分析下存在“活动盈亏”菜单
|
|
||||||
func (h *handler) EnsureActivityProfitLossMenu() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
// 1. 查找是否存在“控制台”或者“运营中心”类的父菜单
|
|
||||||
// 很多系统会将概览放在 Dashboard 下。根据 titles_seed.go,运营是 Operations。
|
|
||||||
parent, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Operations")).First()
|
|
||||||
var parentID int64
|
|
||||||
if parent == nil {
|
|
||||||
// 如果没有 Operations,尝试查找 Dashboard
|
|
||||||
parent, _ = h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Name.Eq("Dashboard")).First()
|
|
||||||
}
|
|
||||||
|
|
||||||
if parent != nil {
|
|
||||||
parentID = parent.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 查找活动盈亏菜单
|
|
||||||
// 路径指向控制台并带上查参数
|
|
||||||
menuPath := "/dashboard/console?tab=activity-profit"
|
|
||||||
exists, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq(menuPath)).First()
|
|
||||||
if exists != nil {
|
|
||||||
ctx.Payload(&ensureActivityProfitLossMenuResponse{Ensured: true, Parent: parentID, MenuID: exists.ID})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 创建菜单
|
|
||||||
newMenu := &model.Menus{
|
|
||||||
ParentID: parentID,
|
|
||||||
Path: menuPath,
|
|
||||||
Name: "活动盈亏",
|
|
||||||
Component: "/dashboard/console/index",
|
|
||||||
Icon: "ri:pie-chart-2-fill",
|
|
||||||
Sort: 60, // 排序在称号之后
|
|
||||||
Status: true,
|
|
||||||
KeepAlive: true,
|
|
||||||
IsHide: false,
|
|
||||||
IsHideTab: false,
|
|
||||||
CreatedUser: "system",
|
|
||||||
UpdatedUser: "system",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.writeDB.Menus.WithContext(ctx.RequestContext()).Create(newMenu); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 21023, "创建菜单失败: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取新创建的 ID
|
|
||||||
created, _ := h.readDB.Menus.WithContext(ctx.RequestContext()).Where(h.readDB.Menus.Path.Eq(menuPath)).First()
|
|
||||||
menuID := int64(0)
|
|
||||||
if created != nil {
|
|
||||||
menuID = created.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&ensureActivityProfitLossMenuResponse{Ensured: true, Parent: parentID, MenuID: menuID})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,344 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bindbox-game/internal/code"
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/pkg/validation"
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type spendingLeaderboardRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
RangeType string `form:"rangeType"` // today, 7d, 30d, custom
|
|
||||||
StartDate string `form:"start"`
|
|
||||||
EndDate string `form:"end"`
|
|
||||||
SortBy string `form:"sort_by"` // spending, profit
|
|
||||||
}
|
|
||||||
|
|
||||||
type spendingLeaderboardItem struct {
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
OrderCount int64 `json:"-"` // Hidden
|
|
||||||
TotalSpending int64 `json:"-"` // Hidden
|
|
||||||
TotalPrizeValue int64 `json:"-"` // Hidden
|
|
||||||
TotalDiscount int64 `json:"total_discount"` // Total Coupon Discount (Fen)
|
|
||||||
TotalPoints int64 `json:"total_points"` // Total Points Discount (Fen)
|
|
||||||
GamePassCount int64 `json:"game_pass_count"` // Count of SourceType=4
|
|
||||||
ItemCardCount int64 `json:"item_card_count"` // Count where ItemCardID > 0
|
|
||||||
// Breakdown by game type
|
|
||||||
IchibanSpending int64 `json:"ichiban_spending"`
|
|
||||||
IchibanPrize int64 `json:"ichiban_prize"`
|
|
||||||
IchibanProfit int64 `json:"ichiban_profit"`
|
|
||||||
IchibanCount int64 `json:"ichiban_count"`
|
|
||||||
InfiniteSpending int64 `json:"infinite_spending"`
|
|
||||||
InfinitePrize int64 `json:"infinite_prize"`
|
|
||||||
InfiniteProfit int64 `json:"infinite_profit"`
|
|
||||||
InfiniteCount int64 `json:"infinite_count"`
|
|
||||||
MatchingSpending int64 `json:"matching_spending"`
|
|
||||||
MatchingPrize int64 `json:"matching_prize"`
|
|
||||||
MatchingProfit int64 `json:"matching_profit"`
|
|
||||||
MatchingCount int64 `json:"matching_count"`
|
|
||||||
// 直播间统计 (source_type=5)
|
|
||||||
LivestreamSpending int64 `json:"livestream_spending"`
|
|
||||||
LivestreamPrize int64 `json:"livestream_prize"`
|
|
||||||
LivestreamProfit int64 `json:"livestream_profit"`
|
|
||||||
LivestreamCount int64 `json:"livestream_count"`
|
|
||||||
|
|
||||||
Profit int64 `json:"profit"` // Spending - PrizeValue
|
|
||||||
ProfitRate float64 `json:"profit_rate"` // Profit / Spending
|
|
||||||
}
|
|
||||||
|
|
||||||
type spendingLeaderboardResponse struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []spendingLeaderboardItem `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) DashboardPlayerSpendingLeaderboard() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(spendingLeaderboardRequest)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var start, end time.Time
|
|
||||||
if req.RangeType != "all" {
|
|
||||||
start, end = parseRange(req.RangeType, req.StartDate, req.EndDate)
|
|
||||||
h.logger.Info(fmt.Sprintf("SpendingLeaderboard range: start=%v, end=%v, type=%s", start, end, req.RangeType))
|
|
||||||
} else {
|
|
||||||
h.logger.Info("SpendingLeaderboard range: ALL TIME")
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
|
||||||
|
|
||||||
// 1. Get Top Spenders from Orders
|
|
||||||
type orderStat struct {
|
|
||||||
UserID int64
|
|
||||||
TotalAmount int64 // ActualAmount
|
|
||||||
OrderCount int64
|
|
||||||
TotalDiscount int64
|
|
||||||
TotalPoints int64
|
|
||||||
GamePassCount int64
|
|
||||||
ItemCardCount int64
|
|
||||||
IchibanSpending int64
|
|
||||||
IchibanCount int64
|
|
||||||
InfiniteSpending int64
|
|
||||||
InfiniteCount int64
|
|
||||||
MatchingSpending int64
|
|
||||||
MatchingCount int64
|
|
||||||
LivestreamSpending int64
|
|
||||||
LivestreamCount int64
|
|
||||||
}
|
|
||||||
var stats []orderStat
|
|
||||||
|
|
||||||
query := db.Table(model.TableNameOrders).
|
|
||||||
Joins("LEFT JOIN (SELECT l.order_id, MAX(a.activity_category_id) as category_id FROM activity_draw_logs l JOIN activity_issues i ON i.id = l.issue_id JOIN activities a ON a.id = i.activity_id GROUP BY l.order_id) oa ON oa.order_id = orders.id").
|
|
||||||
Where("orders.status = ?", 2)
|
|
||||||
|
|
||||||
if req.RangeType != "all" {
|
|
||||||
query = query.Where("orders.created_at >= ?", start).Where("orders.created_at <= ?", end)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Select(`
|
|
||||||
orders.user_id,
|
|
||||||
SUM(orders.total_amount) as total_amount,
|
|
||||||
COUNT(orders.id) as order_count,
|
|
||||||
SUM(orders.discount_amount) as total_discount,
|
|
||||||
SUM(orders.points_amount) as total_points,
|
|
||||||
SUM(CASE WHEN orders.source_type = 4 THEN 1 ELSE 0 END) as game_pass_count,
|
|
||||||
SUM(CASE WHEN orders.item_card_id > 0 THEN 1 ELSE 0 END) as item_card_count,
|
|
||||||
SUM(CASE WHEN oa.category_id = 1 THEN orders.total_amount ELSE 0 END) as ichiban_spending,
|
|
||||||
SUM(CASE WHEN oa.category_id = 1 THEN 1 ELSE 0 END) as ichiban_count,
|
|
||||||
SUM(CASE WHEN oa.category_id = 2 THEN orders.total_amount ELSE 0 END) as infinite_spending,
|
|
||||||
SUM(CASE WHEN oa.category_id = 2 THEN 1 ELSE 0 END) as infinite_count,
|
|
||||||
SUM(CASE WHEN oa.category_id = 3 THEN orders.total_amount ELSE 0 END) as matching_spending,
|
|
||||||
SUM(CASE WHEN oa.category_id = 3 THEN 1 ELSE 0 END) as matching_count,
|
|
||||||
SUM(CASE WHEN orders.source_type = 5 THEN orders.total_amount ELSE 0 END) as livestream_spending,
|
|
||||||
SUM(CASE WHEN orders.source_type = 5 THEN 1 ELSE 0 END) as livestream_count
|
|
||||||
`).
|
|
||||||
Group("orders.user_id").
|
|
||||||
Order("total_amount DESC").
|
|
||||||
Limit(100).
|
|
||||||
Scan(&stats).Error; err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("SpendingLeaderboard SQL error: %v", err))
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 21020, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
h.logger.Info(fmt.Sprintf("SpendingLeaderboard SQL done: count=%d", len(stats)))
|
|
||||||
|
|
||||||
// 2. Collect User IDs
|
|
||||||
userIDs := make([]int64, 0, len(stats))
|
|
||||||
statMap := make(map[int64]*spendingLeaderboardItem)
|
|
||||||
for _, s := range stats {
|
|
||||||
userIDs = append(userIDs, s.UserID)
|
|
||||||
statMap[s.UserID] = &spendingLeaderboardItem{
|
|
||||||
UserID: s.UserID,
|
|
||||||
TotalSpending: s.TotalAmount,
|
|
||||||
OrderCount: s.OrderCount,
|
|
||||||
TotalDiscount: s.TotalDiscount,
|
|
||||||
TotalPoints: s.TotalPoints,
|
|
||||||
GamePassCount: s.GamePassCount,
|
|
||||||
ItemCardCount: s.ItemCardCount,
|
|
||||||
IchibanSpending: s.IchibanSpending,
|
|
||||||
IchibanCount: s.IchibanCount,
|
|
||||||
InfiniteSpending: s.InfiniteSpending,
|
|
||||||
InfiniteCount: s.InfiniteCount,
|
|
||||||
MatchingSpending: s.MatchingSpending,
|
|
||||||
MatchingCount: s.MatchingCount,
|
|
||||||
LivestreamSpending: 0, // Will be updated from douyin_orders
|
|
||||||
LivestreamCount: s.LivestreamCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2.1 Fetch Real Douyin Spending
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type dyStat struct {
|
|
||||||
UserID int64
|
|
||||||
Amount int64
|
|
||||||
Count int64
|
|
||||||
}
|
|
||||||
var dyStats []dyStat
|
|
||||||
dyQuery := h.repo.GetDbR().Table("douyin_orders").
|
|
||||||
Select("CAST(local_user_id AS SIGNED) as user_id, SUM(actual_pay_amount) as amount, COUNT(*) as count").
|
|
||||||
Where("local_user_id IN ?", userIDs).
|
|
||||||
Where("local_user_id != '' AND local_user_id != '0'")
|
|
||||||
|
|
||||||
if req.RangeType != "all" {
|
|
||||||
dyQuery = dyQuery.Where("created_at >= ?", start).Where("created_at <= ?", end)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dyQuery.Group("local_user_id").Scan(&dyStats).Error; err == nil {
|
|
||||||
for _, ds := range dyStats {
|
|
||||||
if item, ok := statMap[ds.UserID]; ok {
|
|
||||||
item.LivestreamSpending = ds.Amount
|
|
||||||
item.LivestreamCount = ds.Count // Use real paid order count
|
|
||||||
item.TotalSpending += ds.Amount // Add to total since orders.total_amount was 0 for these
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
// 3. Get User Info
|
|
||||||
// Use h.readDB.Users (GEN) as it's simple
|
|
||||||
users, _ := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Users.ID.In(userIDs...)).Find()
|
|
||||||
for _, u := range users {
|
|
||||||
if item, ok := statMap[u.ID]; ok {
|
|
||||||
item.Nickname = u.Nickname
|
|
||||||
item.Avatar = u.Avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Calculate Prize Value (Inventory)
|
|
||||||
type invStat struct {
|
|
||||||
UserID int64
|
|
||||||
TotalValue int64
|
|
||||||
IchibanPrize int64
|
|
||||||
InfinitePrize int64
|
|
||||||
MatchingPrize int64
|
|
||||||
LivestreamPrize int64
|
|
||||||
}
|
|
||||||
var invStats []invStat
|
|
||||||
|
|
||||||
// Join with Products, Activities, and Orders (for livestream detection)
|
|
||||||
query := db.Table(model.TableNameUserInventory).
|
|
||||||
Joins("JOIN products ON products.id = user_inventory.product_id").
|
|
||||||
Joins("LEFT JOIN activities ON activities.id = user_inventory.activity_id").
|
|
||||||
Joins("LEFT JOIN orders ON orders.id = user_inventory.order_id").
|
|
||||||
Where("user_inventory.user_id IN ?", userIDs)
|
|
||||||
|
|
||||||
if req.RangeType != "all" {
|
|
||||||
query = query.Where("user_inventory.created_at >= ?", start).
|
|
||||||
Where("user_inventory.created_at <= ?", end)
|
|
||||||
}
|
|
||||||
// Only include Holding (1) and Shipped/Used (3) items. Exclude Void/Decomposed (2).
|
|
||||||
query = query.Where("user_inventory.status IN ?", []int{1, 3}).
|
|
||||||
Where("user_inventory.remark NOT LIKE ?", "%void%")
|
|
||||||
|
|
||||||
err := query.Select(`
|
|
||||||
user_inventory.user_id,
|
|
||||||
SUM(products.price) as total_value,
|
|
||||||
SUM(CASE WHEN activities.activity_category_id = 1 THEN products.price ELSE 0 END) as ichiban_prize,
|
|
||||||
SUM(CASE WHEN activities.activity_category_id = 2 THEN products.price ELSE 0 END) as infinite_prize,
|
|
||||||
SUM(CASE WHEN activities.activity_category_id = 3 THEN products.price ELSE 0 END) as matching_prize
|
|
||||||
`).
|
|
||||||
Group("user_inventory.user_id").
|
|
||||||
Scan(&invStats).Error
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
for _, is := range invStats {
|
|
||||||
if item, ok := statMap[is.UserID]; ok {
|
|
||||||
item.TotalPrizeValue = is.TotalValue
|
|
||||||
item.IchibanPrize = is.IchibanPrize
|
|
||||||
item.InfinitePrize = is.InfinitePrize
|
|
||||||
item.MatchingPrize = is.MatchingPrize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4.1 Calculate Livestream Prize Value (From Draw Logs)
|
|
||||||
type lsStat struct {
|
|
||||||
UserID int64
|
|
||||||
Amount int64
|
|
||||||
}
|
|
||||||
var lsStats []lsStat
|
|
||||||
lsQuery := db.Table(model.TableNameLivestreamDrawLogs).
|
|
||||||
Joins("JOIN livestream_prizes ON livestream_prizes.id = livestream_draw_logs.prize_id").
|
|
||||||
Joins("JOIN products ON products.id = livestream_prizes.product_id").
|
|
||||||
Select("livestream_draw_logs.local_user_id as user_id, SUM(products.price) as amount").
|
|
||||||
Where("livestream_draw_logs.local_user_id IN ?", userIDs).
|
|
||||||
Where("livestream_draw_logs.is_refunded = 0")
|
|
||||||
|
|
||||||
if req.RangeType != "all" {
|
|
||||||
lsQuery = lsQuery.Where("livestream_draw_logs.created_at >= ?", start).
|
|
||||||
Where("livestream_draw_logs.created_at <= ?", end)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := lsQuery.Group("livestream_draw_logs.local_user_id").Scan(&lsStats).Error; err == nil {
|
|
||||||
for _, ls := range lsStats {
|
|
||||||
if item, ok := statMap[ls.UserID]; ok {
|
|
||||||
item.LivestreamPrize = ls.Amount
|
|
||||||
// item.TotalPrizeValue += ls.Amount // Already included in user_inventory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4.2 Calculate Profit for each category
|
|
||||||
for _, item := range statMap {
|
|
||||||
item.IchibanProfit = item.IchibanSpending - item.IchibanPrize
|
|
||||||
item.InfiniteProfit = item.InfiniteSpending - item.InfinitePrize
|
|
||||||
item.MatchingProfit = item.MatchingSpending - item.MatchingPrize
|
|
||||||
item.LivestreamProfit = item.LivestreamSpending - item.LivestreamPrize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Calculate Profit and Final List
|
|
||||||
list := make([]spendingLeaderboardItem, 0, len(statMap))
|
|
||||||
for _, item := range statMap {
|
|
||||||
// Calculate totals based on the 4 displayed categories to ensure UI consistency
|
|
||||||
calculatedSpending := item.IchibanSpending + item.InfiniteSpending + item.MatchingSpending + item.LivestreamSpending
|
|
||||||
calculatedProfit := item.IchibanProfit + item.InfiniteProfit + item.MatchingProfit + item.LivestreamProfit
|
|
||||||
|
|
||||||
item.Profit = calculatedProfit
|
|
||||||
if calculatedSpending > 0 {
|
|
||||||
item.ProfitRate = float64(item.Profit) / float64(calculatedSpending)
|
|
||||||
} else {
|
|
||||||
item.ProfitRate = 0
|
|
||||||
}
|
|
||||||
list = append(list, *item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Sort (in memory since we only have top N spenders)
|
|
||||||
sortBy := req.SortBy
|
|
||||||
if sortBy == "" {
|
|
||||||
sortBy = "spending"
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(list, func(i, j int) bool {
|
|
||||||
switch sortBy {
|
|
||||||
case "profit":
|
|
||||||
return list[i].Profit > list[j].Profit // Higher profit first
|
|
||||||
case "profit_asc":
|
|
||||||
return list[i].Profit < list[j].Profit // Lower profit (loss) first
|
|
||||||
default:
|
|
||||||
return list[i].TotalSpending > list[j].TotalSpending
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Pagination on the result list
|
|
||||||
startIdx := (req.Page - 1) * req.PageSize
|
|
||||||
if startIdx >= len(list) {
|
|
||||||
startIdx = len(list)
|
|
||||||
}
|
|
||||||
endIdx := startIdx + req.PageSize
|
|
||||||
if endIdx > len(list) {
|
|
||||||
endIdx = len(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
finalList := list[startIdx:endIdx]
|
|
||||||
if finalList == nil {
|
|
||||||
finalList = []spendingLeaderboardItem{}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&spendingLeaderboardResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: int64(len(list)), // Total of the fetched top batch
|
|
||||||
List: finalList,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -70,11 +70,9 @@ type douyinOrderItem struct {
|
|||||||
LocalUserID int64 `json:"local_user_id"`
|
LocalUserID int64 `json:"local_user_id"`
|
||||||
LocalUserNickname string `json:"local_user_nickname"`
|
LocalUserNickname string `json:"local_user_nickname"`
|
||||||
ActualReceiveAmount string `json:"actual_receive_amount"`
|
ActualReceiveAmount string `json:"actual_receive_amount"`
|
||||||
ActualPayAmount string `json:"actual_pay_amount"`
|
|
||||||
PayTypeDesc string `json:"pay_type_desc"`
|
PayTypeDesc string `json:"pay_type_desc"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
UserNickname string `json:"user_nickname"`
|
UserNickname string `json:"user_nickname"`
|
||||||
ProductCount int64 `json:"product_count"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,11 +129,9 @@ func (h *handler) ListDouyinOrders() core.HandlerFunc {
|
|||||||
LocalUserID: uid,
|
LocalUserID: uid,
|
||||||
LocalUserNickname: userNicknameMap[uid],
|
LocalUserNickname: userNicknameMap[uid],
|
||||||
ActualReceiveAmount: formatAmount(o.ActualReceiveAmount),
|
ActualReceiveAmount: formatAmount(o.ActualReceiveAmount),
|
||||||
ActualPayAmount: formatAmount(o.ActualPayAmount),
|
|
||||||
PayTypeDesc: o.PayTypeDesc,
|
PayTypeDesc: o.PayTypeDesc,
|
||||||
Remark: o.Remark,
|
Remark: o.Remark,
|
||||||
UserNickname: o.UserNickname,
|
UserNickname: o.UserNickname,
|
||||||
ProductCount: int64(o.ProductCount),
|
|
||||||
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +182,7 @@ func getOrderStatusText(status int32) string {
|
|||||||
case 3:
|
case 3:
|
||||||
return "已发货"
|
return "已发货"
|
||||||
case 4:
|
case 4:
|
||||||
return "已退款/已取消"
|
return "已取消"
|
||||||
case 5:
|
case 5:
|
||||||
return "已完成"
|
return "已完成"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,214 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/pkg/validation"
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ======== 抖店商品奖励规则 CRUD ========
|
|
||||||
|
|
||||||
type douyinProductRewardListRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type douyinProductRewardListResponse struct {
|
|
||||||
List []douyinProductRewardItem `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type douyinProductRewardItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ProductID string `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ActivityID int64 `json:"activity_id"`
|
|
||||||
ActivityName string `json:"activity_name"`
|
|
||||||
RewardType string `json:"reward_type"`
|
|
||||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
|
||||||
Quantity int32 `json:"quantity"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDouyinProductRewards 获取抖店商品奖励规则列表
|
|
||||||
func (h *handler) ListDouyinProductRewards() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(douyinProductRewardListRequest)
|
|
||||||
if err := ctx.ShouldBindForm(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize <= 0 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.repo.GetDbR().Model(&model.DouyinProductRewards{})
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
if err := db.Count(&total).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var list []model.DouyinProductRewards
|
|
||||||
if err := db.Order("id DESC").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&list).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集所有需要查询的 activity_id
|
|
||||||
activityIDs := make([]int64, 0)
|
|
||||||
for _, r := range list {
|
|
||||||
if r.ActivityID > 0 {
|
|
||||||
activityIDs = append(activityIDs, r.ActivityID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询活动名称
|
|
||||||
activityNameMap := make(map[int64]string)
|
|
||||||
if len(activityIDs) > 0 {
|
|
||||||
var activities []model.LivestreamActivities
|
|
||||||
if err := h.repo.GetDbR().Model(&model.LivestreamActivities{}).
|
|
||||||
Select("id, name").
|
|
||||||
Where("id IN ?", activityIDs).
|
|
||||||
Find(&activities).Error; err == nil {
|
|
||||||
for _, a := range activities {
|
|
||||||
activityNameMap[a.ID] = a.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res := douyinProductRewardListResponse{
|
|
||||||
List: make([]douyinProductRewardItem, len(list)),
|
|
||||||
Total: total,
|
|
||||||
Page: req.Page,
|
|
||||||
}
|
|
||||||
for i, r := range list {
|
|
||||||
res.List[i] = douyinProductRewardItem{
|
|
||||||
ID: r.ID,
|
|
||||||
ProductID: r.ProductID,
|
|
||||||
ProductName: r.ProductName,
|
|
||||||
ActivityID: r.ActivityID,
|
|
||||||
ActivityName: activityNameMap[r.ActivityID],
|
|
||||||
RewardType: r.RewardType,
|
|
||||||
RewardPayload: json.RawMessage(r.RewardPayload),
|
|
||||||
Quantity: r.Quantity,
|
|
||||||
Status: r.Status,
|
|
||||||
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type createDouyinProductRewardRequest struct {
|
|
||||||
ProductID string `json:"product_id" binding:"required"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ActivityID int64 `json:"activity_id"`
|
|
||||||
RewardType string `json:"reward_type" binding:"required"`
|
|
||||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
|
||||||
Quantity int32 `json:"quantity"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDouyinProductReward 创建抖店商品奖励规则
|
|
||||||
func (h *handler) CreateDouyinProductReward() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(createDouyinProductRewardRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if req.Quantity <= 0 {
|
|
||||||
req.Quantity = 1
|
|
||||||
}
|
|
||||||
if req.Status == 0 {
|
|
||||||
req.Status = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
row := &model.DouyinProductRewards{
|
|
||||||
ProductID: req.ProductID,
|
|
||||||
ProductName: req.ProductName,
|
|
||||||
ActivityID: req.ActivityID,
|
|
||||||
RewardType: req.RewardType,
|
|
||||||
RewardPayload: string(req.RewardPayload),
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
Status: req.Status,
|
|
||||||
}
|
|
||||||
if err := h.repo.GetDbW().Create(row).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Payload(map[string]any{"id": row.ID, "message": "创建成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateDouyinProductRewardRequest struct {
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ActivityID *int64 `json:"activity_id"`
|
|
||||||
RewardType string `json:"reward_type"`
|
|
||||||
RewardPayload json.RawMessage `json:"reward_payload"`
|
|
||||||
Quantity int32 `json:"quantity"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDouyinProductReward 更新抖店商品奖励规则
|
|
||||||
func (h *handler) UpdateDouyinProductReward() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
idStr := ctx.Param("id")
|
|
||||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
|
||||||
if id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "无效的ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(updateDouyinProductRewardRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updates := map[string]any{
|
|
||||||
"product_name": req.ProductName,
|
|
||||||
"reward_type": req.RewardType,
|
|
||||||
"reward_payload": string(req.RewardPayload),
|
|
||||||
"quantity": req.Quantity,
|
|
||||||
"status": req.Status,
|
|
||||||
}
|
|
||||||
if req.ActivityID != nil {
|
|
||||||
updates["activity_id"] = *req.ActivityID
|
|
||||||
}
|
|
||||||
if err := h.repo.GetDbW().Model(&model.DouyinProductRewards{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Payload(map[string]string{"message": "更新成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteDouyinProductReward 删除抖店商品奖励规则
|
|
||||||
func (h *handler) DeleteDouyinProductReward() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
idStr := ctx.Param("id")
|
|
||||||
id, _ := strconv.ParseInt(idStr, 10, 64)
|
|
||||||
if id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "无效的ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.repo.GetDbW().Delete(&model.DouyinProductRewards{}, id).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10002, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Payload(map[string]string{"message": "删除成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,825 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"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/livestream"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ========== 直播间活动管理 ==========
|
|
||||||
|
|
||||||
type createLivestreamActivityRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
StreamerName string `json:"streamer_name"`
|
|
||||||
StreamerContact string `json:"streamer_contact"`
|
|
||||||
DouyinProductID string `json:"douyin_product_id"`
|
|
||||||
OrderRewardType string `json:"order_reward_type"` // 下单奖励类型: flip_card/minesweeper
|
|
||||||
OrderRewardQuantity int32 `json:"order_reward_quantity"` // 下单奖励数量: 1-100
|
|
||||||
TicketPrice int64 `json:"ticket_price"`
|
|
||||||
StartTime string `json:"start_time"`
|
|
||||||
EndTime string `json:"end_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamActivityResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
StreamerName string `json:"streamer_name"`
|
|
||||||
StreamerContact string `json:"streamer_contact"`
|
|
||||||
AccessCode string `json:"access_code"`
|
|
||||||
DouyinProductID string `json:"douyin_product_id"`
|
|
||||||
OrderRewardType string `json:"order_reward_type"` // 下单奖励类型
|
|
||||||
OrderRewardQuantity int32 `json:"order_reward_quantity"` // 下单奖励数量
|
|
||||||
TicketPrice int64 `json:"ticket_price"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
StartTime string `json:"start_time,omitempty"`
|
|
||||||
EndTime string `json:"end_time,omitempty"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLivestreamActivity 创建直播间活动
|
|
||||||
// @Summary 创建直播间活动
|
|
||||||
// @Description 创建新的直播间活动,自动生成唯一访问码
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param RequestBody body createLivestreamActivityRequest true "请求参数"
|
|
||||||
// @Success 200 {object} livestreamActivityResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities [post]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) CreateLivestreamActivity() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(createLivestreamActivityRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
input := livestream.CreateActivityInput{
|
|
||||||
Name: req.Name,
|
|
||||||
StreamerName: req.StreamerName,
|
|
||||||
StreamerContact: req.StreamerContact,
|
|
||||||
DouyinProductID: req.DouyinProductID,
|
|
||||||
OrderRewardType: req.OrderRewardType,
|
|
||||||
OrderRewardQuantity: req.OrderRewardQuantity,
|
|
||||||
TicketPrice: req.TicketPrice,
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.StartTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
|
||||||
input.StartTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.EndTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
|
||||||
input.EndTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity, err := h.livestream.CreateActivity(ctx.RequestContext(), input)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&livestreamActivityResponse{
|
|
||||||
ID: activity.ID,
|
|
||||||
Name: activity.Name,
|
|
||||||
StreamerName: activity.StreamerName,
|
|
||||||
StreamerContact: activity.StreamerContact,
|
|
||||||
AccessCode: activity.AccessCode,
|
|
||||||
DouyinProductID: activity.DouyinProductID,
|
|
||||||
OrderRewardType: activity.OrderRewardType,
|
|
||||||
OrderRewardQuantity: activity.OrderRewardQuantity,
|
|
||||||
TicketPrice: int64(activity.TicketPrice),
|
|
||||||
Status: activity.Status,
|
|
||||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateLivestreamActivityRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
StreamerName string `json:"streamer_name"`
|
|
||||||
StreamerContact string `json:"streamer_contact"`
|
|
||||||
DouyinProductID string `json:"douyin_product_id"`
|
|
||||||
OrderRewardType string `json:"order_reward_type"` // 下单奖励类型
|
|
||||||
OrderRewardQuantity *int32 `json:"order_reward_quantity"` // 下单奖励数量
|
|
||||||
TicketPrice *int64 `json:"ticket_price"`
|
|
||||||
Status *int32 `json:"status"`
|
|
||||||
StartTime string `json:"start_time"`
|
|
||||||
EndTime string `json:"end_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLivestreamActivity 更新直播间活动
|
|
||||||
// @Summary 更新直播间活动
|
|
||||||
// @Description 更新直播间活动信息
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Param RequestBody body updateLivestreamActivityRequest true "请求参数"
|
|
||||||
// @Success 200 {object} simpleMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id} [put]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) UpdateLivestreamActivity() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(updateLivestreamActivityRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
input := livestream.UpdateActivityInput{
|
|
||||||
Name: req.Name,
|
|
||||||
StreamerName: req.StreamerName,
|
|
||||||
StreamerContact: req.StreamerContact,
|
|
||||||
DouyinProductID: req.DouyinProductID,
|
|
||||||
OrderRewardType: req.OrderRewardType,
|
|
||||||
OrderRewardQuantity: req.OrderRewardQuantity,
|
|
||||||
TicketPrice: req.TicketPrice,
|
|
||||||
Status: req.Status,
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.StartTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
|
||||||
input.StartTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.EndTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
|
||||||
input.EndTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.livestream.UpdateActivity(ctx.RequestContext(), id, input); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&simpleMessageResponse{Message: "操作成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type listLivestreamActivitiesRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
Status *int32 `form:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listLivestreamActivitiesResponse struct {
|
|
||||||
List []livestreamActivityResponse `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLivestreamActivities 直播间活动列表
|
|
||||||
// @Summary 直播间活动列表
|
|
||||||
// @Description 获取直播间活动列表
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param page query int false "页码" default(1)
|
|
||||||
// @Param page_size query int false "每页数量" default(20)
|
|
||||||
// @Param status query int false "状态过滤"
|
|
||||||
// @Success 200 {object} listLivestreamActivitiesResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) ListLivestreamActivities() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(listLivestreamActivitiesRequest)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
list, total, err := h.livestream.ListActivities(ctx.RequestContext(), req.Page, req.PageSize, req.Status)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &listLivestreamActivitiesResponse{
|
|
||||||
List: make([]livestreamActivityResponse, len(list)),
|
|
||||||
Total: total,
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, a := range list {
|
|
||||||
item := livestreamActivityResponse{
|
|
||||||
ID: a.ID,
|
|
||||||
Name: a.Name,
|
|
||||||
StreamerName: a.StreamerName,
|
|
||||||
StreamerContact: a.StreamerContact,
|
|
||||||
AccessCode: a.AccessCode,
|
|
||||||
DouyinProductID: a.DouyinProductID,
|
|
||||||
OrderRewardType: a.OrderRewardType,
|
|
||||||
OrderRewardQuantity: a.OrderRewardQuantity,
|
|
||||||
TicketPrice: int64(a.TicketPrice),
|
|
||||||
Status: a.Status,
|
|
||||||
CreatedAt: a.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
if !a.StartTime.IsZero() {
|
|
||||||
item.StartTime = a.StartTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !a.EndTime.IsZero() {
|
|
||||||
item.EndTime = a.EndTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
res.List[i] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamActivity 获取直播间活动详情
|
|
||||||
// @Summary 获取直播间活动详情
|
|
||||||
// @Description 根据ID获取直播间活动详情
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} livestreamActivityResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id} [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) GetLivestreamActivity() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivity(ctx.RequestContext(), id)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &livestreamActivityResponse{
|
|
||||||
ID: activity.ID,
|
|
||||||
Name: activity.Name,
|
|
||||||
StreamerName: activity.StreamerName,
|
|
||||||
StreamerContact: activity.StreamerContact,
|
|
||||||
AccessCode: activity.AccessCode,
|
|
||||||
DouyinProductID: activity.DouyinProductID,
|
|
||||||
OrderRewardType: activity.OrderRewardType,
|
|
||||||
OrderRewardQuantity: activity.OrderRewardQuantity,
|
|
||||||
TicketPrice: int64(activity.TicketPrice),
|
|
||||||
Status: activity.Status,
|
|
||||||
CreatedAt: activity.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
if !activity.StartTime.IsZero() {
|
|
||||||
res.StartTime = activity.StartTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !activity.EndTime.IsZero() {
|
|
||||||
res.EndTime = activity.EndTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLivestreamActivity 删除直播间活动
|
|
||||||
// @Summary 删除直播间活动
|
|
||||||
// @Description 删除指定直播间活动
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} simpleMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id} [delete]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) DeleteLivestreamActivity() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.livestream.DeleteActivity(ctx.RequestContext(), id); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 直播间奖品管理 ==========
|
|
||||||
|
|
||||||
type createLivestreamPrizeRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
Weight int32 `json:"weight"`
|
|
||||||
Quantity int32 `json:"quantity"`
|
|
||||||
ProductID int64 `json:"product_id"`
|
|
||||||
CostPrice int64 `json:"cost_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamPrizeResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
Weight int32 `json:"weight"`
|
|
||||||
Quantity int32 `json:"quantity"`
|
|
||||||
Remaining int64 `json:"remaining"`
|
|
||||||
ProductID int64 `json:"product_id"`
|
|
||||||
CostPrice int64 `json:"cost_price"`
|
|
||||||
Sort int32 `json:"sort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLivestreamPrizes 批量创建奖品
|
|
||||||
// @Summary 批量创建直播间奖品
|
|
||||||
// @Description 为指定活动批量创建奖品
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param activity_id path integer true "活动ID"
|
|
||||||
// @Param RequestBody body []createLivestreamPrizeRequest true "奖品列表"
|
|
||||||
// @Success 200 {object} simpleMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{activity_id}/prizes [post]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) CreateLivestreamPrizes() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || activityID <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req []createLivestreamPrizeRequest
|
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var inputs []livestream.CreatePrizeInput
|
|
||||||
for _, p := range req {
|
|
||||||
inputs = append(inputs, livestream.CreatePrizeInput{
|
|
||||||
Name: p.Name,
|
|
||||||
Image: p.Image,
|
|
||||||
Weight: p.Weight,
|
|
||||||
Quantity: p.Quantity,
|
|
||||||
Level: p.Level,
|
|
||||||
ProductID: p.ProductID,
|
|
||||||
CostPrice: p.CostPrice,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.livestream.CreatePrizes(ctx.RequestContext(), activityID, inputs); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&simpleMessageResponse{Message: "创建成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLivestreamPrizes 获取活动奖品列表
|
|
||||||
// @Summary 获取直播间活动奖品列表
|
|
||||||
// @Description 获取指定活动的所有奖品
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param activity_id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} []livestreamPrizeResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{activity_id}/prizes [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) ListLivestreamPrizes() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || activityID <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prizes, err := h.livestream.ListPrizes(ctx.RequestContext(), activityID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := make([]livestreamPrizeResponse, len(prizes))
|
|
||||||
for i, p := range prizes {
|
|
||||||
res[i] = livestreamPrizeResponse{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Image: p.Image,
|
|
||||||
Weight: p.Weight,
|
|
||||||
Quantity: p.Quantity,
|
|
||||||
Remaining: int64(p.Remaining),
|
|
||||||
Level: p.Level,
|
|
||||||
ProductID: p.ProductID,
|
|
||||||
CostPrice: p.CostPrice,
|
|
||||||
Sort: p.Sort,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLivestreamPrize 删除奖品
|
|
||||||
// @Summary 删除直播间奖品
|
|
||||||
// @Description 删除指定奖品
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param prize_id path integer true "奖品ID"
|
|
||||||
// @Success 200 {object} simpleMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/prizes/{prize_id} [delete]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) DeleteLivestreamPrize() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
prizeID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || prizeID <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的奖品ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.livestream.DeletePrize(ctx.RequestContext(), prizeID); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&simpleMessageResponse{Message: "删除成功"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 直播间中奖记录 ==========
|
|
||||||
|
|
||||||
type livestreamDrawLogResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ActivityID int64 `json:"activity_id"`
|
|
||||||
PrizeID int64 `json:"prize_id"`
|
|
||||||
PrizeName string `json:"prize_name"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
DouyinOrderID int64 `json:"douyin_order_id"` // 关联ID
|
|
||||||
ShopOrderID string `json:"shop_order_id"` // 店铺订单号
|
|
||||||
LocalUserID int64 `json:"local_user_id"`
|
|
||||||
DouyinUserID string `json:"douyin_user_id"`
|
|
||||||
UserNickname string `json:"user_nickname"` // 用户昵称
|
|
||||||
SeedHash string `json:"seed_hash"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listLivestreamDrawLogsResponse struct {
|
|
||||||
List []livestreamDrawLogResponse `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Stats *livestreamDrawLogsStats `json:"stats,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamDrawLogsStats struct {
|
|
||||||
UserCount int64 `json:"user_count"`
|
|
||||||
OrderCount int64 `json:"order_count"`
|
|
||||||
TotalRev int64 `json:"total_revenue"` // 总流水
|
|
||||||
TotalRefund int64 `json:"total_refund"`
|
|
||||||
TotalCost int64 `json:"total_cost"`
|
|
||||||
NetProfit int64 `json:"net_profit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listLivestreamDrawLogsRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
StartTime string `form:"start_time"`
|
|
||||||
EndTime string `form:"end_time"`
|
|
||||||
Keyword string `form:"keyword"`
|
|
||||||
ExcludeUserIDs string `form:"exclude_user_ids"` // 逗号分隔的 UserIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLivestreamDrawLogs 获取中奖记录
|
|
||||||
// @Summary 获取直播间中奖记录
|
|
||||||
// @Description 获取指定活动的中奖记录,支持时间范围和关键词筛选
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param activity_id path integer true "活动ID"
|
|
||||||
// @Param page query int false "页码" default(1)
|
|
||||||
// @Param page_size query int false "每页数量" default(20)
|
|
||||||
// @Param start_time query string false "开始时间 (YYYY-MM-DD)"
|
|
||||||
// @Param end_time query string false "结束时间 (YYYY-MM-DD)"
|
|
||||||
// @Param keyword query string false "搜索关键词 (昵称/订单号/奖品名称)"
|
|
||||||
// @Success 200 {object} listLivestreamDrawLogsResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{activity_id}/draw_logs [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) ListLivestreamDrawLogs() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
activityID, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || activityID <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(listLivestreamDrawLogsRequest)
|
|
||||||
_ = ctx.ShouldBindForm(req)
|
|
||||||
|
|
||||||
page := req.Page
|
|
||||||
pageSize := req.PageSize
|
|
||||||
if page <= 0 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
if pageSize <= 0 {
|
|
||||||
pageSize = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析时间范围 (支持 YYYY-MM-DD HH:mm:ss 和 YYYY-MM-DD)
|
|
||||||
var startTime, endTime *time.Time
|
|
||||||
if req.StartTime != "" {
|
|
||||||
// 尝试解析完整时间
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
|
|
||||||
startTime = &t
|
|
||||||
} else if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
|
|
||||||
// 只有日期,默认 00:00:00
|
|
||||||
startTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.EndTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
|
|
||||||
endTime = &t
|
|
||||||
} else if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
|
|
||||||
// 只有日期,设为当天结束 23:59:59.999
|
|
||||||
end := t.Add(24*time.Hour - time.Nanosecond)
|
|
||||||
endTime = &end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析排除用户ID
|
|
||||||
var excludeUIDs []int64
|
|
||||||
if req.ExcludeUserIDs != "" {
|
|
||||||
parts := strings.Split(req.ExcludeUserIDs, ",")
|
|
||||||
for _, p := range parts {
|
|
||||||
p = strings.TrimSpace(p)
|
|
||||||
if val, err := strconv.ParseInt(p, 10, 64); err == nil && val > 0 {
|
|
||||||
excludeUIDs = append(excludeUIDs, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用底层 GORM 直接查询以支持 keyword
|
|
||||||
db := h.repo.GetDbR().Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
|
|
||||||
|
|
||||||
if startTime != nil {
|
|
||||||
db = db.Where("created_at >= ?", startTime)
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
db = db.Where("created_at <= ?", endTime)
|
|
||||||
}
|
|
||||||
if req.Keyword != "" {
|
|
||||||
keyword := "%" + req.Keyword + "%"
|
|
||||||
db = db.Where("(user_nickname LIKE ? OR shop_order_id LIKE ? OR prize_name LIKE ?)", keyword, keyword, keyword)
|
|
||||||
}
|
|
||||||
if len(excludeUIDs) > 0 {
|
|
||||||
db = db.Where("local_user_id NOT IN ?", excludeUIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
db.Count(&total)
|
|
||||||
|
|
||||||
// 计算统计数据 (仅当有数据时)
|
|
||||||
var stats *livestreamDrawLogsStats
|
|
||||||
if total > 0 {
|
|
||||||
stats = &livestreamDrawLogsStats{}
|
|
||||||
// 1. 统计用户数
|
|
||||||
// 使用 Session() 避免污染主 db 对象
|
|
||||||
db.Session(&gorm.Session{}).Select("COUNT(DISTINCT douyin_user_id)").Scan(&stats.UserCount)
|
|
||||||
|
|
||||||
// 2. 获取所有相关的 douyin_order_id 和 prize_id,用于在内存中聚合金额和成本
|
|
||||||
// 注意:如果数据量极大,这里可能有性能隐患。但考虑到这是后台查询且通常带有筛选,暂且全量拉取 ID。
|
|
||||||
// 优化:只查需要的字段
|
|
||||||
type logMeta struct {
|
|
||||||
DouyinOrderID int64
|
|
||||||
PrizeID int64
|
|
||||||
ShopOrderID string // 用于关联退款状态查 douyin_orders
|
|
||||||
}
|
|
||||||
var metas []logMeta
|
|
||||||
// 使用不带分页的 db 克隆
|
|
||||||
if err := db.Session(&gorm.Session{}).Select("douyin_order_id, prize_id, shop_order_id").Scan(&metas).Error; err == nil {
|
|
||||||
orderIDs := make([]int64, 0, len(metas))
|
|
||||||
distinctOrderIDs := make(map[int64]bool)
|
|
||||||
prizeIDCount := make(map[int64]int64)
|
|
||||||
|
|
||||||
for _, m := range metas {
|
|
||||||
if !distinctOrderIDs[m.DouyinOrderID] {
|
|
||||||
distinctOrderIDs[m.DouyinOrderID] = true
|
|
||||||
orderIDs = append(orderIDs, m.DouyinOrderID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats.OrderCount = int64(len(orderIDs))
|
|
||||||
|
|
||||||
// 3. 查询订单金额和退款状态
|
|
||||||
if len(orderIDs) > 0 {
|
|
||||||
var orders []model.DouyinOrders
|
|
||||||
// 分批查询防止 IN 子句过长? 暂时假设量级可控
|
|
||||||
h.repo.GetDbR().Select("id, actual_pay_amount, order_status").
|
|
||||||
Where("id IN ?", orderIDs).Find(&orders)
|
|
||||||
|
|
||||||
orderRefundMap := make(map[int64]bool)
|
|
||||||
|
|
||||||
for _, o := range orders {
|
|
||||||
// 统计营收 (总流水)
|
|
||||||
stats.TotalRev += int64(o.ActualPayAmount)
|
|
||||||
|
|
||||||
if o.OrderStatus == 4 { // 已退款
|
|
||||||
stats.TotalRefund += int64(o.ActualPayAmount)
|
|
||||||
orderRefundMap[o.ID] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 统计成本 (剔除退款订单)
|
|
||||||
for _, m := range metas {
|
|
||||||
if !orderRefundMap[m.DouyinOrderID] {
|
|
||||||
prizeIDCount[m.PrizeID]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算奖品成本 (逻辑参考 GetLivestreamStats,简化版)
|
|
||||||
if len(prizeIDCount) > 0 {
|
|
||||||
prizeIDs := make([]int64, 0, len(prizeIDCount))
|
|
||||||
for pid := range prizeIDCount {
|
|
||||||
prizeIDs = append(prizeIDs, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var prizes []model.LivestreamPrizes
|
|
||||||
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
|
||||||
|
|
||||||
// 批量获取关联商品
|
|
||||||
productIDs := make([]int64, 0)
|
|
||||||
for _, p := range prizes {
|
|
||||||
if p.CostPrice == 0 && p.ProductID > 0 {
|
|
||||||
productIDs = append(productIDs, p.ProductID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
productPriceMap := make(map[int64]int64)
|
|
||||||
if len(productIDs) > 0 {
|
|
||||||
var products []model.Products
|
|
||||||
h.repo.GetDbR().Select("id, price").Where("id IN ?", productIDs).Find(&products)
|
|
||||||
for _, prod := range products {
|
|
||||||
productPriceMap[prod.ID] = prod.Price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range prizes {
|
|
||||||
cost := p.CostPrice
|
|
||||||
if cost == 0 && p.ProductID > 0 {
|
|
||||||
cost = productPriceMap[p.ProductID]
|
|
||||||
}
|
|
||||||
count := prizeIDCount[p.ID]
|
|
||||||
stats.TotalCost += cost * count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats.NetProfit = (stats.TotalRev - stats.TotalRefund) - stats.TotalCost
|
|
||||||
}
|
|
||||||
|
|
||||||
var logs []model.LivestreamDrawLogs
|
|
||||||
// 重置 Select,确保查询 logs 时获取所有字段 (或者指定 default fields)
|
|
||||||
// db 对象如果被污染,这里需要显式清除 Select。使用 Session 应该能避免。
|
|
||||||
// 安全起见,这里也可以用 db.Session(&gorm.Session{})
|
|
||||||
if err := db.Session(&gorm.Session{}).Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&logs).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &listLivestreamDrawLogsResponse{
|
|
||||||
List: make([]livestreamDrawLogResponse, len(logs)),
|
|
||||||
Total: total,
|
|
||||||
Page: page,
|
|
||||||
PageSize: pageSize,
|
|
||||||
Stats: stats,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, log := range logs {
|
|
||||||
res.List[i] = livestreamDrawLogResponse{
|
|
||||||
ID: log.ID,
|
|
||||||
ActivityID: log.ActivityID,
|
|
||||||
PrizeID: log.PrizeID,
|
|
||||||
PrizeName: log.PrizeName,
|
|
||||||
Level: log.Level,
|
|
||||||
DouyinOrderID: log.DouyinOrderID,
|
|
||||||
ShopOrderID: log.ShopOrderID,
|
|
||||||
LocalUserID: log.LocalUserID,
|
|
||||||
DouyinUserID: log.DouyinUserID,
|
|
||||||
UserNickname: log.UserNickname,
|
|
||||||
SeedHash: log.SeedHash,
|
|
||||||
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 直播间承诺管理 ==========
|
|
||||||
|
|
||||||
type livestreamCommitmentSummaryResponse struct {
|
|
||||||
SeedVersion int32 `json:"seed_version"`
|
|
||||||
Algo string `json:"algo"`
|
|
||||||
HasSeed bool `json:"has_seed"`
|
|
||||||
LenSeed int `json:"len_seed_master"`
|
|
||||||
LenHash int `json:"len_seed_hash"`
|
|
||||||
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开复制)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateLivestreamCommitment 生成直播间活动承诺
|
|
||||||
// @Summary 生成直播间活动承诺
|
|
||||||
// @Description 为直播间活动生成可验证的承诺种子
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} map[string]int32
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id}/commitment/generate [post]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) GenerateLivestreamCommitment() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
version, err := h.livestream.GenerateCommitment(ctx.RequestContext(), id)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]int32{"seed_version": version})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamCommitmentSummary 获取直播间活动承诺摘要
|
|
||||||
// @Summary 获取直播间活动承诺摘要
|
|
||||||
// @Description 获取直播间活动的承诺状态信息
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} livestreamCommitmentSummaryResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id}/commitment/summary [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) GetLivestreamCommitmentSummary() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
summary, err := h.livestream.GetCommitmentSummary(ctx.RequestContext(), id)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&livestreamCommitmentSummaryResponse{
|
|
||||||
SeedVersion: summary.SeedVersion,
|
|
||||||
Algo: summary.Algo,
|
|
||||||
HasSeed: summary.HasSeed,
|
|
||||||
LenSeed: summary.LenSeed,
|
|
||||||
LenHash: summary.LenHash,
|
|
||||||
SeedHashHex: summary.SeedHashHex,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,333 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"bindbox-game/internal/code"
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dailyLivestreamStats struct {
|
|
||||||
Date string `json:"date"` // 日期
|
|
||||||
TotalRevenue int64 `json:"total_revenue"` // 营收
|
|
||||||
TotalRefund int64 `json:"total_refund"` // 退款
|
|
||||||
TotalCost int64 `json:"total_cost"` // 成本
|
|
||||||
NetProfit int64 `json:"net_profit"` // 净利润
|
|
||||||
ProfitMargin float64 `json:"profit_margin"` // 利润率
|
|
||||||
OrderCount int64 `json:"order_count"` // 订单数
|
|
||||||
RefundCount int64 `json:"refund_count"` // 退款单数
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamStatsResponse struct {
|
|
||||||
TotalRevenue int64 `json:"total_revenue"` // 总营收(分)
|
|
||||||
TotalRefund int64 `json:"total_refund"` // 总退款(分)
|
|
||||||
TotalCost int64 `json:"total_cost"` // 总成本(分)
|
|
||||||
NetProfit int64 `json:"net_profit"` // 净利润(分)
|
|
||||||
OrderCount int64 `json:"order_count"` // 订单数
|
|
||||||
RefundCount int64 `json:"refund_count"` // 退款数
|
|
||||||
ProfitMargin float64 `json:"profit_margin"` // 利润率 %
|
|
||||||
Daily []dailyLivestreamStats `json:"daily"` // 每日明细
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamStats 获取直播间盈亏统计
|
|
||||||
// @Summary 获取直播间盈亏统计
|
|
||||||
// @Description 计算逻辑:净利润 = (营收 - 退款) - 奖品成本。营收 = 抽奖次数 * 门票价格。成本 = 中奖奖品成本总和。
|
|
||||||
// @Tags 管理端.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path integer true "活动ID"
|
|
||||||
// @Success 200 {object} livestreamStatsResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/livestream/activities/{id}/stats [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) GetLivestreamStats() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的活动ID"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(struct {
|
|
||||||
StartTime string `form:"start_time"`
|
|
||||||
EndTime string `form:"end_time"`
|
|
||||||
})
|
|
||||||
_ = ctx.ShouldBindQuery(req)
|
|
||||||
|
|
||||||
var startTime, endTime *time.Time
|
|
||||||
if req.StartTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02", req.StartTime, time.Local); err == nil {
|
|
||||||
startTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.EndTime != "" {
|
|
||||||
if t, err := time.ParseInLocation("2006-01-02", req.EndTime, time.Local); err == nil {
|
|
||||||
end := t.Add(24*time.Hour - time.Nanosecond)
|
|
||||||
endTime = &end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 获取活动信息(门票价格)
|
|
||||||
var activity model.LivestreamActivities
|
|
||||||
if err := h.repo.GetDbR().Where("id = ?", id).First(&activity).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, code.ServerError, "活动不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ticketPrice 暂未使用,但在统计中可能作为参考,这里移除未使用的报错
|
|
||||||
|
|
||||||
// 2. 核心统计逻辑重构:从关联的订单表获取真实金额
|
|
||||||
// 使用子查询或 Join 来获取不重复的订单金额,确保即便一次订单对应多次抽奖,金额也不重计
|
|
||||||
var totalRevenue, orderCount int64
|
|
||||||
// 统计营收:来自已参与过抽奖(产生过日志)且未退款的订单 (order_status != 4)
|
|
||||||
// 使用 actual_pay_amount (实付金额)
|
|
||||||
queryRevenue := `
|
|
||||||
SELECT
|
|
||||||
CAST(IFNULL(SUM(distinct_orders.actual_pay_amount), 0) AS SIGNED) as rev,
|
|
||||||
COUNT(*) as cnt
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT o.id, o.actual_pay_amount
|
|
||||||
FROM douyin_orders o
|
|
||||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
|
||||||
WHERE l.activity_id = ?
|
|
||||||
`
|
|
||||||
if startTime != nil {
|
|
||||||
queryRevenue += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
queryRevenue += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
queryRevenue += ") as distinct_orders"
|
|
||||||
|
|
||||||
_ = h.repo.GetDbR().Raw(queryRevenue, id).Row().Scan(&totalRevenue, &orderCount)
|
|
||||||
|
|
||||||
// 统计退款:来自已参与过抽奖且标记为退款的订单 (order_status = 4)
|
|
||||||
var totalRefund, refundCount int64
|
|
||||||
queryRefund := `
|
|
||||||
SELECT
|
|
||||||
CAST(IFNULL(SUM(distinct_orders.actual_pay_amount), 0) AS SIGNED) as ref_amt,
|
|
||||||
COUNT(*) as ref_cnt
|
|
||||||
FROM (
|
|
||||||
SELECT DISTINCT o.id, o.actual_pay_amount
|
|
||||||
FROM douyin_orders o
|
|
||||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
|
||||||
WHERE l.activity_id = ? AND o.order_status = 4
|
|
||||||
`
|
|
||||||
if startTime != nil {
|
|
||||||
queryRefund += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
queryRefund += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
queryRefund += ") as distinct_orders"
|
|
||||||
|
|
||||||
_ = h.repo.GetDbR().Raw(queryRefund, id).Row().Scan(&totalRefund, &refundCount)
|
|
||||||
|
|
||||||
// 3. 获取所有抽奖记录用于成本计算
|
|
||||||
var drawLogs []model.LivestreamDrawLogs
|
|
||||||
db := h.repo.GetDbR().Where("activity_id = ?", id)
|
|
||||||
if startTime != nil {
|
|
||||||
db = db.Where("created_at >= ?", startTime)
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
db = db.Where("created_at <= ?", endTime)
|
|
||||||
}
|
|
||||||
db.Find(&drawLogs)
|
|
||||||
|
|
||||||
// 3.1 获取该时间段内所有退款的 shop_order_id 集合,用于过滤成本
|
|
||||||
refundedShopOrderIDs := make(map[string]bool)
|
|
||||||
var refundedOrders []string
|
|
||||||
qRefundIDs := `
|
|
||||||
SELECT DISTINCT o.shop_order_id
|
|
||||||
FROM douyin_orders o
|
|
||||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
|
||||||
WHERE l.activity_id = ? AND o.order_status = 4
|
|
||||||
`
|
|
||||||
if startTime != nil {
|
|
||||||
qRefundIDs += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
qRefundIDs += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
h.repo.GetDbR().Raw(qRefundIDs, id).Scan(&refundedOrders)
|
|
||||||
for _, oid := range refundedOrders {
|
|
||||||
refundedShopOrderIDs[oid] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 计算成本(只统计未退款订单的奖品成本)
|
|
||||||
prizeIDCountMap := make(map[int64]int64)
|
|
||||||
for _, log := range drawLogs {
|
|
||||||
// 排除已退款的订单 (检查 douyin_orders 状态)
|
|
||||||
if refundedShopOrderIDs[log.ShopOrderID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
prizeIDCountMap[log.PrizeID]++
|
|
||||||
}
|
|
||||||
|
|
||||||
prizeIDs := make([]int64, 0, len(prizeIDCountMap))
|
|
||||||
for pid := range prizeIDCountMap {
|
|
||||||
prizeIDs = append(prizeIDs, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCost int64
|
|
||||||
prizeCostMap := make(map[int64]int64)
|
|
||||||
if len(prizeIDs) > 0 {
|
|
||||||
var prizes []model.LivestreamPrizes
|
|
||||||
h.repo.GetDbR().Where("id IN ?", prizeIDs).Find(&prizes)
|
|
||||||
|
|
||||||
productIDsNeedingFallback := make([]int64, 0)
|
|
||||||
prizeProductMap := make(map[int64]int64)
|
|
||||||
|
|
||||||
for _, p := range prizes {
|
|
||||||
if p.CostPrice > 0 {
|
|
||||||
prizeCostMap[p.ID] = p.CostPrice
|
|
||||||
} else if p.ProductID > 0 {
|
|
||||||
productIDsNeedingFallback = append(productIDsNeedingFallback, p.ProductID)
|
|
||||||
prizeProductMap[p.ID] = p.ProductID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(productIDsNeedingFallback) > 0 {
|
|
||||||
var products []model.Products
|
|
||||||
h.repo.GetDbR().Where("id IN ?", productIDsNeedingFallback).Find(&products)
|
|
||||||
productPriceMap := make(map[int64]int64)
|
|
||||||
for _, prod := range products {
|
|
||||||
productPriceMap[prod.ID] = prod.Price
|
|
||||||
}
|
|
||||||
for prizeID, productID := range prizeProductMap {
|
|
||||||
if _, ok := prizeCostMap[prizeID]; !ok {
|
|
||||||
if price, found := productPriceMap[productID]; found {
|
|
||||||
prizeCostMap[prizeID] = price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for prizeID, count := range prizeIDCountMap {
|
|
||||||
if cost, ok := prizeCostMap[prizeID]; ok {
|
|
||||||
totalCost += cost * count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 按天分组统计
|
|
||||||
dailyMap := make(map[string]*dailyLivestreamStats)
|
|
||||||
|
|
||||||
// 5.1 统计每日营收和退款(直接累加订单实付金额)
|
|
||||||
type DailyAmount struct {
|
|
||||||
DateKey string
|
|
||||||
Amount int64
|
|
||||||
Count int64
|
|
||||||
IsRefunded int32
|
|
||||||
}
|
|
||||||
var dailyAmounts []DailyAmount
|
|
||||||
queryDailyCorrect := `
|
|
||||||
SELECT
|
|
||||||
date_key,
|
|
||||||
CAST(SUM(actual_pay_amount) AS SIGNED) as amount,
|
|
||||||
COUNT(id) as cnt,
|
|
||||||
refund_flag as is_refunded
|
|
||||||
FROM (
|
|
||||||
SELECT
|
|
||||||
o.id,
|
|
||||||
DATE_FORMAT(MIN(l.created_at), '%Y-%m-%d') as date_key,
|
|
||||||
o.actual_pay_amount,
|
|
||||||
IF(o.order_status = 4, 1, 0) as refund_flag
|
|
||||||
FROM douyin_orders o
|
|
||||||
JOIN livestream_draw_logs l ON o.id = l.douyin_order_id
|
|
||||||
WHERE l.activity_id = ?
|
|
||||||
`
|
|
||||||
if startTime != nil {
|
|
||||||
queryDailyCorrect += " AND l.created_at >= '" + startTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
if endTime != nil {
|
|
||||||
queryDailyCorrect += " AND l.created_at <= '" + endTime.Format("2006-01-02 15:04:05") + "'"
|
|
||||||
}
|
|
||||||
queryDailyCorrect += `
|
|
||||||
GROUP BY o.id
|
|
||||||
) as t
|
|
||||||
GROUP BY date_key, is_refunded
|
|
||||||
`
|
|
||||||
rows, _ := h.repo.GetDbR().Raw(queryDailyCorrect, id).Rows()
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var da DailyAmount
|
|
||||||
_ = rows.Scan(&da.DateKey, &da.Amount, &da.Count, &da.IsRefunded)
|
|
||||||
dailyAmounts = append(dailyAmounts, da)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, da := range dailyAmounts {
|
|
||||||
if _, ok := dailyMap[da.DateKey]; !ok {
|
|
||||||
dailyMap[da.DateKey] = &dailyLivestreamStats{Date: da.DateKey}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正口径:营收(Revenue) = 总流水 (含退款与未退款)
|
|
||||||
// 这样下面的 NetProfit = TotalRevenue - TotalRefund - TotalCost 才不会双重扣除
|
|
||||||
dailyMap[da.DateKey].TotalRevenue += da.Amount
|
|
||||||
dailyMap[da.DateKey].OrderCount += da.Count
|
|
||||||
|
|
||||||
if da.IsRefunded == 1 {
|
|
||||||
dailyMap[da.DateKey].TotalRefund += da.Amount
|
|
||||||
dailyMap[da.DateKey].RefundCount += da.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5.2 统计每日成本(基于 Logs)
|
|
||||||
for _, log := range drawLogs {
|
|
||||||
// 排除退款订单
|
|
||||||
if refundedShopOrderIDs[log.ShopOrderID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dateKey := log.CreatedAt.Format("2006-01-02")
|
|
||||||
ds := dailyMap[dateKey]
|
|
||||||
if ds != nil {
|
|
||||||
if cost, ok := prizeCostMap[log.PrizeID]; ok {
|
|
||||||
ds.TotalCost += cost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 汇总每日数据并计算总体指标
|
|
||||||
var calcTotalRevenue, calcTotalRefund, calcTotalCost int64
|
|
||||||
dailyList := make([]dailyLivestreamStats, 0, len(dailyMap))
|
|
||||||
for _, ds := range dailyMap {
|
|
||||||
ds.NetProfit = (ds.TotalRevenue - ds.TotalRefund) - ds.TotalCost
|
|
||||||
netRev := ds.TotalRevenue - ds.TotalRefund
|
|
||||||
if netRev > 0 {
|
|
||||||
ds.ProfitMargin = math.Trunc(float64(ds.NetProfit)/float64(netRev)*10000) / 100
|
|
||||||
} else if netRev == 0 && ds.TotalCost > 0 {
|
|
||||||
ds.ProfitMargin = -100
|
|
||||||
}
|
|
||||||
dailyList = append(dailyList, *ds)
|
|
||||||
|
|
||||||
calcTotalRevenue += ds.TotalRevenue
|
|
||||||
calcTotalRefund += ds.TotalRefund
|
|
||||||
calcTotalCost += ds.TotalCost
|
|
||||||
}
|
|
||||||
|
|
||||||
netProfit := (totalRevenue - totalRefund) - totalCost
|
|
||||||
var margin float64
|
|
||||||
netRevenue := totalRevenue - totalRefund
|
|
||||||
if netRevenue > 0 {
|
|
||||||
margin = float64(netProfit) / float64(netRevenue) * 100
|
|
||||||
} else if netRevenue == 0 && totalCost > 0 {
|
|
||||||
margin = -100
|
|
||||||
} else {
|
|
||||||
margin = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&livestreamStatsResponse{
|
|
||||||
TotalRevenue: totalRevenue,
|
|
||||||
TotalRefund: totalRefund,
|
|
||||||
TotalCost: totalCost,
|
|
||||||
NetProfit: netProfit,
|
|
||||||
OrderCount: orderCount,
|
|
||||||
RefundCount: refundCount,
|
|
||||||
ProfitMargin: math.Trunc(margin*100) / 100,
|
|
||||||
Daily: dailyList,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -181,8 +181,6 @@ func (h *handler) SettleIssue() core.HandlerFunc {
|
|||||||
if refundCouponID > 0 {
|
if refundCouponID > 0 {
|
||||||
_ = usersvc.New(h.logger, h.repo).AddCoupon(ctx.RequestContext(), o.UserID, refundCouponID)
|
_ = usersvc.New(h.logger, h.repo).AddCoupon(ctx.RequestContext(), o.UserID, refundCouponID)
|
||||||
}
|
}
|
||||||
// 增加一番赏位置恢复
|
|
||||||
_ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), o.ID)
|
|
||||||
refunded++
|
refunded++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type miniappQRCodeRequest struct {
|
type miniappQRCodeRequest struct {
|
||||||
@ -48,12 +48,8 @@ func (h *handler) GenerateMiniAppQRCode() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := "/pages/login/index?" + q.Encode()
|
path := "/pages/login/index?" + q.Encode()
|
||||||
|
cfg := configs.Get()
|
||||||
// 使用动态配置
|
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret}
|
||||||
dc := sysconfig.GetDynamicConfig()
|
|
||||||
wxConfig := dc.GetWechat(ctx.RequestContext())
|
|
||||||
|
|
||||||
wxcfg := &wechat.WechatConfig{AppID: wxConfig.AppID, AppSecret: wxConfig.AppSecret}
|
|
||||||
qReq := &wechat.QRCodeRequest{Path: path}
|
qReq := &wechat.QRCodeRequest{Path: path}
|
||||||
if req.Width != nil {
|
if req.Width != nil {
|
||||||
qReq.Width = *req.Width
|
qReq.Width = *req.Width
|
||||||
|
|||||||
@ -277,22 +277,11 @@ func (h *handler) ListPayOrders() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type adminOrderPointsLedgerItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Points int64 `json:"points"`
|
|
||||||
RefTable string `json:"ref_table"`
|
|
||||||
RefID string `json:"ref_id"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type getPayOrderResponse struct {
|
type getPayOrderResponse struct {
|
||||||
Order *model.Orders `json:"order"`
|
Order *model.Orders `json:"order"`
|
||||||
Items []*model.OrderItems `json:"items"`
|
Items []*model.OrderItems `json:"items"`
|
||||||
Shipments []*model.ShippingRecords `json:"shipments"`
|
Shipments []*model.ShippingRecords `json:"shipments"`
|
||||||
Ledgers []adminOrderPointsLedgerItem `json:"ledgers"`
|
Ledgers []*model.UserPointsLedger `json:"ledgers"`
|
||||||
User *model.Users `json:"user"`
|
User *model.Users `json:"user"`
|
||||||
Coupons []*struct {
|
Coupons []*struct {
|
||||||
UserCouponID int64 `json:"user_coupon_id"`
|
UserCouponID int64 `json:"user_coupon_id"`
|
||||||
@ -655,19 +644,7 @@ func (h *handler) GetPayOrderDetail() core.HandlerFunc {
|
|||||||
rsp.Items = items
|
rsp.Items = items
|
||||||
rsp.Coupons = couponList
|
rsp.Coupons = couponList
|
||||||
rsp.Shipments = shipments
|
rsp.Shipments = shipments
|
||||||
rsp.Ledgers = make([]adminOrderPointsLedgerItem, len(ledgers))
|
rsp.Ledgers = ledgers
|
||||||
for i, l := range ledgers {
|
|
||||||
rsp.Ledgers[i] = adminOrderPointsLedgerItem{
|
|
||||||
ID: l.ID,
|
|
||||||
UserID: l.UserID,
|
|
||||||
Action: l.Action,
|
|
||||||
Points: int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), l.Points)),
|
|
||||||
RefTable: l.RefTable,
|
|
||||||
RefID: l.RefID,
|
|
||||||
Remark: l.Remark,
|
|
||||||
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if order.SourceType == 2 {
|
if order.SourceType == 2 {
|
||||||
unit := int64(0)
|
unit := int64(0)
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -49,7 +50,14 @@ func (h *handler) ExportPayOrders() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 23001, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 23001, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var pointsRate int64 = 1
|
||||||
|
if cfgRate, _ := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First(); cfgRate != nil {
|
||||||
|
var r int64
|
||||||
|
_, _ = fmt.Sscanf(cfgRate.ConfigValue, "%d", &r)
|
||||||
|
if r > 0 {
|
||||||
|
pointsRate = r
|
||||||
|
}
|
||||||
|
}
|
||||||
file := xlsx.NewFile()
|
file := xlsx.NewFile()
|
||||||
sheet, _ := file.AddSheet("orders")
|
sheet, _ := file.AddSheet("orders")
|
||||||
header := []string{"订单号", "用户ID", "来源", "状态", "总金额", "折扣", "积分抵扣(分)", "积分抵扣(积分)", "优惠券抵扣(分)", "实付", "支付时间", "创建时间"}
|
header := []string{"订单号", "用户ID", "来源", "状态", "总金额", "折扣", "积分抵扣(分)", "积分抵扣(积分)", "优惠券抵扣(分)", "实付", "支付时间", "创建时间"}
|
||||||
@ -68,17 +76,13 @@ func (h *handler) ExportPayOrders() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
pa := o.PointsAmount
|
pa := o.PointsAmount
|
||||||
if pa == 0 && consumePointsSum > 0 {
|
if pa == 0 && consumePointsSum > 0 {
|
||||||
{
|
pa = consumePointsSum / pointsRate
|
||||||
// Backwards compatibility if o.PointsAmount is missing
|
|
||||||
// If consumePointsSum is Cents, pa is Cents.
|
|
||||||
pa = consumePointsSum
|
|
||||||
}
|
}
|
||||||
}
|
pu := int64(0)
|
||||||
// pu is Points Unit
|
if consumePointsSum > 0 {
|
||||||
pu := int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), consumePointsSum))
|
pu = consumePointsSum
|
||||||
if pu == 0 && pa > 0 {
|
} else if pa > 0 {
|
||||||
// If no ledger, try converting from Cents
|
pu = pa * pointsRate
|
||||||
pu = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), pa))
|
|
||||||
}
|
}
|
||||||
ocs, _ := h.readDB.OrderCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.OrderCoupons.OrderID.Eq(o.ID)).Find()
|
ocs, _ := h.readDB.OrderCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.OrderCoupons.OrderID.Eq(o.ID)).Find()
|
||||||
var couponApplied int64
|
var couponApplied int64
|
||||||
|
|||||||
@ -168,9 +168,6 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
|
|
||||||
// 全额退款:回收中奖资产与奖品库存(包含已兑换积分的资产)
|
// 全额退款:回收中奖资产与奖品库存(包含已兑换积分的资产)
|
||||||
svc := usersvc.New(h.logger, h.repo)
|
svc := usersvc.New(h.logger, h.repo)
|
||||||
// 直接使用已初始化的 activity service 清理格位
|
|
||||||
_ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), order.ID)
|
|
||||||
|
|
||||||
var pointsShortage bool
|
var pointsShortage bool
|
||||||
for _, inv := range allInvs {
|
for _, inv := range allInvs {
|
||||||
if inv.Status == 1 {
|
if inv.Status == 1 {
|
||||||
@ -239,28 +236,13 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 全额退款:回退次数卡(user_game_passes)
|
// 全额退款:回退次数卡(user_game_passes)
|
||||||
// 优先解析新格式: gp_use:ID:Count (支持多张卡、多数量)
|
// 解析订单 remark 中的 game_pass:xxx ID
|
||||||
reGpNew := regexp.MustCompile(`gp_use:(\d+):(\d+)`)
|
|
||||||
matchesNew := reGpNew.FindAllStringSubmatch(order.Remark, -1)
|
|
||||||
if len(matchesNew) > 0 {
|
|
||||||
for _, m := range matchesNew {
|
|
||||||
gpID, _ := strconv.ParseInt(m[1], 10, 64)
|
|
||||||
gpCount, _ := strconv.ParseInt(m[2], 10, 64)
|
|
||||||
if gpID > 0 && gpCount > 0 {
|
|
||||||
if err := h.repo.GetDbW().Exec("UPDATE user_game_passes SET remaining = remaining + ?, total_used = GREATEST(total_used - ?, 0), updated_at = NOW(3) WHERE id = ?", gpCount, gpCount, gpID).Error; err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("refund restore game_pass failed: order=%s gp_id=%d count=%d err=%v", order.OrderNo, gpID, gpCount, err))
|
|
||||||
} else {
|
|
||||||
h.logger.Info(fmt.Sprintf("refund restore game_pass success: order=%s gp_id=%d count=%d", order.OrderNo, gpID, gpCount))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 兼容旧格式: game_pass:ID (仅恢复 1 次)
|
|
||||||
reGamePass := regexp.MustCompile(`game_pass:(\d+)`)
|
reGamePass := regexp.MustCompile(`game_pass:(\d+)`)
|
||||||
gamePassMatches := reGamePass.FindStringSubmatch(order.Remark)
|
gamePassMatches := reGamePass.FindStringSubmatch(order.Remark)
|
||||||
if len(gamePassMatches) > 1 {
|
if len(gamePassMatches) > 1 {
|
||||||
gamePassID, _ := strconv.ParseInt(gamePassMatches[1], 10, 64)
|
gamePassID, _ := strconv.ParseInt(gamePassMatches[1], 10, 64)
|
||||||
if gamePassID > 0 {
|
if gamePassID > 0 {
|
||||||
|
// 恢复次数卡:remaining +1, total_used -1
|
||||||
if err := h.repo.GetDbW().Exec("UPDATE user_game_passes SET remaining = remaining + 1, total_used = GREATEST(total_used - 1, 0), updated_at = NOW(3) WHERE id = ?", gamePassID).Error; err != nil {
|
if err := h.repo.GetDbW().Exec("UPDATE user_game_passes SET remaining = remaining + 1, total_used = GREATEST(total_used - 1, 0), updated_at = NOW(3) WHERE id = ?", gamePassID).Error; err != nil {
|
||||||
h.logger.Error(fmt.Sprintf("refund restore game_pass failed: order=%s game_pass_id=%d err=%v", order.OrderNo, gamePassID, err))
|
h.logger.Error(fmt.Sprintf("refund restore game_pass failed: order=%s game_pass_id=%d err=%v", order.OrderNo, gamePassID, err))
|
||||||
} else {
|
} else {
|
||||||
@ -269,7 +251,6 @@ func (h *handler) CreateRefund() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 记录积分按比例恢复(幂等增量)- 仅对 ActualAmount > 0 的订单执行
|
// 记录积分按比例恢复(幂等增量)- 仅对 ActualAmount > 0 的订单执行
|
||||||
if order.PointsAmount > 0 && order.ActualAmount > 0 {
|
if order.PointsAmount > 0 && order.ActualAmount > 0 {
|
||||||
restores, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(
|
restores, _ := h.readDB.UserPointsLedger.WithContext(ctx.RequestContext()).ReadDB().Where(
|
||||||
|
|||||||
@ -45,7 +45,6 @@ type ShippingOrderGroup struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
Count int64 `json:"count"` // 增加数量字段
|
|
||||||
} `json:"products"` // 商品详情列表
|
} `json:"products"` // 商品详情列表
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,32 +215,29 @@ func (h *handler) ListShippingOrders() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取商品信息(去重并计数)
|
// 获取商品信息(去重)
|
||||||
pidCounts := make(map[int64]int64)
|
pidSet := make(map[int64]struct{})
|
||||||
for _, pid := range a.pid {
|
for _, pid := range a.pid {
|
||||||
pidCounts[pid]++
|
pidSet[pid] = struct{}{}
|
||||||
}
|
}
|
||||||
var products []struct {
|
var products []struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
Count int64 `json:"count"`
|
|
||||||
}
|
}
|
||||||
for pid, count := range pidCounts {
|
for pid := range pidSet {
|
||||||
if prod, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(pid)).First(); prod != nil {
|
if prod, _ := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.ID.Eq(pid)).First(); prod != nil {
|
||||||
products = append(products, struct {
|
products = append(products, struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
Count int64 `json:"count"`
|
|
||||||
}{
|
}{
|
||||||
ID: prod.ID,
|
ID: prod.ID,
|
||||||
Name: prod.Name,
|
Name: prod.Name,
|
||||||
Image: prod.ImagesJSON, // 商品图片JSON
|
Image: prod.ImagesJSON, // 商品图片JSON
|
||||||
Price: prod.Price,
|
Price: prod.Price,
|
||||||
Count: count,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,7 +139,8 @@ func (h *handler) DeleteSystemCoupon() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
set := map[string]any{"deleted_at": time.Now()}
|
uid := int64(ctx.SessionUserInfo().Id)
|
||||||
|
set := map[string]any{"deleted_at": time.Now(), "deleted_by": uid}
|
||||||
if _, err := h.writeDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.SystemCoupons.ID.Eq(id)).Updates(set); err != nil {
|
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()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -20,9 +19,7 @@ type listUsersRequest struct {
|
|||||||
InviteCode string `form:"inviteCode"`
|
InviteCode string `form:"inviteCode"`
|
||||||
StartDate string `form:"startDate"`
|
StartDate string `form:"startDate"`
|
||||||
EndDate string `form:"endDate"`
|
EndDate string `form:"endDate"`
|
||||||
ID string `form:"id"`
|
ID *int64 `form:"id"`
|
||||||
DouyinID string `form:"douyin_id"`
|
|
||||||
DouyinUserID string `form:"douyin_user_id"`
|
|
||||||
}
|
}
|
||||||
type listUsersResponse struct {
|
type listUsersResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
@ -64,22 +61,13 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
if req.PageSize > 100 {
|
if req.PageSize > 100 {
|
||||||
req.PageSize = 100
|
req.PageSize = 100
|
||||||
}
|
}
|
||||||
u := h.readDB.Users
|
|
||||||
c := h.readDB.Channels
|
|
||||||
|
|
||||||
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
q := h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
LeftJoin(c, c.ID.EqCol(u.ChannelID)).
|
LeftJoin(h.readDB.Channels, h.readDB.Channels.ID.EqCol(h.readDB.Users.ChannelID)).
|
||||||
Select(
|
Select(h.readDB.Users.ALL, h.readDB.Channels.Name.As("channel_name"), h.readDB.Channels.Code.As("channel_code"))
|
||||||
u.ALL,
|
|
||||||
c.Name.As("channel_name"),
|
|
||||||
c.Code.As("channel_code"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 应用搜索条件
|
// 应用搜索条件
|
||||||
if req.ID != "" {
|
if req.ID != nil {
|
||||||
if id, err := strconv.ParseInt(req.ID, 10, 64); err == nil {
|
q = q.Where(h.readDB.Users.ID.Eq(*req.ID))
|
||||||
q = q.Where(h.readDB.Users.ID.Eq(id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if req.Nickname != "" {
|
if req.Nickname != "" {
|
||||||
q = q.Where(h.readDB.Users.Nickname.Like("%" + req.Nickname + "%"))
|
q = q.Where(h.readDB.Users.Nickname.Like("%" + req.Nickname + "%"))
|
||||||
@ -99,12 +87,6 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
q = q.Where(h.readDB.Users.CreatedAt.Lte(endTime))
|
q = q.Where(h.readDB.Users.CreatedAt.Lte(endTime))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.DouyinID != "" {
|
|
||||||
q = q.Where(h.readDB.Users.DouyinID.Like("%" + req.DouyinID + "%"))
|
|
||||||
}
|
|
||||||
if req.DouyinUserID != "" {
|
|
||||||
q = q.Where(h.readDB.Users.DouyinUserID.Like("%" + req.DouyinUserID + "%"))
|
|
||||||
}
|
|
||||||
|
|
||||||
total, err := q.Count()
|
total, err := q.Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -161,7 +143,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
Group(h.readDB.UserPoints.UserID).
|
Group(h.readDB.UserPoints.UserID).
|
||||||
Scan(&bRes)
|
Scan(&bRes)
|
||||||
for _, b := range bRes {
|
for _, b := range bRes {
|
||||||
pointBalances[b.UserID] = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), b.Points))
|
pointBalances[b.UserID] = b.Points
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,13 +169,10 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
// 批量查询消费统计
|
// 批量查询消费统计
|
||||||
todayConsume := make(map[int64]int64)
|
todayConsume := make(map[int64]int64)
|
||||||
sevenDayConsume := make(map[int64]int64)
|
sevenDayConsume := make(map[int64]int64)
|
||||||
thirtyDayConsume := make(map[int64]int64)
|
|
||||||
totalConsume := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
if len(userIDs) > 0 {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
sevenDayStart := todayStart.AddDate(0, 0, -6)
|
sevenDayStart := todayStart.AddDate(0, 0, -6) // 包括今天共7天
|
||||||
thirtyDayStart := todayStart.AddDate(0, 0, -29)
|
|
||||||
|
|
||||||
type consumeResult struct {
|
type consumeResult struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
@ -205,8 +184,7 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)). // 2=已支付
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&todayRes)
|
Scan(&todayRes)
|
||||||
@ -219,172 +197,13 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)). // 2=已支付
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
||||||
Group(h.readDB.Orders.UserID).
|
Group(h.readDB.Orders.UserID).
|
||||||
Scan(&sevenRes)
|
Scan(&sevenRes)
|
||||||
for _, r := range sevenRes {
|
for _, r := range sevenRes {
|
||||||
sevenDayConsume[r.UserID] = r.Amount
|
sevenDayConsume[r.UserID] = r.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
// 近30天消费
|
|
||||||
var thirtyRes []consumeResult
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
|
||||||
Group(h.readDB.Orders.UserID).
|
|
||||||
Scan(&thirtyRes)
|
|
||||||
for _, r := range thirtyRes {
|
|
||||||
thirtyDayConsume[r.UserID] = r.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
// 累计消费
|
|
||||||
var totalRes []consumeResult
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.UserID, h.readDB.Orders.ActualAmount.Sum().As("amount")).
|
|
||||||
Where(h.readDB.Orders.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Group(h.readDB.Orders.UserID).
|
|
||||||
Scan(&totalRes)
|
|
||||||
for _, r := range totalRes {
|
|
||||||
totalConsume[r.UserID] = r.Amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询邀请人数
|
|
||||||
inviteCounts := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type countResult struct {
|
|
||||||
InviterID int64
|
|
||||||
Count int64
|
|
||||||
}
|
|
||||||
var counts []countResult
|
|
||||||
h.readDB.Users.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Users.InviterID, h.readDB.Users.ID.Count().As("count")).
|
|
||||||
Where(h.readDB.Users.InviterID.In(userIDs...)).
|
|
||||||
Group(h.readDB.Users.InviterID).
|
|
||||||
Scan(&counts)
|
|
||||||
for _, c := range counts {
|
|
||||||
inviteCounts[c.InviterID] = c.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询次数卡数量
|
|
||||||
gamePassCounts := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type countResult struct {
|
|
||||||
UserID int64
|
|
||||||
Count int64
|
|
||||||
}
|
|
||||||
var counts []countResult
|
|
||||||
now := time.Now()
|
|
||||||
h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.UserGamePasses.UserID, h.readDB.UserGamePasses.Remaining.Sum().As("count")).
|
|
||||||
Where(h.readDB.UserGamePasses.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.UserGamePasses.Remaining.Gt(0)).
|
|
||||||
Where(h.readDB.UserGamePasses.Where(h.readDB.UserGamePasses.ExpiredAt.Gt(now)).Or(h.readDB.UserGamePasses.ExpiredAt.IsNull())).
|
|
||||||
Group(h.readDB.UserGamePasses.UserID).
|
|
||||||
Scan(&counts)
|
|
||||||
for _, c := range counts {
|
|
||||||
gamePassCounts[c.UserID] = c.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询游戏资格数量
|
|
||||||
gameTicketCounts := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type countResult struct {
|
|
||||||
UserID int64
|
|
||||||
Count int64
|
|
||||||
}
|
|
||||||
var counts []countResult
|
|
||||||
h.readDB.UserGameTickets.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.UserGameTickets.UserID, h.readDB.UserGameTickets.Available.Sum().As("count")).
|
|
||||||
Where(h.readDB.UserGameTickets.UserID.In(userIDs...)).
|
|
||||||
Group(h.readDB.UserGameTickets.UserID).
|
|
||||||
Scan(&counts)
|
|
||||||
for _, c := range counts {
|
|
||||||
gameTicketCounts[c.UserID] = c.Count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询持有商品价值
|
|
||||||
inventoryValues := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type invResult struct {
|
|
||||||
UserID int64
|
|
||||||
Value int64
|
|
||||||
}
|
|
||||||
var invRes []invResult
|
|
||||||
h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
LeftJoin(h.readDB.Products, h.readDB.Products.ID.EqCol(h.readDB.UserInventory.ProductID)).
|
|
||||||
Select(h.readDB.UserInventory.UserID, h.readDB.Products.Price.Sum().As("value")).
|
|
||||||
Where(h.readDB.UserInventory.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.UserInventory.Status.Eq(1)). // 1=持有
|
|
||||||
Group(h.readDB.UserInventory.UserID).
|
|
||||||
Scan(&invRes)
|
|
||||||
for _, r := range invRes {
|
|
||||||
inventoryValues[r.UserID] = r.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询优惠券价值(余额之和)
|
|
||||||
couponValues := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type valResult struct {
|
|
||||||
UserID int64
|
|
||||||
Value int64
|
|
||||||
}
|
|
||||||
var vRes []valResult
|
|
||||||
h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.UserCoupons.UserID, h.readDB.UserCoupons.BalanceAmount.Sum().As("value")).
|
|
||||||
Where(h.readDB.UserCoupons.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.UserCoupons.Status.Eq(1)).
|
|
||||||
Group(h.readDB.UserCoupons.UserID).
|
|
||||||
Scan(&vRes)
|
|
||||||
for _, v := range vRes {
|
|
||||||
couponValues[v.UserID] = v.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询道具卡价值
|
|
||||||
itemCardValues := make(map[int64]int64)
|
|
||||||
if len(userIDs) > 0 {
|
|
||||||
type valResult struct {
|
|
||||||
UserID int64
|
|
||||||
Value int64
|
|
||||||
}
|
|
||||||
var vRes []valResult
|
|
||||||
h.readDB.UserItemCards.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
LeftJoin(h.readDB.SystemItemCards, h.readDB.SystemItemCards.ID.EqCol(h.readDB.UserItemCards.CardID)).
|
|
||||||
Select(h.readDB.UserItemCards.UserID, h.readDB.SystemItemCards.Price.Sum().As("value")).
|
|
||||||
Where(h.readDB.UserItemCards.UserID.In(userIDs...)).
|
|
||||||
Where(h.readDB.UserItemCards.Status.Eq(1)).
|
|
||||||
Group(h.readDB.UserItemCards.UserID).
|
|
||||||
Scan(&vRes)
|
|
||||||
for _, v := range vRes {
|
|
||||||
itemCardValues[v.UserID] = v.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询所有用户的邀请人昵称
|
|
||||||
inviterNicknames := make(map[int64]string)
|
|
||||||
inviterIDs := make([]int64, 0)
|
|
||||||
for _, v := range rows {
|
|
||||||
if v.InviterID > 0 {
|
|
||||||
inviterIDs = append(inviterIDs, v.InviterID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(inviterIDs) > 0 {
|
|
||||||
inviters, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.In(inviterIDs...)).Find()
|
|
||||||
for _, inv := range inviters {
|
|
||||||
inviterNicknames[inv.ID] = inv.Nickname
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rsp.Page = req.Page
|
rsp.Page = req.Page
|
||||||
@ -392,44 +211,21 @@ func (h *handler) ListAppUsers() core.HandlerFunc {
|
|||||||
rsp.Total = total
|
rsp.Total = total
|
||||||
rsp.List = make([]adminUserItem, len(rows))
|
rsp.List = make([]adminUserItem, len(rows))
|
||||||
for i, v := range rows {
|
for i, v := range rows {
|
||||||
pointsBal := pointBalances[v.ID]
|
|
||||||
invVal := inventoryValues[v.ID]
|
|
||||||
cpVal := couponValues[v.ID]
|
|
||||||
icVal := itemCardValues[v.ID]
|
|
||||||
gpCount := gamePassCounts[v.ID]
|
|
||||||
gtCount := gameTicketCounts[v.ID]
|
|
||||||
|
|
||||||
// 总资产估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
|
||||||
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
|
||||||
assetVal := pointsBal*100 + invVal + cpVal + icVal + gpCount*200
|
|
||||||
|
|
||||||
rsp.List[i] = adminUserItem{
|
rsp.List[i] = adminUserItem{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
Nickname: v.Nickname,
|
Nickname: v.Nickname,
|
||||||
Avatar: v.Avatar,
|
Avatar: v.Avatar,
|
||||||
InviteCode: v.InviteCode,
|
InviteCode: v.InviteCode,
|
||||||
InviterID: v.InviterID,
|
InviterID: v.InviterID,
|
||||||
InviterNickname: inviterNicknames[v.InviterID],
|
|
||||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
DouyinID: v.DouyinID,
|
DouyinID: v.DouyinID,
|
||||||
DouyinUserID: v.DouyinUserID,
|
|
||||||
Mobile: v.Mobile,
|
|
||||||
Remark: v.Remark,
|
|
||||||
ChannelName: v.ChannelName,
|
ChannelName: v.ChannelName,
|
||||||
ChannelCode: v.ChannelCode,
|
ChannelCode: v.ChannelCode,
|
||||||
PointsBalance: pointsBal,
|
PointsBalance: pointBalances[v.ID],
|
||||||
CouponsCount: couponCounts[v.ID],
|
CouponsCount: couponCounts[v.ID],
|
||||||
ItemCardsCount: cardCounts[v.ID],
|
ItemCardsCount: cardCounts[v.ID],
|
||||||
TodayConsume: todayConsume[v.ID],
|
TodayConsume: todayConsume[v.ID],
|
||||||
SevenDayConsume: sevenDayConsume[v.ID],
|
SevenDayConsume: sevenDayConsume[v.ID],
|
||||||
ThirtyDayConsume: thirtyDayConsume[v.ID],
|
|
||||||
TotalConsume: totalConsume[v.ID],
|
|
||||||
InviteCount: inviteCounts[v.ID],
|
|
||||||
GamePassCount: gpCount,
|
|
||||||
GameTicketCount: gtCount,
|
|
||||||
InventoryValue: invVal,
|
|
||||||
TotalAssetValue: assetVal,
|
|
||||||
Status: v.Status,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
@ -503,8 +299,6 @@ type listOrdersResponse struct {
|
|||||||
type listInventoryRequest struct {
|
type listInventoryRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
PageSize int `form:"page_size"`
|
PageSize int `form:"page_size"`
|
||||||
Keyword string `form:"keyword"` // 搜索关键词(商品名称)
|
|
||||||
Status int32 `form:"status"` // 状态筛选:0=全部, 1=持有, 2=作废, 3=已使用
|
|
||||||
}
|
}
|
||||||
type listInventoryResponse struct {
|
type listInventoryResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
@ -561,8 +355,6 @@ func (h *handler) ListUserOrders() core.HandlerFunc {
|
|||||||
// @Param user_id path integer true "用户ID"
|
// @Param user_id path integer true "用户ID"
|
||||||
// @Param page query int true "页码" default(1)
|
// @Param page query int true "页码" default(1)
|
||||||
// @Param page_size query int true "每页数量,最多100" default(20)
|
// @Param page_size query int true "每页数量,最多100" default(20)
|
||||||
// @Param keyword query string false "搜索关键词"
|
|
||||||
// @Param status query int false "状态筛选: 0=全部, 1=持有, 2=作废, 3=已使用"
|
|
||||||
// @Success 200 {object} listInventoryResponse
|
// @Success 200 {object} listInventoryResponse
|
||||||
// @Failure 400 {object} code.Failure
|
// @Failure 400 {object} code.Failure
|
||||||
// @Router /api/admin/users/{user_id}/inventory [get]
|
// @Router /api/admin/users/{user_id}/inventory [get]
|
||||||
@ -580,139 +372,7 @@ func (h *handler) ListUserInventory() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rows, total, err := h.userSvc.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize)
|
||||||
// 处理分页参数
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize <= 0 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
if req.PageSize > 100 {
|
|
||||||
req.PageSize = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有搜索关键词,使用带搜索的查询
|
|
||||||
if req.Keyword != "" {
|
|
||||||
// 联表查询以支持按商品名称搜索
|
|
||||||
ui := h.readDB.UserInventory
|
|
||||||
p := h.readDB.Products
|
|
||||||
|
|
||||||
// Check if keyword is numeric
|
|
||||||
numKeyword, errNum := strconv.ParseInt(req.Keyword, 10, 64)
|
|
||||||
|
|
||||||
// Count query logic
|
|
||||||
countQ := h.readDB.UserInventory.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
LeftJoin(p, p.ID.EqCol(ui.ProductID)).
|
|
||||||
Where(ui.UserID.Eq(userID))
|
|
||||||
|
|
||||||
// 应用状态筛选
|
|
||||||
if req.Status > 0 {
|
|
||||||
countQ = countQ.Where(ui.Status.Eq(req.Status))
|
|
||||||
} else {
|
|
||||||
// 默认只过滤掉已软删除的记录(如果有的话,status=2是作废,通常后台要能看到作废的,所以这里如果不传status默认查所有非删除的?)
|
|
||||||
// 既然是管理端,如果不传status,应该显示所有状态的记录
|
|
||||||
}
|
|
||||||
|
|
||||||
if errNum == nil {
|
|
||||||
// Keyword is numeric, search by name OR ID OR OrderID
|
|
||||||
countQ = countQ.Where(
|
|
||||||
ui.Where(p.Name.Like("%" + req.Keyword + "%")).
|
|
||||||
Or(ui.ID.Eq(numKeyword)).
|
|
||||||
Or(ui.OrderID.Eq(numKeyword)),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Keyword is not numeric, search by name only
|
|
||||||
countQ = countQ.Where(p.Name.Like("%" + req.Keyword + "%"))
|
|
||||||
}
|
|
||||||
|
|
||||||
total, err := countQ.Count()
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询资产数据
|
|
||||||
type inventoryRow struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
ProductID int64
|
|
||||||
OrderID int64
|
|
||||||
ActivityID int64
|
|
||||||
RewardID int64
|
|
||||||
Status int32
|
|
||||||
Remark string
|
|
||||||
CreatedAt string
|
|
||||||
UpdatedAt string
|
|
||||||
ProductName string
|
|
||||||
ProductImages string
|
|
||||||
ProductPrice int64
|
|
||||||
}
|
|
||||||
var rows []inventoryRow
|
|
||||||
|
|
||||||
sql := `
|
|
||||||
SELECT ui.id, ui.user_id, ui.product_id, ui.order_id, ui.activity_id, ui.reward_id,
|
|
||||||
ui.status, ui.remark, ui.created_at, ui.updated_at,
|
|
||||||
p.name as product_name, p.images_json as product_images, p.price as product_price
|
|
||||||
FROM user_inventory ui
|
|
||||||
LEFT JOIN products p ON p.id = ui.product_id
|
|
||||||
WHERE ui.user_id = ?
|
|
||||||
`
|
|
||||||
var args []interface{}
|
|
||||||
args = append(args, userID)
|
|
||||||
|
|
||||||
if req.Status > 0 {
|
|
||||||
sql += " AND ui.status = ?"
|
|
||||||
args = append(args, req.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errNum == nil {
|
|
||||||
sql += " AND (p.name LIKE ? OR ui.id = ? OR ui.order_id = ?)"
|
|
||||||
args = append(args, "%"+req.Keyword+"%", numKeyword, numKeyword)
|
|
||||||
} else {
|
|
||||||
sql += " AND p.name LIKE ?"
|
|
||||||
args = append(args, "%"+req.Keyword+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
sql += " ORDER BY ui.id DESC LIMIT ? OFFSET ?"
|
|
||||||
args = append(args, req.PageSize, (req.Page-1)*req.PageSize)
|
|
||||||
|
|
||||||
err = h.repo.GetDbR().Raw(sql, args...).Scan(&rows).Error
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换结果
|
|
||||||
items := make([]*user.InventoryWithProduct, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
items[i] = &user.InventoryWithProduct{
|
|
||||||
UserInventory: &model.UserInventory{
|
|
||||||
ID: r.ID,
|
|
||||||
UserID: r.UserID,
|
|
||||||
ProductID: r.ProductID,
|
|
||||||
OrderID: r.OrderID,
|
|
||||||
ActivityID: r.ActivityID,
|
|
||||||
RewardID: r.RewardID,
|
|
||||||
Status: r.Status,
|
|
||||||
Remark: r.Remark,
|
|
||||||
},
|
|
||||||
ProductName: r.ProductName,
|
|
||||||
ProductImages: r.ProductImages,
|
|
||||||
ProductPrice: r.ProductPrice,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rsp.Page = req.Page
|
|
||||||
rsp.PageSize = req.PageSize
|
|
||||||
rsp.Total = total
|
|
||||||
rsp.List = items
|
|
||||||
ctx.Payload(rsp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无搜索关键词时使用原有逻辑
|
|
||||||
rows, total, err := h.userSvc.ListInventoryWithProduct(ctx.RequestContext(), userID, req.Page, req.PageSize, req.Status)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20105, err.Error()))
|
||||||
return
|
return
|
||||||
@ -852,17 +512,16 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
MinSpend int64
|
MinSpend int64
|
||||||
BalanceAmount int64
|
BalanceAmount int64
|
||||||
}
|
}
|
||||||
|
q := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
q := base.
|
LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)).
|
||||||
Select(
|
Select(
|
||||||
h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status,
|
h.readDB.UserCoupons.ID, h.readDB.UserCoupons.CouponID, h.readDB.UserCoupons.Status,
|
||||||
h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt,
|
h.readDB.UserCoupons.UsedOrderID, h.readDB.UserCoupons.UsedAt, h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd,
|
||||||
h.readDB.UserCoupons.ValidStart, h.readDB.UserCoupons.ValidEnd,
|
h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType, h.readDB.SystemCoupons.DiscountType,
|
||||||
h.readDB.SystemCoupons.Name, h.readDB.SystemCoupons.ScopeType,
|
h.readDB.SystemCoupons.DiscountValue, h.readDB.SystemCoupons.MinSpend,
|
||||||
h.readDB.SystemCoupons.DiscountType, h.readDB.SystemCoupons.DiscountValue,
|
h.readDB.UserCoupons.BalanceAmount,
|
||||||
h.readDB.SystemCoupons.MinSpend,
|
|
||||||
).
|
).
|
||||||
LeftJoin(h.readDB.SystemCoupons, h.readDB.SystemCoupons.ID.EqCol(h.readDB.UserCoupons.CouponID)).
|
Where(h.readDB.UserCoupons.UserID.Eq(userID)).
|
||||||
Order(h.readDB.UserCoupons.ID.Desc()).
|
Order(h.readDB.UserCoupons.ID.Desc()).
|
||||||
Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize)
|
Limit(req.PageSize).Offset((req.Page - 1) * req.PageSize)
|
||||||
|
|
||||||
@ -897,164 +556,6 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuditLogItem struct {
|
|
||||||
CreatedAt string `json:"created_at"` // 时间
|
|
||||||
Category string `json:"category"` // 大类: points/order/shipping/draw
|
|
||||||
SubType string `json:"sub_type"` // 子类: action/status
|
|
||||||
AmountStr string `json:"amount_str"` // 金额/数值变化 (带符号字符串)
|
|
||||||
RefInfo string `json:"ref_info"` // 关联信息 (RefID/OrderNo/ExpressNo)
|
|
||||||
DetailInfo string `json:"detail_info"` // 详细描述 (Remark/PrizeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
type listAuditLogsResponse struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Total int64 `json:"total"` // 由于UNION ALL分页较难精确Count Total,这里可能返回估算值或分步Count,为简化MVP先只做翻页不用Total或者Total设为0
|
|
||||||
List []AuditLogItem `json:"list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUserAuditLogs 查看用户行为审计日志
|
|
||||||
// @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 "每页数量" default(20)
|
|
||||||
// @Success 200 {object} listAuditLogsResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/users/{user_id}/audit [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) ListUserAuditLogs() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(listInvitesRequest) // 复用分页参数结构
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if req.Page <= 0 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize <= 0 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (req.Page - 1) * req.PageSize
|
|
||||||
limit := req.PageSize
|
|
||||||
|
|
||||||
var logs []AuditLogItem
|
|
||||||
|
|
||||||
// 构建 UNION ALL 查询
|
|
||||||
// 1. 积分流水
|
|
||||||
// 2. 订单记录
|
|
||||||
// 3. 发货记录
|
|
||||||
// 4. 抽奖记录 (只看中奖的? 或者全部? 这里先只看中奖 IsWinner=1 避免数据量太大)
|
|
||||||
|
|
||||||
sql := `
|
|
||||||
SELECT * FROM (
|
|
||||||
-- 1. Points Ledger
|
|
||||||
SELECT
|
|
||||||
created_at,
|
|
||||||
'points' as category,
|
|
||||||
CONVERT(action USING utf8mb4) as sub_type,
|
|
||||||
CAST(points AS CHAR) as amount_str,
|
|
||||||
CONCAT(CONVERT(ref_table USING utf8mb4), ':', CONVERT(ref_id USING utf8mb4)) as ref_info,
|
|
||||||
CONVERT(remark USING utf8mb4) as detail_info
|
|
||||||
FROM user_points_ledger
|
|
||||||
WHERE user_id = ?
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- 2. Orders
|
|
||||||
SELECT
|
|
||||||
created_at,
|
|
||||||
'order' as category,
|
|
||||||
'paid' as sub_type,
|
|
||||||
CAST(actual_amount AS CHAR) as amount_str,
|
|
||||||
CONVERT(order_no USING utf8mb4) as ref_info,
|
|
||||||
CONVERT(remark USING utf8mb4) as detail_info
|
|
||||||
FROM orders
|
|
||||||
WHERE user_id = ? AND status = 2
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- 3. Shipping Records
|
|
||||||
SELECT
|
|
||||||
created_at,
|
|
||||||
'shipping' as category,
|
|
||||||
CAST(status AS CHAR) as sub_type,
|
|
||||||
CAST(quantity AS CHAR) as amount_str,
|
|
||||||
CONCAT(IFNULL(CONVERT(express_code USING utf8mb4),''), ':', IFNULL(CONVERT(express_no USING utf8mb4),'')) as ref_info,
|
|
||||||
CONVERT(remark USING utf8mb4) as detail_info
|
|
||||||
FROM shipping_records
|
|
||||||
WHERE user_id = ?
|
|
||||||
|
|
||||||
UNION ALL
|
|
||||||
|
|
||||||
-- 4. Draw Logs (Winners)
|
|
||||||
SELECT
|
|
||||||
l.created_at,
|
|
||||||
'draw' as category,
|
|
||||||
IF(l.is_winner=1, 'win', 'lose') as sub_type,
|
|
||||||
CAST(1 AS CHAR) as amount_str,
|
|
||||||
CAST(l.order_id AS CHAR) as ref_info,
|
|
||||||
CONCAT(
|
|
||||||
'游戏: ', IFNULL(CONVERT(act.name USING utf8mb4), '未知'),
|
|
||||||
' | 奖品: ', IFNULL(CONVERT(prod.name USING utf8mb4), '未知'),
|
|
||||||
' | 级别: ', CASE l.level WHEN 1 THEN 'S' WHEN 2 THEN 'A' WHEN 3 THEN 'B' WHEN 4 THEN 'C' ELSE CAST(l.level AS CHAR) END
|
|
||||||
) as detail_info
|
|
||||||
FROM activity_draw_logs l
|
|
||||||
LEFT JOIN activity_issues issue ON l.issue_id = issue.id
|
|
||||||
LEFT JOIN activities act ON issue.activity_id = act.id
|
|
||||||
LEFT JOIN activity_reward_settings reward ON l.reward_id = reward.id
|
|
||||||
LEFT JOIN products prod ON reward.product_id = prod.id
|
|
||||||
WHERE l.user_id = ? AND l.is_winner = 1
|
|
||||||
) as combined_logs
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`
|
|
||||||
|
|
||||||
if err := h.repo.GetDbR().Raw(sql, userID, userID, userID, userID, limit, offset).Scan(&logs).Error; err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化处理 (Optional)
|
|
||||||
for i := range logs {
|
|
||||||
// 将时间标准化
|
|
||||||
if t, err := time.Parse(time.RFC3339, logs[i].CreatedAt); err == nil {
|
|
||||||
logs[i].CreatedAt = t.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
// 翻译 Shipping Status 等 (可选项,也可以前端做)
|
|
||||||
if logs[i].Category == "shipping" {
|
|
||||||
switch logs[i].SubType {
|
|
||||||
case "1":
|
|
||||||
logs[i].SubType = "待发货"
|
|
||||||
case "2":
|
|
||||||
logs[i].SubType = "已发货"
|
|
||||||
case "3":
|
|
||||||
logs[i].SubType = "已签收"
|
|
||||||
case "4":
|
|
||||||
logs[i].SubType = "异常"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&listAuditLogsResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: 0, // 为了性能暂时忽略
|
|
||||||
List: logs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullableToString(s *string) string {
|
func nullableToString(s *string) string {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return ""
|
return ""
|
||||||
@ -1066,22 +567,11 @@ type listPointsRequest struct {
|
|||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
PageSize int `form:"page_size"`
|
PageSize int `form:"page_size"`
|
||||||
}
|
}
|
||||||
type adminUserPointsLedgerItem struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Points float64 `json:"points"` // 改为 float64 支持小数积分
|
|
||||||
RefTable string `json:"ref_table"`
|
|
||||||
RefID string `json:"ref_id"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listPointsResponse struct {
|
type listPointsResponse struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
List []adminUserPointsLedgerItem `json:"list"`
|
List []*model.UserPointsLedger `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUserPoints 查看用户积分记录
|
// ListUserPoints 查看用户积分记录
|
||||||
@ -1118,20 +608,7 @@ func (h *handler) ListUserPoints() core.HandlerFunc {
|
|||||||
rsp.Page = req.Page
|
rsp.Page = req.Page
|
||||||
rsp.PageSize = req.PageSize
|
rsp.PageSize = req.PageSize
|
||||||
rsp.Total = total
|
rsp.Total = total
|
||||||
// Convert ledger items
|
rsp.List = items
|
||||||
rsp.List = make([]adminUserPointsLedgerItem, len(items))
|
|
||||||
for i, v := range items {
|
|
||||||
rsp.List[i] = adminUserPointsLedgerItem{
|
|
||||||
ID: v.ID,
|
|
||||||
UserID: v.UserID,
|
|
||||||
Action: v.Action,
|
|
||||||
Points: h.userSvc.CentsToPointsFloat(ctx.RequestContext(), v.Points),
|
|
||||||
RefTable: v.RefTable,
|
|
||||||
RefID: v.RefID,
|
|
||||||
Remark: v.Remark,
|
|
||||||
CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1146,12 +623,8 @@ type adminUserItem struct {
|
|||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
InviterID int64 `json:"inviter_id"`
|
InviterID int64 `json:"inviter_id"`
|
||||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
DouyinID string `json:"douyin_id"`
|
DouyinID string `json:"douyin_id"`
|
||||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
|
||||||
Mobile string `json:"mobile"` // 手机号
|
|
||||||
Remark string `json:"remark"` // 备注
|
|
||||||
ChannelName string `json:"channel_name"`
|
ChannelName string `json:"channel_name"`
|
||||||
ChannelCode string `json:"channel_code"`
|
ChannelCode string `json:"channel_code"`
|
||||||
PointsBalance int64 `json:"points_balance"`
|
PointsBalance int64 `json:"points_balance"`
|
||||||
@ -1159,14 +632,6 @@ type adminUserItem struct {
|
|||||||
ItemCardsCount int64 `json:"item_cards_count"`
|
ItemCardsCount int64 `json:"item_cards_count"`
|
||||||
TodayConsume int64 `json:"today_consume"`
|
TodayConsume int64 `json:"today_consume"`
|
||||||
SevenDayConsume int64 `json:"seven_day_consume"`
|
SevenDayConsume int64 `json:"seven_day_consume"`
|
||||||
ThirtyDayConsume int64 `json:"thirty_day_consume"` // 近30天消费
|
|
||||||
TotalConsume int64 `json:"total_consume"` // 累计消费
|
|
||||||
InviteCount int64 `json:"invite_count"` // 邀请人数
|
|
||||||
GamePassCount int64 `json:"game_pass_count"` // 次数卡数量
|
|
||||||
GameTicketCount int64 `json:"game_ticket_count"` // 游戏资格数量
|
|
||||||
InventoryValue int64 `json:"inventory_value"` // 持有商品总价值
|
|
||||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
|
||||||
Status int32 `json:"status"` // 用户状态:1正常 2禁用 3黑名单
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
// ListAppUsers 管理端用户列表GetUserPointsBalance 查看用户积分余额
|
||||||
@ -1193,13 +658,13 @@ func (h *handler) GetUserPointsBalance() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20107, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rsp.Balance = int64(h.userSvc.CentsToPointsFloat(ctx.RequestContext(), total))
|
rsp.Balance = total
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type addPointsRequest struct {
|
type addPointsRequest struct {
|
||||||
Points float64 `json:"points"` // 正数=增加,负数=扣减
|
Points int64 `json:"points"` // 正数=增加,负数=扣减
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
ValidDays *int `json:"valid_days"`
|
ValidDays *int `json:"valid_days"`
|
||||||
@ -1238,16 +703,10 @@ func (h *handler) AddUserPoints() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将浮点数积分转换为分(Cents)
|
|
||||||
// 1 积分 = 1 元 = 100 分
|
|
||||||
// 使用 math.Round 避免精度问题
|
|
||||||
pointsCents := int64(math.Round(req.Points * 100))
|
|
||||||
|
|
||||||
// 如果是扣减积分,先检查余额
|
// 如果是扣减积分,先检查余额
|
||||||
if pointsCents < 0 {
|
if req.Points < 0 {
|
||||||
balance, _ := h.userSvc.GetPointsBalance(ctx.RequestContext(), userID)
|
balance, _ := h.userSvc.GetPointsBalance(ctx.RequestContext(), userID)
|
||||||
deductCents := -pointsCents
|
if balance+req.Points < 0 {
|
||||||
if balance < deductCents {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, "积分余额不足,无法扣减"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, "积分余额不足,无法扣减"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1257,15 +716,14 @@ func (h *handler) AddUserPoints() core.HandlerFunc {
|
|||||||
var validEnd *time.Time
|
var validEnd *time.Time
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
// 只有增加积分时才设置有效期
|
// 只有增加积分时才设置有效期
|
||||||
if pointsCents > 0 {
|
if req.Points > 0 {
|
||||||
validStart = &now
|
validStart = &now
|
||||||
if req.ValidDays != nil && *req.ValidDays > 0 {
|
if req.ValidDays != nil && *req.ValidDays > 0 {
|
||||||
ve := now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
|
ve := now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour)
|
||||||
validEnd = &ve
|
validEnd = &ve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := h.userSvc.AddPoints(ctx.RequestContext(), userID, req.Points, req.Kind, req.Remark, validStart, validEnd); err != nil {
|
||||||
if err := h.userSvc.AddPoints(ctx.RequestContext(), userID, pointsCents, req.Kind, req.Remark, validStart, validEnd); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1564,145 +1022,3 @@ func (h *handler) ListUserCouponUsage() core.HandlerFunc {
|
|||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LinkUserDouyinRequest 关联用户抖音账号请求
|
|
||||||
type LinkUserDouyinRequest struct {
|
|
||||||
DouyinUserID string `json:"douyin_user_id" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserDouyinID 更新用户的抖音账号ID
|
|
||||||
// @Summary 更新用户抖音ID
|
|
||||||
// @Description 管理员绑定或修改用户的抖音账号ID
|
|
||||||
// @Tags 管理端.用户
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param user_id path integer true "用户ID"
|
|
||||||
// @Param body body LinkUserDouyinRequest true "抖音用户ID"
|
|
||||||
// @Success 200 {object} map[string]any
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/users/{user_id}/douyin_user_id [put]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) UpdateUserDouyinID() 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(LinkUserDouyinRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户抖音ID
|
|
||||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
|
||||||
Update(h.writeDB.Users.DouyinUserID, req.DouyinUserID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20301, "更新失败: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{
|
|
||||||
"success": true,
|
|
||||||
"message": "抖音ID更新成功",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateUserRemarkRequest 更新用户备注请求
|
|
||||||
type updateUserRemarkRequest struct {
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserRemark 更新用户备注
|
|
||||||
// @Summary 更新用户备注
|
|
||||||
// @Description 管理员修改用户备注
|
|
||||||
// @Tags 管理端.用户
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param user_id path integer true "用户ID"
|
|
||||||
// @Param body body updateUserRemarkRequest true "备注信息"
|
|
||||||
// @Success 200 {object} map[string]any
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/users/{user_id}/remark [put]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) UpdateUserRemark() 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(updateUserRemarkRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户备注
|
|
||||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
|
||||||
Update(h.writeDB.Users.Remark, req.Remark)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20302, "更新失败: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{
|
|
||||||
"success": true,
|
|
||||||
"message": "备注更新成功",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateUserStatusRequest struct {
|
|
||||||
Status int32 `json:"status" form:"status"` // 1=正常 2=禁用 3=黑名单
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserStatus 修改用户状态
|
|
||||||
// @Summary 修改用户状态
|
|
||||||
// @Description 管理员修改用户状态(1正常 2禁用 3黑名单)
|
|
||||||
// @Tags 管理端.用户
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param user_id path integer true "用户ID"
|
|
||||||
// @Param body body updateUserStatusRequest true "状态信息"
|
|
||||||
// @Success 200 {object} map[string]any
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/users/{user_id}/status [put]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) UpdateUserStatus() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(updateUserStatusRequest)
|
|
||||||
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 req.Status != 1 && req.Status != 2 && req.Status != 3 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "无效的状态值"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 Updates 以支持更新为 0 (虽然这里status不为0) 但 gorm Update 单列更安全
|
|
||||||
_, err = h.writeDB.Users.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.writeDB.Users.ID.Eq(userID)).
|
|
||||||
Update(h.writeDB.Users.Status, req.Status)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{"success": true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -62,10 +62,8 @@ func (h *handler) BatchAddUserPoints() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
||||||
// 将管理员输入的积分转换为分
|
|
||||||
amountCents, _ := h.userSvc.PointsToCents(ctx.RequestContext(), req.Amount)
|
|
||||||
for _, uid := range req.Users {
|
for _, uid := range req.Users {
|
||||||
if err := h.userSvc.AddPoints(ctx.RequestContext(), uid, amountCents, "manual", req.Reason, nil, nil); err != nil {
|
if err := h.userSvc.AddPoints(ctx.RequestContext(), uid, req.Amount, "manual", req.Reason, nil, nil); err != nil {
|
||||||
res.Failed++
|
res.Failed++
|
||||||
res.Details = append(res.Details, batchItemResult{UserID: uid, Status: "failed", Message: err.Error()})
|
res.Details = append(res.Details, batchItemResult{UserID: uid, Status: "failed", Message: err.Error()})
|
||||||
} else {
|
} else {
|
||||||
@ -92,19 +90,13 @@ func (h *handler) BatchAddUserCoupons() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "当前仅支持单用户操作,请仅选择1位用户"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "当前仅支持单用户操作,请仅选择1位用户"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.QuantityPerUser <= 0 {
|
if req.QuantityPerUser <= 0 { req.QuantityPerUser = 1 }
|
||||||
req.QuantityPerUser = 1
|
if req.QuantityPerUser > 5 { req.QuantityPerUser = 5 }
|
||||||
}
|
|
||||||
if req.QuantityPerUser > 5 {
|
|
||||||
req.QuantityPerUser = 5
|
|
||||||
}
|
|
||||||
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
||||||
for _, uid := range req.Users {
|
for _, uid := range req.Users {
|
||||||
ok := true
|
ok := true
|
||||||
for i := 0; i < req.QuantityPerUser; i++ {
|
for i := 0; i < req.QuantityPerUser; i++ {
|
||||||
if err := h.userSvc.AddCoupon(ctx.RequestContext(), uid, req.CouponID); err != nil {
|
if err := h.userSvc.AddCoupon(ctx.RequestContext(), uid, req.CouponID); err != nil { ok = false }
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
res.Success++
|
res.Success++
|
||||||
@ -133,12 +125,8 @@ func (h *handler) BatchGrantUserRewards() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "当前仅支持单用户操作,请仅选择1位用户"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "当前仅支持单用户操作,请仅选择1位用户"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Quantity <= 0 {
|
if req.Quantity <= 0 { req.Quantity = 1 }
|
||||||
req.Quantity = 1
|
if req.Quantity > 10 { req.Quantity = 10 }
|
||||||
}
|
|
||||||
if req.Quantity > 10 {
|
|
||||||
req.Quantity = 10
|
|
||||||
}
|
|
||||||
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
res := &batchResponse{Details: make([]batchItemResult, 0, len(req.Users))}
|
||||||
r := usersvc.GrantRewardRequest{ProductID: req.ProductID, Quantity: req.Quantity, ActivityID: req.ActivityID, RewardID: req.RewardID, AddressID: req.AddressID, Remark: req.Remark}
|
r := usersvc.GrantRewardRequest{ProductID: req.ProductID, Quantity: req.Quantity, ActivityID: req.ActivityID, RewardID: req.RewardID, AddressID: req.AddressID, Remark: req.Remark}
|
||||||
for _, uid := range req.Users {
|
for _, uid := range req.Users {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ type UserProfileResponse struct {
|
|||||||
ChannelID int64 `json:"channel_id"`
|
ChannelID int64 `json:"channel_id"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
DouyinID string `json:"douyin_id"`
|
DouyinID string `json:"douyin_id"`
|
||||||
DouyinUserID string `json:"douyin_user_id"` // 用户的抖音账号ID
|
|
||||||
InviterNickname string `json:"inviter_nickname"` // 邀请人昵称
|
|
||||||
|
|
||||||
// 邀请统计
|
// 邀请统计
|
||||||
InviteCount int64 `json:"invite_count"`
|
InviteCount int64 `json:"invite_count"`
|
||||||
@ -33,9 +31,6 @@ type UserProfileResponse struct {
|
|||||||
TotalRefunded int64 `json:"total_refunded"` // 累计退款
|
TotalRefunded int64 `json:"total_refunded"` // 累计退款
|
||||||
NetCashCost int64 `json:"net_cash_cost"` // 净现金支出
|
NetCashCost int64 `json:"net_cash_cost"` // 净现金支出
|
||||||
OrderCount int64 `json:"order_count"` // 订单数
|
OrderCount int64 `json:"order_count"` // 订单数
|
||||||
TodayPaid int64 `json:"today_paid"` // 当日支付
|
|
||||||
SevenDayPaid int64 `json:"seven_day_paid"` // 近7天支付
|
|
||||||
ThirtyDayPaid int64 `json:"thirty_day_paid"` // 近30天支付
|
|
||||||
} `json:"lifetime_stats"`
|
} `json:"lifetime_stats"`
|
||||||
|
|
||||||
// 当前资产快照
|
// 当前资产快照
|
||||||
@ -47,8 +42,6 @@ type UserProfileResponse struct {
|
|||||||
CouponValue int64 `json:"coupon_value"` // 持有优惠券价值
|
CouponValue int64 `json:"coupon_value"` // 持有优惠券价值
|
||||||
ItemCardCount int64 `json:"item_card_count"` // 持有道具卡数
|
ItemCardCount int64 `json:"item_card_count"` // 持有道具卡数
|
||||||
ItemCardValue int64 `json:"item_card_value"` // 持有道具卡价值
|
ItemCardValue int64 `json:"item_card_value"` // 持有道具卡价值
|
||||||
GamePassCount int64 `json:"game_pass_count"` // 持有次数卡数
|
|
||||||
GameTicketCount int64 `json:"game_ticket_count"` // 持有游戏资格数
|
|
||||||
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
TotalAssetValue int64 `json:"total_asset_value"` // 总资产估值
|
||||||
ProfitLossRatio float64 `json:"profit_loss_ratio"` // 累计盈亏比
|
ProfitLossRatio float64 `json:"profit_loss_ratio"` // 累计盈亏比
|
||||||
} `json:"current_assets"`
|
} `json:"current_assets"`
|
||||||
@ -89,85 +82,27 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
rsp.InviterID = user.InviterID
|
rsp.InviterID = user.InviterID
|
||||||
rsp.ChannelID = user.ChannelID
|
rsp.ChannelID = user.ChannelID
|
||||||
rsp.DouyinID = user.DouyinID
|
rsp.DouyinID = user.DouyinID
|
||||||
rsp.DouyinUserID = user.DouyinUserID
|
|
||||||
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
rsp.CreatedAt = user.CreatedAt.Format(time.RFC3339)
|
||||||
|
|
||||||
// 1.1 查询邀请人昵称
|
|
||||||
if user.InviterID > 0 {
|
|
||||||
inviter, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(user.InviterID)).First()
|
|
||||||
if inviter != nil {
|
|
||||||
rsp.InviterNickname = inviter.Nickname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 邀请统计
|
// 2. 邀请统计
|
||||||
rsp.InviteCount, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.InviterID.Eq(userID)).Count()
|
rsp.InviteCount, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.InviterID.Eq(userID)).Count()
|
||||||
|
|
||||||
// 3. 生命周期财务指标
|
// 3. 生命周期财务指标
|
||||||
// 3.1 消费统计
|
// 3.1 累计支付 & 订单数 - 只统计未退款的订单
|
||||||
type orderStats struct {
|
type orderStats struct {
|
||||||
TotalPaid *int64
|
TotalPaid int64
|
||||||
OrderCount int64
|
OrderCount int64
|
||||||
TodayPaid *int64
|
|
||||||
SevenDayPaid *int64
|
|
||||||
ThirtyDayPaid *int64
|
|
||||||
}
|
}
|
||||||
var os orderStats
|
var os orderStats
|
||||||
now := time.Now()
|
|
||||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
||||||
sevenDayStart := todayStart.AddDate(0, 0, -6)
|
|
||||||
thirtyDayStart := todayStart.AddDate(0, 0, -29)
|
|
||||||
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
Select(
|
Select(h.readDB.Orders.ActualAmount.Sum().As("total_paid"), h.readDB.Orders.ID.Count().As("order_count")).
|
||||||
h.readDB.Orders.ActualAmount.Sum().As("total_paid"),
|
|
||||||
h.readDB.Orders.ID.Count().As("order_count"),
|
|
||||||
).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
|
||||||
Scan(&os)
|
Scan(&os)
|
||||||
|
rsp.LifetimeStats.TotalPaid = os.TotalPaid
|
||||||
// 分阶段统计
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("today_paid")).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(todayStart)).
|
|
||||||
Scan(&os.TodayPaid)
|
|
||||||
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("seven_day_paid")).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(sevenDayStart)).
|
|
||||||
Scan(&os.SevenDayPaid)
|
|
||||||
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.ActualAmount.Sum().As("thirty_day_paid")).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 统计抽奖、对对碰、次卡购买(排除积分兑换)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(thirtyDayStart)).
|
|
||||||
Scan(&os.ThirtyDayPaid)
|
|
||||||
|
|
||||||
if os.TotalPaid != nil {
|
|
||||||
rsp.LifetimeStats.TotalPaid = *os.TotalPaid
|
|
||||||
}
|
|
||||||
rsp.LifetimeStats.OrderCount = os.OrderCount
|
rsp.LifetimeStats.OrderCount = os.OrderCount
|
||||||
if os.TodayPaid != nil {
|
|
||||||
rsp.LifetimeStats.TodayPaid = *os.TodayPaid
|
|
||||||
}
|
|
||||||
if os.SevenDayPaid != nil {
|
|
||||||
rsp.LifetimeStats.SevenDayPaid = *os.SevenDayPaid
|
|
||||||
}
|
|
||||||
if os.ThirtyDayPaid != nil {
|
|
||||||
rsp.LifetimeStats.ThirtyDayPaid = *os.ThirtyDayPaid
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2 累计退款
|
// 3.2 累计退款 - 显示实际退款金额(参考信息)
|
||||||
var totalRefunded int64
|
var totalRefunded int64
|
||||||
_ = h.repo.GetDbR().Raw(`
|
_ = h.repo.GetDbR().Raw(`
|
||||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
||||||
@ -176,11 +111,8 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
|
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
|
||||||
`, userID).Scan(&totalRefunded).Error
|
`, userID).Scan(&totalRefunded).Error
|
||||||
rsp.LifetimeStats.TotalRefunded = totalRefunded
|
rsp.LifetimeStats.TotalRefunded = totalRefunded
|
||||||
// 净现金投入 = 累计实付 - 累计退款
|
// 净投入 = 累计支付(因为已排除退款订单,所以不减退款)
|
||||||
rsp.LifetimeStats.NetCashCost = rsp.LifetimeStats.TotalPaid - totalRefunded
|
rsp.LifetimeStats.NetCashCost = rsp.LifetimeStats.TotalPaid
|
||||||
if rsp.LifetimeStats.NetCashCost < 0 {
|
|
||||||
rsp.LifetimeStats.NetCashCost = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 当前资产快照
|
// 4. 当前资产快照
|
||||||
// 4.1 积分余额
|
// 4.1 积分余额
|
||||||
@ -232,24 +164,11 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
rsp.CurrentAssets.ItemCardCount = cds.Count
|
rsp.CurrentAssets.ItemCardCount = cds.Count
|
||||||
rsp.CurrentAssets.ItemCardValue = cds.Value
|
rsp.CurrentAssets.ItemCardValue = cds.Value
|
||||||
|
|
||||||
// 4.5 持有次数卡
|
|
||||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(remaining), 0) FROM user_game_passes WHERE user_id = ? AND remaining > 0 AND (expired_at IS NULL OR expired_at > NOW())", userID).Scan(&rsp.CurrentAssets.GamePassCount).Error
|
|
||||||
|
|
||||||
// 4.6 持有游戏资格
|
|
||||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(available), 0) FROM user_game_tickets WHERE user_id = ?", userID).Scan(&rsp.CurrentAssets.GameTicketCount).Error
|
|
||||||
|
|
||||||
// 4.5 总资产估值
|
// 4.5 总资产估值
|
||||||
// 估值逻辑:积分余额 + 商品价值 + 优惠券价值 + 道具卡价值 + 次数卡(2元/次)
|
|
||||||
// 游戏资格不计入估值(购买其他商品赠送,无实际价值)
|
|
||||||
gamePassValue := rsp.CurrentAssets.GamePassCount * 200 // 估值:2元/次
|
|
||||||
gameTicketValue := int64(0) // 游戏资格不计入估值
|
|
||||||
|
|
||||||
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
rsp.CurrentAssets.TotalAssetValue = rsp.CurrentAssets.PointsBalance +
|
||||||
rsp.CurrentAssets.InventoryValue +
|
rsp.CurrentAssets.InventoryValue +
|
||||||
rsp.CurrentAssets.CouponValue +
|
rsp.CurrentAssets.CouponValue +
|
||||||
rsp.CurrentAssets.ItemCardValue +
|
rsp.CurrentAssets.ItemCardValue
|
||||||
gamePassValue +
|
|
||||||
gameTicketValue
|
|
||||||
|
|
||||||
// 4.6 累计盈亏比
|
// 4.6 累计盈亏比
|
||||||
if rsp.LifetimeStats.NetCashCost > 0 {
|
if rsp.LifetimeStats.NetCashCost > 0 {
|
||||||
|
|||||||
@ -17,8 +17,8 @@ type userProfitLossRequest struct {
|
|||||||
|
|
||||||
type userProfitLossPoint struct {
|
type userProfitLossPoint struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
Cost int64 `json:"cost"` // 累计投入(已支付-已退款)
|
Cost int64 `json:"cost"` // 净支出(仅已支付未退款订单)
|
||||||
Value int64 `json:"value"` // 累计产出(当前资产快照)
|
Value int64 `json:"value"` // 当前资产快照(实时)
|
||||||
Profit int64 `json:"profit"` // 净盈亏
|
Profit int64 `json:"profit"` // 净盈亏
|
||||||
Ratio float64 `json:"ratio"` // 盈亏比
|
Ratio float64 `json:"ratio"` // 盈亏比
|
||||||
Breakdown struct {
|
Breakdown struct {
|
||||||
@ -32,12 +32,6 @@ type userProfitLossPoint struct {
|
|||||||
type userProfitLossResponse struct {
|
type userProfitLossResponse struct {
|
||||||
Granularity string `json:"granularity"`
|
Granularity string `json:"granularity"`
|
||||||
List []userProfitLossPoint `json:"list"`
|
List []userProfitLossPoint `json:"list"`
|
||||||
Summary struct {
|
|
||||||
TotalCost int64 `json:"total_cost"`
|
|
||||||
TotalValue int64 `json:"total_value"`
|
|
||||||
TotalProfit int64 `json:"total_profit"`
|
|
||||||
AvgRatio float64 `json:"avg_ratio"`
|
|
||||||
} `json:"summary"`
|
|
||||||
CurrentAssets struct {
|
CurrentAssets struct {
|
||||||
Points int64 `json:"points"`
|
Points int64 `json:"points"`
|
||||||
Products int64 `json:"products"`
|
Products int64 `json:"products"`
|
||||||
@ -91,55 +85,14 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(balance_amount), 0) FROM user_coupons WHERE user_id = ? AND status = 1", userID).Scan(&curAssets.Coupons).Error
|
_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(balance_amount), 0) FROM user_coupons WHERE user_id = ? AND status = 1", userID).Scan(&curAssets.Coupons).Error
|
||||||
totalAssetValue := curAssets.Points + curAssets.Products + curAssets.Cards + curAssets.Coupons
|
totalAssetValue := curAssets.Points + curAssets.Products + curAssets.Cards + curAssets.Coupons
|
||||||
|
|
||||||
// --- 2. 获取订单数据(仅 status=2 已支付) ---
|
// --- 2. 获取订单数据(仅 status=2 已支付未退款)---
|
||||||
// 注意:为了计算累计趋势,我们需要获取 start 之前的所有已支付订单总额作为基数
|
|
||||||
var baseCost int64 = 0
|
|
||||||
var baseCostPtr *int64
|
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Lt(start)).
|
|
||||||
Scan(&baseCostPtr)
|
|
||||||
if baseCostPtr != nil {
|
|
||||||
baseCost = *baseCostPtr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扣除历史退款 (如果有的话,此处简化处理,主要关注当前范围内的波动)
|
|
||||||
var baseRefund int64 = 0
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
|
||||||
FROM payment_refunds pr
|
|
||||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
|
||||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS' AND pr.created_at < ?
|
|
||||||
`, userID, start).Scan(&baseRefund).Error
|
|
||||||
baseCost -= baseRefund
|
|
||||||
if baseCost < 0 {
|
|
||||||
baseCost = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
orderRows, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
Where(h.readDB.Orders.UserID.Eq(userID)).
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
Where(h.readDB.Orders.Status.Eq(2)). // 仅已支付,不含已退款
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
||||||
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
Where(h.readDB.Orders.CreatedAt.Lte(end)).
|
||||||
Find()
|
Find()
|
||||||
|
|
||||||
// 获取当前范围内的退款
|
|
||||||
type refundInfo struct {
|
|
||||||
Amount int64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
var refunds []refundInfo
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT pr.amount_refund as amount, pr.created_at
|
|
||||||
FROM payment_refunds pr
|
|
||||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
|
||||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS' AND pr.created_at BETWEEN ? AND ?
|
|
||||||
`, userID, start, end).Scan(&refunds).Error
|
|
||||||
|
|
||||||
// --- 3. 按时间分桶计算 ---
|
// --- 3. 按时间分桶计算 ---
|
||||||
list := make([]userProfitLossPoint, len(buckets))
|
list := make([]userProfitLossPoint, len(buckets))
|
||||||
|
|
||||||
@ -147,35 +100,24 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
return (t.After(b.Start) || t.Equal(b.Start)) && t.Before(b.End)
|
return (t.After(b.Start) || t.Equal(b.Start)) && t.Before(b.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
cumulativeCost := baseCost
|
var cumulativeCost int64 = 0
|
||||||
|
|
||||||
for i, b := range buckets {
|
for i, b := range buckets {
|
||||||
p := &list[i]
|
p := &list[i]
|
||||||
p.Date = b.Label
|
p.Date = b.Label
|
||||||
|
|
||||||
// 计算该时间段内的净投入变化
|
// 计算该时间段内的支出
|
||||||
var periodDelta int64 = 0
|
var periodCost int64 = 0
|
||||||
for _, o := range orderRows {
|
for _, o := range orderRows {
|
||||||
if inBucket(o.CreatedAt, b) {
|
if inBucket(o.CreatedAt, b) {
|
||||||
periodDelta += o.ActualAmount
|
periodCost += o.ActualAmount
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, r := range refunds {
|
|
||||||
if inBucket(r.CreatedAt, b) {
|
|
||||||
periodDelta -= r.Amount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cumulativeCost += periodCost
|
||||||
|
p.Cost = periodCost
|
||||||
|
|
||||||
cumulativeCost += periodDelta
|
// 使用当前资产快照作为产出值(最后一个桶显示完整值,其他桶按比例或显示0)
|
||||||
if cumulativeCost < 0 {
|
// 简化:所有桶都显示当前快照值,让用户一眼看到当前状态
|
||||||
cumulativeCost = 0
|
|
||||||
}
|
|
||||||
p.Cost = cumulativeCost
|
|
||||||
|
|
||||||
// 产出值:当前资产是一个存量值。
|
|
||||||
// 理想逻辑是回溯各时间点的余额,简化逻辑下:
|
|
||||||
// 如果该点还没有在该范围内发生过任何投入(且没有基数),则显示0;否则显示当前快照值。
|
|
||||||
// 这里我们统一显示当前快照,但在前端图表上它会是一条水平线或阶梯线。
|
|
||||||
p.Value = totalAssetValue
|
p.Value = totalAssetValue
|
||||||
p.Breakdown.Points = curAssets.Points
|
p.Breakdown.Points = curAssets.Points
|
||||||
p.Breakdown.Products = curAssets.Products
|
p.Breakdown.Products = curAssets.Products
|
||||||
@ -190,342 +132,42 @@ func (h *handler) GetUserProfitLossTrend() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 汇总数据
|
// 计算累计值用于汇总显示
|
||||||
var totalCost int64 = 0
|
var totalCost int64 = 0
|
||||||
var totalCostPtr *int64
|
for _, o := range orderRows {
|
||||||
h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
totalCost += o.ActualAmount
|
||||||
Select(h.readDB.Orders.ActualAmount.Sum()).
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.SourceType.In(2, 3, 4)). // 排除积分兑换(1)和奖品兑换(5)
|
|
||||||
Scan(&totalCostPtr)
|
|
||||||
if totalCostPtr != nil {
|
|
||||||
totalCost = *totalCostPtr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalRefund int64 = 0
|
// 最后一个桶使用累计成本
|
||||||
_ = h.repo.GetDbR().Raw(`
|
if len(list) > 0 {
|
||||||
SELECT COALESCE(SUM(pr.amount_refund), 0)
|
lastIdx := len(list) - 1
|
||||||
FROM payment_refunds pr
|
// 汇总数据:使用累计成本和当前资产值
|
||||||
JOIN orders o ON o.order_no = pr.order_no COLLATE utf8mb4_unicode_ci
|
list[lastIdx].Cost = totalCost
|
||||||
WHERE o.user_id = ? AND pr.status = 'SUCCESS'
|
list[lastIdx].Value = totalAssetValue
|
||||||
`, userID).Scan(&totalRefund).Error
|
list[lastIdx].Profit = totalAssetValue - totalCost
|
||||||
|
if totalCost > 0 {
|
||||||
finalNetCost := totalCost - totalRefund
|
list[lastIdx].Ratio = float64(totalAssetValue) / float64(totalCost)
|
||||||
if finalNetCost < 0 {
|
} else if totalAssetValue > 0 {
|
||||||
finalNetCost = 0
|
list[lastIdx].Ratio = 99.9
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := userProfitLossResponse{
|
ctx.Payload(userProfitLossResponse{
|
||||||
Granularity: gran,
|
Granularity: gran,
|
||||||
List: list,
|
List: list,
|
||||||
}
|
CurrentAssets: struct {
|
||||||
resp.Summary.TotalCost = finalNetCost
|
Points int64 `json:"points"`
|
||||||
resp.Summary.TotalValue = totalAssetValue
|
Products int64 `json:"products"`
|
||||||
resp.Summary.TotalProfit = totalAssetValue - finalNetCost
|
Cards int64 `json:"cards"`
|
||||||
if finalNetCost > 0 {
|
Coupons int64 `json:"coupons"`
|
||||||
resp.Summary.AvgRatio = float64(totalAssetValue) / float64(finalNetCost)
|
|
||||||
} else if totalAssetValue > 0 {
|
|
||||||
resp.Summary.AvgRatio = 99.9
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.CurrentAssets.Points = curAssets.Points
|
|
||||||
resp.CurrentAssets.Products = curAssets.Products
|
|
||||||
resp.CurrentAssets.Cards = curAssets.Cards
|
|
||||||
resp.CurrentAssets.Coupons = curAssets.Coupons
|
|
||||||
resp.CurrentAssets.Total = totalAssetValue
|
|
||||||
|
|
||||||
ctx.Payload(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盈亏明细请求
|
|
||||||
type profitLossDetailsRequest struct {
|
|
||||||
Page int `form:"page"`
|
|
||||||
PageSize int `form:"page_size"`
|
|
||||||
RangeType string `form:"rangeType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盈亏明细项
|
|
||||||
type profitLossDetailItem struct {
|
|
||||||
OrderID int64 `json:"order_id"`
|
|
||||||
OrderNo string `json:"order_no"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
SourceType int32 `json:"source_type"` // 来源类型 1商城 2抽奖 3系统
|
|
||||||
ActivityName string `json:"activity_name"` // 活动名称
|
|
||||||
ActualAmount int64 `json:"actual_amount"` // 实际支付金额(分)
|
|
||||||
RefundAmount int64 `json:"refund_amount"` // 退款金额(分)
|
|
||||||
NetCost int64 `json:"net_cost"` // 净投入(分)
|
|
||||||
PrizeValue int64 `json:"prize_value"` // 获得奖品价值(分)
|
|
||||||
PrizeName string `json:"prize_name"` // 奖品名称
|
|
||||||
PointsEarned int64 `json:"points_earned"` // 获得积分
|
|
||||||
PointsValue int64 `json:"points_value"` // 积分价值(分)
|
|
||||||
CouponUsedValue int64 `json:"coupon_used_value"` // 使用优惠券价值(分)
|
|
||||||
CouponUsedName string `json:"coupon_used_name"` // 使用的优惠券名称
|
|
||||||
ItemCardUsed string `json:"item_card_used"` // 使用的道具卡名称
|
|
||||||
ItemCardValue int64 `json:"item_card_value"` // 道具卡价值(分)
|
|
||||||
NetProfit int64 `json:"net_profit"` // 净盈亏
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盈亏明细响应
|
|
||||||
type profitLossDetailsResponse struct {
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
List []profitLossDetailItem `json:"list"`
|
}{
|
||||||
Summary struct {
|
Points: curAssets.Points,
|
||||||
TotalCost int64 `json:"total_cost"`
|
Products: curAssets.Products,
|
||||||
TotalValue int64 `json:"total_value"`
|
Cards: curAssets.Cards,
|
||||||
TotalProfit int64 `json:"total_profit"`
|
Coupons: curAssets.Coupons,
|
||||||
} `json:"summary"`
|
Total: totalAssetValue,
|
||||||
}
|
},
|
||||||
|
})
|
||||||
// GetUserProfitLossDetails 获取用户盈亏明细
|
|
||||||
// @Summary 获取用户盈亏明细
|
|
||||||
// @Description 获取用户每笔订单的详细盈亏信息
|
|
||||||
// @Tags 管理端.用户
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param user_id path integer true "用户ID"
|
|
||||||
// @Param page query int false "页码" default(1)
|
|
||||||
// @Param page_size query int false "每页数量" default(20)
|
|
||||||
// @Param rangeType query string false "时间范围" default("all")
|
|
||||||
// @Success 200 {object} profitLossDetailsResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/admin/users/{user_id}/stats/profit_loss_details [get]
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
func (h *handler) GetUserProfitLossDetails() 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(profitLossDetailsRequest)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析时间范围
|
|
||||||
start, end := parseRange(req.RangeType, "", "")
|
|
||||||
if req.RangeType == "all" {
|
|
||||||
u, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First()
|
|
||||||
if u != nil {
|
|
||||||
start = u.CreatedAt
|
|
||||||
} else {
|
|
||||||
start = time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单总数
|
|
||||||
orderQ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().
|
|
||||||
Where(h.readDB.Orders.UserID.Eq(userID)).
|
|
||||||
Where(h.readDB.Orders.Status.Eq(2)).
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Gte(start)).
|
|
||||||
Where(h.readDB.Orders.CreatedAt.Lte(end))
|
|
||||||
|
|
||||||
total, err := orderQ.Count()
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页查询订单
|
|
||||||
orders, err := orderQ.Order(h.readDB.Orders.CreatedAt.Desc()).
|
|
||||||
Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find()
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20108, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集订单ID
|
|
||||||
orderIDs := make([]int64, len(orders))
|
|
||||||
orderNos := make([]string, len(orders))
|
|
||||||
for i, o := range orders {
|
|
||||||
orderIDs[i] = o.ID
|
|
||||||
orderNos[i] = o.OrderNo
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询退款信息
|
|
||||||
refundMap := make(map[string]int64)
|
|
||||||
if len(orderNos) > 0 {
|
|
||||||
type refundRow struct {
|
|
||||||
OrderNo string
|
|
||||||
Amount int64
|
|
||||||
}
|
|
||||||
var refunds []refundRow
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT order_no, COALESCE(SUM(amount_refund), 0) as amount
|
|
||||||
FROM payment_refunds
|
|
||||||
WHERE order_no IN ? AND status = 'SUCCESS'
|
|
||||||
GROUP BY order_no
|
|
||||||
`, orderNos).Scan(&refunds).Error
|
|
||||||
for _, r := range refunds {
|
|
||||||
refundMap[r.OrderNo] = r.Amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询库存价值(获得的奖品)
|
|
||||||
prizeValueMap := make(map[int64]int64)
|
|
||||||
prizeNameMap := make(map[int64]string)
|
|
||||||
if len(orderIDs) > 0 {
|
|
||||||
type prizeRow struct {
|
|
||||||
OrderID int64
|
|
||||||
Value int64
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
var prizes []prizeRow
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT ui.order_id, COALESCE(SUM(p.price), 0) as value,
|
|
||||||
GROUP_CONCAT(p.name SEPARATOR ', ') as name
|
|
||||||
FROM user_inventory ui
|
|
||||||
LEFT JOIN products p ON p.id = ui.product_id
|
|
||||||
WHERE ui.order_id IN ?
|
|
||||||
GROUP BY ui.order_id
|
|
||||||
`, orderIDs).Scan(&prizes).Error
|
|
||||||
for _, p := range prizes {
|
|
||||||
prizeValueMap[p.OrderID] = p.Value
|
|
||||||
prizeNameMap[p.OrderID] = p.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询使用的优惠券
|
|
||||||
couponValueMap := make(map[int64]int64)
|
|
||||||
couponNameMap := make(map[int64]string)
|
|
||||||
if len(orderIDs) > 0 {
|
|
||||||
type couponRow struct {
|
|
||||||
OrderID int64
|
|
||||||
Value int64
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
var coupons []couponRow
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT ucu.order_id, COALESCE(SUM(ABS(ucu.change_amount)), 0) as value,
|
|
||||||
GROUP_CONCAT(DISTINCT sc.name SEPARATOR ', ') as name
|
|
||||||
FROM user_coupon_usage ucu
|
|
||||||
LEFT JOIN user_coupons uc ON uc.id = ucu.user_coupon_id
|
|
||||||
LEFT JOIN system_coupons sc ON sc.id = uc.coupon_id
|
|
||||||
WHERE ucu.order_id IN ?
|
|
||||||
GROUP BY ucu.order_id
|
|
||||||
`, orderIDs).Scan(&coupons).Error
|
|
||||||
for _, c := range coupons {
|
|
||||||
couponValueMap[c.OrderID] = c.Value
|
|
||||||
couponNameMap[c.OrderID] = c.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量查询活动信息
|
|
||||||
activityNameMap := make(map[int64]string)
|
|
||||||
if len(orderIDs) > 0 {
|
|
||||||
type actRow struct {
|
|
||||||
OrderID int64
|
|
||||||
ActivityName string
|
|
||||||
}
|
|
||||||
var acts []actRow
|
|
||||||
_ = h.repo.GetDbR().Raw(`
|
|
||||||
SELECT o.id as order_id, a.name as activity_name
|
|
||||||
FROM orders o
|
|
||||||
LEFT JOIN activities a ON a.id = o.activity_id
|
|
||||||
WHERE o.id IN ? AND o.activity_id > 0
|
|
||||||
`, orderIDs).Scan(&acts).Error
|
|
||||||
for _, a := range acts {
|
|
||||||
activityNameMap[a.OrderID] = a.ActivityName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组装明细数据
|
|
||||||
list := make([]profitLossDetailItem, len(orders))
|
|
||||||
var totalCost, totalValue int64
|
|
||||||
|
|
||||||
for i, o := range orders {
|
|
||||||
refund := refundMap[o.OrderNo]
|
|
||||||
prizeValue := prizeValueMap[o.ID]
|
|
||||||
couponValue := couponValueMap[o.ID]
|
|
||||||
netCost := o.ActualAmount - refund
|
|
||||||
netProfit := prizeValue - netCost
|
|
||||||
|
|
||||||
list[i] = profitLossDetailItem{
|
|
||||||
OrderID: o.ID,
|
|
||||||
OrderNo: o.OrderNo,
|
|
||||||
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
SourceType: o.SourceType,
|
|
||||||
ActivityName: activityNameMap[o.ID],
|
|
||||||
ActualAmount: o.ActualAmount,
|
|
||||||
RefundAmount: refund,
|
|
||||||
NetCost: netCost,
|
|
||||||
PrizeValue: prizeValue,
|
|
||||||
PrizeName: prizeNameMap[o.ID],
|
|
||||||
PointsEarned: 0, // 简化处理
|
|
||||||
PointsValue: 0,
|
|
||||||
CouponUsedValue: couponValue,
|
|
||||||
CouponUsedName: couponNameMap[o.ID],
|
|
||||||
ItemCardUsed: "", // 从订单备注中解析
|
|
||||||
ItemCardValue: 0,
|
|
||||||
NetProfit: netProfit,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析道具卡信息(从订单备注)
|
|
||||||
if o.Remark != "" {
|
|
||||||
list[i].ItemCardUsed = parseItemCardFromRemark(o.Remark)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalCost += netCost
|
|
||||||
totalValue += prizeValue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := profitLossDetailsResponse{
|
|
||||||
Page: req.Page,
|
|
||||||
PageSize: req.PageSize,
|
|
||||||
Total: total,
|
|
||||||
List: list,
|
|
||||||
}
|
|
||||||
resp.Summary.TotalCost = totalCost
|
|
||||||
resp.Summary.TotalValue = totalValue
|
|
||||||
resp.Summary.TotalProfit = totalValue - totalCost
|
|
||||||
|
|
||||||
ctx.Payload(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从订单备注中解析道具卡信息
|
|
||||||
func parseItemCardFromRemark(remark string) string {
|
|
||||||
// 格式: itemcard:xxx|...
|
|
||||||
if len(remark) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
idx := 0
|
|
||||||
for i := 0; i < len(remark); i++ {
|
|
||||||
if remark[i:] == "itemcard:" || (i+9 <= len(remark) && remark[i:i+9] == "itemcard:") {
|
|
||||||
idx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if idx == 0 && len(remark) < 9 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if idx+9 >= len(remark) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
seg := remark[idx+9:]
|
|
||||||
// 找到 | 分隔符
|
|
||||||
end := len(seg)
|
|
||||||
for i := 0; i < len(seg); i++ {
|
|
||||||
if seg[i] == '|' {
|
|
||||||
end = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return seg[:end]
|
|
||||||
}
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ type listAppProductsItem struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MainImage string `json:"main_image"`
|
MainImage string `json:"main_image"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
PointsRequired float64 `json:"points_required"` // 积分(分/rate)`
|
PointsRequired int64 `json:"points_required"`
|
||||||
Sales int64 `json:"sales"`
|
Sales int64 `json:"sales"`
|
||||||
InStock bool `json:"in_stock"`
|
InStock bool `json:"in_stock"`
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func (h *productHandler) ListProductsForApp() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
rsp := &listAppProductsResponse{Total: total, CurrentPage: req.Page, PageSize: req.PageSize, List: make([]listAppProductsItem, len(items))}
|
rsp := &listAppProductsResponse{Total: total, CurrentPage: req.Page, PageSize: req.PageSize, List: make([]listAppProductsItem, len(items))}
|
||||||
for i, it := range items {
|
for i, it := range items {
|
||||||
pts := h.user.CentsToPointsFloat(ctx.RequestContext(), it.Price)
|
pts, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price)
|
||||||
rsp.List[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: pts, Sales: it.Sales, InStock: it.InStock}
|
rsp.List[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: pts, Sales: it.Sales, InStock: it.InStock}
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
@ -99,7 +99,7 @@ type getAppProductDetailResponse struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Album []string `json:"album"`
|
Album []string `json:"album"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
PointsRequired float64 `json:"points_required"` // 积分(分/rate)`
|
PointsRequired int64 `json:"points_required"`
|
||||||
Sales int64 `json:"sales"`
|
Sales int64 `json:"sales"`
|
||||||
Stock int64 `json:"stock"`
|
Stock int64 `json:"stock"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@ -135,10 +135,10 @@ func (h *productHandler) GetProductDetailForApp() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ptsDetail := h.user.CentsToPointsFloat(ctx.RequestContext(), d.Price)
|
ptsDetail, _ := h.user.CentsToPoints(ctx.RequestContext(), d.Price)
|
||||||
rsp := &getAppProductDetailResponse{ID: d.ID, Name: d.Name, Album: d.Album, Price: d.Price, PointsRequired: ptsDetail, Sales: d.Sales, Stock: d.Stock, Description: d.Description, Service: d.Service, Recommendations: make([]listAppProductsItem, len(d.Recommendations))}
|
rsp := &getAppProductDetailResponse{ID: d.ID, Name: d.Name, Album: d.Album, Price: d.Price, PointsRequired: ptsDetail, Sales: d.Sales, Stock: d.Stock, Description: d.Description, Service: d.Service, Recommendations: make([]listAppProductsItem, len(d.Recommendations))}
|
||||||
for i, it := range d.Recommendations {
|
for i, it := range d.Recommendations {
|
||||||
ptsRec := h.user.CentsToPointsFloat(ctx.RequestContext(), it.Price)
|
ptsRec, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price)
|
||||||
rsp.Recommendations[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: ptsRec, Sales: it.Sales, InStock: it.InStock}
|
rsp.Recommendations[i] = listAppProductsItem{ID: it.ID, Name: it.Name, MainImage: it.MainImage, Price: it.Price, PointsRequired: ptsRec, Sales: it.Sales, InStock: it.InStock}
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
|
|||||||
@ -25,9 +25,6 @@ type listStoreItemsRequest struct {
|
|||||||
Kind string `form:"kind"`
|
Kind string `form:"kind"`
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
PageSize int `form:"page_size"`
|
PageSize int `form:"page_size"`
|
||||||
Keyword string `form:"keyword"` // 关键词搜索
|
|
||||||
PriceMin *int64 `form:"price_min"` // 最低积分价格(积分单位)
|
|
||||||
PriceMax *int64 `form:"price_max"` // 最高积分价格(积分单位)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type listStoreItem struct {
|
type listStoreItem struct {
|
||||||
@ -36,7 +33,7 @@ type listStoreItem struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MainImage string `json:"main_image"`
|
MainImage string `json:"main_image"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
PointsRequired float64 `json:"points_required"` // 积分(分/rate)`
|
PointsRequired int64 `json:"points_required"`
|
||||||
InStock bool `json:"in_stock"`
|
InStock bool `json:"in_stock"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
DiscountType int32 `json:"discount_type"`
|
DiscountType int32 `json:"discount_type"`
|
||||||
@ -86,76 +83,32 @@ func (h *storeHandler) ListStoreItemsForApp() core.HandlerFunc {
|
|||||||
offset := (req.Page - 1) * req.PageSize
|
offset := (req.Page - 1) * req.PageSize
|
||||||
limit := req.PageSize
|
limit := req.PageSize
|
||||||
|
|
||||||
// 将积分价格转换为分进行查询
|
|
||||||
var priceMinCents, priceMaxCents int64
|
|
||||||
if req.PriceMin != nil && *req.PriceMin > 0 {
|
|
||||||
centsVal, _ := h.user.PointsToCents(ctx.RequestContext(), *req.PriceMin)
|
|
||||||
priceMinCents = centsVal
|
|
||||||
}
|
|
||||||
if req.PriceMax != nil && *req.PriceMax > 0 {
|
|
||||||
centsVal, _ := h.user.PointsToCents(ctx.RequestContext(), *req.PriceMax)
|
|
||||||
priceMaxCents = centsVal
|
|
||||||
}
|
|
||||||
|
|
||||||
switch req.Kind {
|
switch req.Kind {
|
||||||
case "item_card":
|
case "item_card":
|
||||||
q := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.SystemItemCards.Status.Eq(1))
|
q := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.SystemItemCards.Status.Eq(1))
|
||||||
// 关键词筛选
|
|
||||||
if req.Keyword != "" {
|
|
||||||
q = q.Where(h.readDB.SystemItemCards.Name.Like("%" + req.Keyword + "%"))
|
|
||||||
}
|
|
||||||
// 价格区间筛选
|
|
||||||
if priceMinCents > 0 {
|
|
||||||
q = q.Where(h.readDB.SystemItemCards.Price.Gte(priceMinCents))
|
|
||||||
}
|
|
||||||
if priceMaxCents > 0 {
|
|
||||||
q = q.Where(h.readDB.SystemItemCards.Price.Lte(priceMaxCents))
|
|
||||||
}
|
|
||||||
total, _ = q.Count()
|
total, _ = q.Count()
|
||||||
rows, _ := q.Order(h.readDB.SystemItemCards.ID.Desc()).Offset(offset).Limit(limit).Find()
|
rows, _ := q.Order(h.readDB.SystemItemCards.ID.Desc()).Offset(offset).Limit(limit).Find()
|
||||||
list = make([]listStoreItem, len(rows))
|
list = make([]listStoreItem, len(rows))
|
||||||
for i, it := range rows {
|
for i, it := range rows {
|
||||||
pts := h.user.CentsToPointsFloat(ctx.RequestContext(), it.Price)
|
pts, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price)
|
||||||
list[i] = listStoreItem{ID: it.ID, Kind: "item_card", Name: it.Name, Price: it.Price, PointsRequired: pts, Status: it.Status, Supported: true}
|
list[i] = listStoreItem{ID: it.ID, Kind: "item_card", Name: it.Name, Price: it.Price, PointsRequired: pts, Status: it.Status, Supported: true}
|
||||||
}
|
}
|
||||||
case "coupon":
|
case "coupon":
|
||||||
q := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.SystemCoupons.Status.Eq(1))
|
q := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.SystemCoupons.Status.Eq(1))
|
||||||
// 关键词筛选
|
|
||||||
if req.Keyword != "" {
|
|
||||||
q = q.Where(h.readDB.SystemCoupons.Name.Like("%" + req.Keyword + "%"))
|
|
||||||
}
|
|
||||||
// 价格区间筛选 (优惠券用 DiscountValue)
|
|
||||||
if priceMinCents > 0 {
|
|
||||||
q = q.Where(h.readDB.SystemCoupons.DiscountValue.Gte(priceMinCents))
|
|
||||||
}
|
|
||||||
if priceMaxCents > 0 {
|
|
||||||
q = q.Where(h.readDB.SystemCoupons.DiscountValue.Lte(priceMaxCents))
|
|
||||||
}
|
|
||||||
total, _ = q.Count()
|
total, _ = q.Count()
|
||||||
rows, _ := q.Order(h.readDB.SystemCoupons.ID.Desc()).Offset(offset).Limit(limit).Find()
|
rows, _ := q.Order(h.readDB.SystemCoupons.ID.Desc()).Offset(offset).Limit(limit).Find()
|
||||||
list = make([]listStoreItem, len(rows))
|
list = make([]listStoreItem, len(rows))
|
||||||
for i, it := range rows {
|
for i, it := range rows {
|
||||||
pts := h.user.CentsToPointsFloat(ctx.RequestContext(), it.DiscountValue)
|
pts, _ := h.user.CentsToPoints(ctx.RequestContext(), it.DiscountValue)
|
||||||
list[i] = listStoreItem{ID: it.ID, Kind: "coupon", Name: it.Name, DiscountType: it.DiscountType, DiscountValue: it.DiscountValue, PointsRequired: pts, MinSpend: it.MinSpend, Status: it.Status, Supported: it.DiscountType == 1}
|
list[i] = listStoreItem{ID: it.ID, Kind: "coupon", Name: it.Name, DiscountType: it.DiscountType, DiscountValue: it.DiscountValue, PointsRequired: pts, MinSpend: it.MinSpend, Status: it.Status, Supported: it.DiscountType == 1}
|
||||||
}
|
}
|
||||||
default: // product
|
default: // product
|
||||||
q := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.Status.Eq(1))
|
q := h.readDB.Products.WithContext(ctx.RequestContext()).ReadDB().Where(h.readDB.Products.Status.Eq(1))
|
||||||
// 关键词筛选
|
|
||||||
if req.Keyword != "" {
|
|
||||||
q = q.Where(h.readDB.Products.Name.Like("%" + req.Keyword + "%"))
|
|
||||||
}
|
|
||||||
// 价格区间筛选
|
|
||||||
if priceMinCents > 0 {
|
|
||||||
q = q.Where(h.readDB.Products.Price.Gte(priceMinCents))
|
|
||||||
}
|
|
||||||
if priceMaxCents > 0 {
|
|
||||||
q = q.Where(h.readDB.Products.Price.Lte(priceMaxCents))
|
|
||||||
}
|
|
||||||
total, _ = q.Count()
|
total, _ = q.Count()
|
||||||
rows, _ := q.Order(h.readDB.Products.ID.Desc()).Offset(offset).Limit(limit).Find()
|
rows, _ := q.Order(h.readDB.Products.ID.Desc()).Offset(offset).Limit(limit).Find()
|
||||||
list = make([]listStoreItem, len(rows))
|
list = make([]listStoreItem, len(rows))
|
||||||
for i, it := range rows {
|
for i, it := range rows {
|
||||||
pts := h.user.CentsToPointsFloat(ctx.RequestContext(), it.Price)
|
pts, _ := h.user.CentsToPoints(ctx.RequestContext(), it.Price)
|
||||||
list[i] = listStoreItem{ID: it.ID, Kind: "product", Name: it.Name, MainImage: parseFirstImage(it.ImagesJSON), Price: it.Price, PointsRequired: pts, InStock: it.Stock > 0 && it.Status == 1, Status: it.Status, Supported: true}
|
list[i] = listStoreItem{ID: it.ID, Kind: "product", Name: it.Name, MainImage: parseFirstImage(it.ImagesJSON), Price: it.Price, PointsRequired: pts, InStock: it.Stock > 0 && it.Status == 1, Status: it.Status, Supported: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConfigResponse struct {
|
|
||||||
SubscribeTemplates map[string]string `json:"subscribe_templates"`
|
|
||||||
ContactServiceQRCode string `json:"contact_service_qrcode"` // 客服二维码
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPublicConfig 获取公开配置(包含订阅模板ID)
|
|
||||||
// @Summary 获取公开配置
|
|
||||||
// @Description 获取小程序前端需要用到的公开配置,如订阅消息模板ID
|
|
||||||
// @Tags 公共
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} ConfigResponse
|
|
||||||
// @Router /api/app/config/public [get]
|
|
||||||
func (h *handler) GetPublicConfig() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
// 查询配置
|
|
||||||
var subscribeTemplateID string
|
|
||||||
var serviceQRCode string
|
|
||||||
|
|
||||||
configs, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.readDB.SystemConfigs.ConfigKey.In("wechat.lottery_result_template_id", "contact.service_qrcode")).
|
|
||||||
Find()
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
for _, cfg := range configs {
|
|
||||||
switch cfg.ConfigKey {
|
|
||||||
case "wechat.lottery_result_template_id":
|
|
||||||
subscribeTemplateID = cfg.ConfigValue
|
|
||||||
case "contact.service_qrcode":
|
|
||||||
serviceQRCode = cfg.ConfigValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rsp := ConfigResponse{
|
|
||||||
SubscribeTemplates: map[string]string{
|
|
||||||
"lottery_result": subscribeTemplateID,
|
|
||||||
},
|
|
||||||
ContactServiceQRCode: serviceQRCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(rsp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,9 +6,6 @@ import (
|
|||||||
"bindbox-game/configs"
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type openidRequest struct {
|
type openidRequest struct {
|
||||||
@ -29,19 +26,8 @@ func (h *handler) GetOpenID() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用动态配置
|
|
||||||
wxcfg := &wechat.WechatConfig{}
|
|
||||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
|
||||||
c := dc.GetWechat(ctx.RequestContext().Context)
|
|
||||||
wxcfg.AppID = c.AppID
|
|
||||||
wxcfg.AppSecret = c.AppSecret
|
|
||||||
} else {
|
|
||||||
cfg := configs.Get()
|
cfg := configs.Get()
|
||||||
wxcfg.AppID = cfg.Wechat.AppID
|
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret}
|
||||||
wxcfg.AppSecret = cfg.Wechat.AppSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
h.logger.Info("GetOpenID Config", zap.String("AppID", wxcfg.AppID), zap.String("AppSecret", wxcfg.AppSecret))
|
|
||||||
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
|
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -192,20 +191,16 @@ func (h *handler) EnterGame() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询剩余次数
|
// 查询剩余次数
|
||||||
remaining := 0
|
|
||||||
if req.GameCode == "minesweeper_free" {
|
|
||||||
remaining = 999999 // Represent infinite for free mode
|
|
||||||
} else {
|
|
||||||
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
|
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
|
||||||
|
remaining := 0
|
||||||
if ticket != nil {
|
if ticket != nil {
|
||||||
remaining = int(ticket.Available)
|
remaining = int(ticket.Available)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 从系统配置读取Nakama服务器信息
|
// 从系统配置读取Nakama服务器信息
|
||||||
nakamaServer := "ws://127.0.0.1:7350"
|
nakamaServer := "wss://nakama.yourdomain.com"
|
||||||
nakamaKey := "defaultkey"
|
nakamaKey := "defaultkey"
|
||||||
clientUrl := "http://127.0.0.1:9991" // 指向当前后端地址作为默认
|
clientUrl := "https://game.1024tool.vip"
|
||||||
configKey := "game_" + req.GameCode + "_config"
|
configKey := "game_" + req.GameCode + "_config"
|
||||||
// map generic game code to specific config key if needed, or just use convention
|
// map generic game code to specific config key if needed, or just use convention
|
||||||
if req.GameCode == "minesweeper" {
|
if req.GameCode == "minesweeper" {
|
||||||
@ -317,21 +312,8 @@ func (h *handler) VerifyTicket() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从Redis验证token
|
// 从Redis验证token
|
||||||
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||||
if err != nil {
|
if err != nil || storedUserID != req.UserID {
|
||||||
ctx.Payload(&verifyResponse{Valid: false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse "userID:gameType"
|
|
||||||
parts := strings.Split(storedValue, ":")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
ctx.Payload(&verifyResponse{Valid: false})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storedUserID := parts[0]
|
|
||||||
|
|
||||||
if storedUserID != req.UserID {
|
|
||||||
ctx.Payload(&verifyResponse{Valid: false})
|
ctx.Payload(&verifyResponse{Valid: false})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -354,7 +336,6 @@ type settleRequest struct {
|
|||||||
MatchID string `json:"match_id"`
|
MatchID string `json:"match_id"`
|
||||||
Win bool `json:"win"`
|
Win bool `json:"win"`
|
||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
GameType string `json:"game_type"` // 游戏类型,如 "minesweeper" 或 "minesweeper_free"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type settleResponse struct {
|
type settleResponse struct {
|
||||||
@ -376,39 +357,17 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接从请求参数判断是否为免费模式
|
// 验证token(可选,如果游戏服务器传了ticket则验证,否则信任internal调用)
|
||||||
isFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
|
|
||||||
// 拦截免费场结算(免费模式不发放任何奖励)
|
|
||||||
if isFreeMode {
|
|
||||||
h.logger.Info("Free mode game settled without rewards",
|
|
||||||
zap.String("user_id", req.UserID),
|
|
||||||
zap.String("match_id", req.MatchID),
|
|
||||||
zap.Bool("win", req.Win))
|
|
||||||
ctx.Payload(&settleResponse{Success: true, Reward: "体验模式无奖励"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 ticket(可选,用于防止重复结算)
|
|
||||||
if req.Ticket != "" {
|
if req.Ticket != "" {
|
||||||
storedValue, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:token:ticket:"+req.Ticket).Result()
|
||||||
if err != nil {
|
if err != nil || storedUserID != req.UserID {
|
||||||
h.logger.Warn("Ticket validation failed (not found)", zap.String("ticket", req.Ticket))
|
h.logger.Warn("Ticket validation failed, but proceeding with internal trust",
|
||||||
|
zap.String("ticket", req.Ticket), zap.String("user_id", req.UserID))
|
||||||
} else {
|
} else {
|
||||||
// Parse "userID:gameType"
|
// 删除token防止重复使用
|
||||||
parts := strings.Split(storedValue, ":")
|
|
||||||
storedUserID := parts[0]
|
|
||||||
|
|
||||||
if storedUserID != req.UserID {
|
|
||||||
h.logger.Warn("Ticket validation failed (user mismatch)",
|
|
||||||
zap.String("ticket", req.Ticket), zap.String("user_id", req.UserID), zap.String("stored", storedUserID))
|
|
||||||
} else {
|
|
||||||
// 删除 ticket 防止重复使用
|
|
||||||
h.redis.Del(ctx.RequestContext(), "game:token:ticket:"+req.Ticket)
|
h.redis.Del(ctx.RequestContext(), "game:token:ticket:"+req.Ticket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:即使ticket验证失败,作为internal API我们仍然信任游戏服务器传来的UserID
|
// 注意:即使ticket验证失败,作为internal API我们仍然信任游戏服务器传来的UserID
|
||||||
|
|
||||||
// 奖品发放逻辑
|
// 奖品发放逻辑
|
||||||
@ -447,8 +406,7 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 发放奖励(仅付费模式,免费模式已在前面拦截)
|
// 3. 发放奖励
|
||||||
|
|
||||||
if targetProductID > 0 {
|
if targetProductID > 0 {
|
||||||
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
|
||||||
ProductID: targetProductID,
|
ProductID: targetProductID,
|
||||||
@ -510,17 +468,12 @@ func (h *handler) ConsumeTicket() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 扣减游戏次数
|
// 扣减游戏次数
|
||||||
if gameCode == "minesweeper_free" {
|
|
||||||
// 免费场场不扣减次数,直接通过
|
|
||||||
h.logger.Info("Free mode consume ticket skipped deduction", zap.Int64("user_id", uid))
|
|
||||||
} else {
|
|
||||||
err := h.ticketSvc.UseTicket(ctx.RequestContext(), uid, gameCode)
|
err := h.ticketSvc.UseTicket(ctx.RequestContext(), uid, gameCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to consume ticket", zap.Int64("user_id", uid), zap.String("game_code", gameCode), zap.Error(err))
|
h.logger.Error("Failed to consume ticket", zap.Int64("user_id", uid), zap.String("game_code", gameCode), zap.Error(err))
|
||||||
ctx.Payload(&consumeTicketResponse{Success: false, Error: err.Error()})
|
ctx.Payload(&consumeTicketResponse{Success: false, Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 使 ticket 失效(防止重复扣减)
|
// 使 ticket 失效(防止重复扣减)
|
||||||
if req.Ticket != "" {
|
if req.Ticket != "" {
|
||||||
|
|||||||
@ -1,331 +0,0 @@
|
|||||||
package game_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alicebob/miniredis/v2"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// settleRequest 结算请求结构体(与 handler.go 保持一致)
|
|
||||||
type settleRequest struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Ticket string `json:"ticket"`
|
|
||||||
MatchID string `json:"match_id"`
|
|
||||||
Win bool `json:"win"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
GameType string `json:"game_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// settleResponse 结算响应结构体
|
|
||||||
type settleResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Reward string `json:"reward,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSettleGame_FreeModeDetection 测试免费模式判断逻辑
|
|
||||||
// 这是核心测试:验证免费模式通过 game_type 参数判断,而不是依赖 Redis
|
|
||||||
func TestSettleGame_FreeModeDetection(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
gameType string
|
|
||||||
ticketInRedis bool // 是否在 Redis 中存储 ticket
|
|
||||||
expectedReward string // 预期的奖励消息
|
|
||||||
shouldBlock bool // 是否应该被拦截(免费模式)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "免费模式_有ticket_应拦截",
|
|
||||||
gameType: "minesweeper_free",
|
|
||||||
ticketInRedis: true,
|
|
||||||
expectedReward: "体验模式无奖励",
|
|
||||||
shouldBlock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "免费模式_无ticket_应拦截",
|
|
||||||
gameType: "minesweeper_free",
|
|
||||||
ticketInRedis: false,
|
|
||||||
expectedReward: "体验模式无奖励",
|
|
||||||
shouldBlock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "付费模式_有ticket_应发奖",
|
|
||||||
gameType: "minesweeper",
|
|
||||||
ticketInRedis: true,
|
|
||||||
expectedReward: "", // 付费模式会发放积分奖励
|
|
||||||
shouldBlock: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "付费模式_无ticket_应发奖",
|
|
||||||
gameType: "minesweeper",
|
|
||||||
ticketInRedis: false,
|
|
||||||
expectedReward: "", // 付费模式会发放积分奖励
|
|
||||||
shouldBlock: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "空game_type_应发奖",
|
|
||||||
gameType: "",
|
|
||||||
ticketInRedis: false,
|
|
||||||
expectedReward: "", // 空类型不是免费模式
|
|
||||||
shouldBlock: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// 模拟判断逻辑
|
|
||||||
isFreeMode := tt.gameType == "minesweeper_free"
|
|
||||||
|
|
||||||
if tt.shouldBlock {
|
|
||||||
assert.True(t, isFreeMode, "免费模式应该被正确识别")
|
|
||||||
} else {
|
|
||||||
assert.False(t, isFreeMode, "非免费模式不应该被拦截")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSettleGame_FreeModeWithRedis 测试 Redis ticket 不影响免费模式判断
|
|
||||||
func TestSettleGame_FreeModeWithRedis(t *testing.T) {
|
|
||||||
// 1. 启动 miniredis
|
|
||||||
mr, err := miniredis.Run()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer mr.Close()
|
|
||||||
|
|
||||||
rdb := redis.NewClient(&redis.Options{
|
|
||||||
Addr: mr.Addr(),
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
userID := "12345"
|
|
||||||
ticket := "GT123456789"
|
|
||||||
|
|
||||||
// 场景1: Redis 中有 ticket,但 game_type 是免费模式
|
|
||||||
t.Run("Redis有ticket但game_type是免费模式", func(t *testing.T) {
|
|
||||||
// 存储 ticket 到 Redis
|
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
|
||||||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
|
||||||
|
|
||||||
req := settleRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Ticket: ticket,
|
|
||||||
MatchID: "match-001",
|
|
||||||
Win: true,
|
|
||||||
Score: 100,
|
|
||||||
GameType: "minesweeper_free",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接从 req.GameType 判断
|
|
||||||
isFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
assert.True(t, isFreeMode, "应该识别为免费模式")
|
|
||||||
|
|
||||||
// 清理
|
|
||||||
rdb.Del(ctx, ticketKey)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 场景2: Redis 中没有 ticket(已被删除),但 game_type 是免费模式
|
|
||||||
t.Run("Redis无ticket但game_type是免费模式", func(t *testing.T) {
|
|
||||||
req := settleRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Ticket: ticket,
|
|
||||||
MatchID: "match-002",
|
|
||||||
Win: true,
|
|
||||||
Score: 100,
|
|
||||||
GameType: "minesweeper_free",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认 Redis 中没有 ticket
|
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
|
||||||
_, err := rdb.Get(ctx, ticketKey).Result()
|
|
||||||
assert.Error(t, err, "ticket 应该不存在")
|
|
||||||
|
|
||||||
// 直接从 req.GameType 判断(修复后的逻辑)
|
|
||||||
isFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
assert.True(t, isFreeMode, "即使 Redis 中没有 ticket,也应该识别为免费模式")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 场景3: Redis 中有 ticket 且是免费模式,但 game_type 参数为空(防止绕过)
|
|
||||||
t.Run("Redis标记免费但game_type参数为空", func(t *testing.T) {
|
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
|
||||||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
|
||||||
|
|
||||||
req := settleRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Ticket: ticket,
|
|
||||||
MatchID: "match-003",
|
|
||||||
Win: true,
|
|
||||||
Score: 100,
|
|
||||||
GameType: "", // 恶意留空
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用修复后的逻辑:以请求参数为准
|
|
||||||
isFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
assert.False(t, isFreeMode, "game_type 为空时不应识别为免费模式")
|
|
||||||
|
|
||||||
// 注意:这里是一个潜在的安全风险,需要确保游戏服务器正确传递 game_type
|
|
||||||
// 建议:可以增加双重校验,从 Redis 读取作为备份
|
|
||||||
|
|
||||||
rdb.Del(ctx, ticketKey)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSettleGame_OldBugScenario 重现并验证旧 bug 已被修复
|
|
||||||
func TestSettleGame_OldBugScenario(t *testing.T) {
|
|
||||||
// 模拟旧代码的问题场景
|
|
||||||
t.Run("旧bug重现_ticket被删除后误判为付费模式", func(t *testing.T) {
|
|
||||||
mr, _ := miniredis.Run()
|
|
||||||
defer mr.Close()
|
|
||||||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
userID := "12345"
|
|
||||||
ticket := "GT123456789"
|
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
|
||||||
|
|
||||||
// 模拟场景:
|
|
||||||
// 1. 用户进入免费游戏,ticket 存入 Redis
|
|
||||||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
|
||||||
|
|
||||||
// 2. 匹配成功后,ticket 被删除
|
|
||||||
rdb.Del(ctx, ticketKey)
|
|
||||||
|
|
||||||
// 3. 游戏结算时尝试读取 ticket
|
|
||||||
_, err := rdb.Get(ctx, ticketKey).Result()
|
|
||||||
assert.Error(t, err, "ticket 应该已被删除")
|
|
||||||
|
|
||||||
// --- 旧代码逻辑(有 bug)---
|
|
||||||
oldIsFreeMode := false
|
|
||||||
if err == nil {
|
|
||||||
// 只有在 Redis 中找到 ticket 时才能判断
|
|
||||||
// 这里 err != nil,所以 isFreeMode 保持 false
|
|
||||||
}
|
|
||||||
assert.False(t, oldIsFreeMode, "旧代码:ticket 被删除后无法判断免费模式")
|
|
||||||
|
|
||||||
// --- 新代码逻辑(已修复)---
|
|
||||||
req := settleRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Ticket: ticket,
|
|
||||||
GameType: "minesweeper_free", // 直接从请求参数获取
|
|
||||||
}
|
|
||||||
newIsFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
assert.True(t, newIsFreeMode, "新代码:直接从 game_type 判断,不受 Redis 影响")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSettleGame_Integration 集成测试(模拟完整的 HTTP 请求)
|
|
||||||
func TestSettleGame_Integration(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
request settleRequest
|
|
||||||
expectedStatus int
|
|
||||||
checkResponse func(t *testing.T, body []byte)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "免费模式结算_应返回体验模式无奖励",
|
|
||||||
request: settleRequest{
|
|
||||||
UserID: "12345",
|
|
||||||
Ticket: "GT123456789",
|
|
||||||
MatchID: "match-001",
|
|
||||||
Win: true,
|
|
||||||
Score: 100,
|
|
||||||
GameType: "minesweeper_free",
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
checkResponse: func(t *testing.T, body []byte) {
|
|
||||||
var resp settleResponse
|
|
||||||
err := json.Unmarshal(body, &resp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, resp.Success)
|
|
||||||
assert.Equal(t, "体验模式无奖励", resp.Reward)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// 创建模拟的 handler(简化版,仅测试免费模式判断逻辑)
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/internal/game/settle", func(c *gin.Context) {
|
|
||||||
var req settleRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 核心逻辑:直接从请求参数判断
|
|
||||||
isFreeMode := req.GameType == "minesweeper_free"
|
|
||||||
if isFreeMode {
|
|
||||||
c.JSON(http.StatusOK, settleResponse{
|
|
||||||
Success: true,
|
|
||||||
Reward: "体验模式无奖励",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 付费模式发奖逻辑(简化)
|
|
||||||
c.JSON(http.StatusOK, settleResponse{
|
|
||||||
Success: true,
|
|
||||||
Reward: "100积分",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
body, _ := json.Marshal(tt.request)
|
|
||||||
req, _ := http.NewRequest("POST", "/internal/game/settle", bytes.NewBuffer(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
if tt.checkResponse != nil {
|
|
||||||
respBody, _ := io.ReadAll(w.Body)
|
|
||||||
tt.checkResponse(t, respBody)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkFreeModeCheck 性能测试:对比新旧实现
|
|
||||||
func BenchmarkFreeModeCheck(b *testing.B) {
|
|
||||||
// 旧实现:需要查询 Redis
|
|
||||||
b.Run("旧实现_Redis查询", func(b *testing.B) {
|
|
||||||
mr, _ := miniredis.Run()
|
|
||||||
defer mr.Close()
|
|
||||||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
ticket := "GT123456789"
|
|
||||||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
|
||||||
rdb.Set(ctx, ticketKey, "12345:minesweeper_free", 30*time.Minute)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
val, err := rdb.Get(ctx, ticketKey).Result()
|
|
||||||
if err == nil {
|
|
||||||
_ = val == "12345:minesweeper_free"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 新实现:直接比较字符串
|
|
||||||
b.Run("新实现_字符串比较", func(b *testing.B) {
|
|
||||||
gameType := "minesweeper_free"
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = gameType == "minesweeper_free"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/pay"
|
"bindbox-game/internal/pkg/pay"
|
||||||
@ -15,7 +16,6 @@ import (
|
|||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||||
|
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type notifyAck struct {
|
type notifyAck struct {
|
||||||
@ -44,61 +45,39 @@ type notifyAck struct {
|
|||||||
// @Router /pay/wechat/notify [post]
|
// @Router /pay/wechat/notify [post]
|
||||||
func (h *handler) WechatNotify() core.HandlerFunc {
|
func (h *handler) WechatNotify() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
// Use dynamic configurations exclusively
|
c := configs.Get()
|
||||||
dc := sysconfig.GetDynamicConfig()
|
if c.WechatPay.ApiV3Key == "" {
|
||||||
cfg := dc.GetWechatPay(ctx.RequestContext().Context)
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
|
||||||
|
|
||||||
if cfg.ApiV3Key == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config (ApiV3Key) missing"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var handler *notify.Handler
|
||||||
mchID := cfg.MchID
|
if c.WechatPay.PublicKeyID != "" && c.WechatPay.PublicKeyPath != "" {
|
||||||
serialNo := cfg.SerialNo
|
pubKey, err := utils.LoadPublicKeyWithPath(c.WechatPay.PublicKeyPath)
|
||||||
apiV3Key := cfg.ApiV3Key
|
|
||||||
publicKeyID := cfg.PublicKeyID
|
|
||||||
|
|
||||||
var notifyHandler *notify.Handler
|
|
||||||
if publicKeyID != "" {
|
|
||||||
// 使用公钥验签模式
|
|
||||||
if cfg.PublicKey == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay public key content missing"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pubKey, err := pay.LoadPublicKeyFromBase64(cfg.PublicKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, "load public key err: "+err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifyHandler = notify.NewNotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(publicKeyID, *pubKey))
|
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(c.WechatPay.PublicKeyID, *pubKey))
|
||||||
} else {
|
} else {
|
||||||
// 使用证书自动下载模式
|
if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" {
|
||||||
if mchID == "" || serialNo == "" {
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay mchid/serial_no missing for cert mode"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.WechatPay.PrivateKeyPath)
|
||||||
if cfg.PrivateKey == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay private key missing"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mchPrivateKey, err := pay.LoadPrivateKeyFromBase64(cfg.PrivateKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, "load private key err: "+err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext(), mchPrivateKey, c.WechatPay.SerialNo, c.WechatPay.MchID, c.WechatPay.ApiV3Key); err != nil {
|
||||||
if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext().Context, mchPrivateKey, serialNo, mchID, apiV3Key); err != nil {
|
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, err.Error()))
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, "register downloader err: "+err.Error()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
|
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(c.WechatPay.MchID)
|
||||||
notifyHandler = notify.NewNotifyHandler(apiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
|
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
|
||||||
}
|
}
|
||||||
|
|
||||||
var transaction payments.Transaction
|
var transaction payments.Transaction
|
||||||
notification, err := notifyHandler.ParseNotifyRequest(ctx.RequestContext().Context, ctx.Request(), &transaction)
|
notification, err := handler.ParseNotifyRequest(ctx.RequestContext(), ctx.Request(), &transaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
|
||||||
return
|
return
|
||||||
@ -299,18 +278,6 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
rmk := remark.Parse(ord.Remark)
|
rmk := remark.Parse(ord.Remark)
|
||||||
act, _ := h.readDB.Activities.WithContext(bgCtx).Where(h.readDB.Activities.ID.Eq(rmk.ActivityID)).First()
|
act, _ := h.readDB.Activities.WithContext(bgCtx).Where(h.readDB.Activities.ID.Eq(rmk.ActivityID)).First()
|
||||||
|
|
||||||
// 获取微信配置 (动态)
|
|
||||||
var wxConfig *wechat.WechatConfig
|
|
||||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
|
||||||
cfg := dc.GetWechat(bgCtx)
|
|
||||||
wxConfig = &wechat.WechatConfig{AppID: cfg.AppID, AppSecret: cfg.AppSecret}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wxConfig == nil || wxConfig.AppID == "" {
|
|
||||||
h.logger.Error("微信配置缺失(AppID为空),跳过虚拟发货/抽奖", zap.String("order_no", order.OrderNo))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ord.SourceType == 2 && act != nil && act.DrawMode == "instant" {
|
if ord.SourceType == 2 && act != nil && act.DrawMode == "instant" {
|
||||||
_ = h.activity.ProcessOrderLottery(bgCtx, ord.ID)
|
_ = h.activity.ProcessOrderLottery(bgCtx, ord.ID)
|
||||||
} else if ord.SourceType == 4 {
|
} else if ord.SourceType == 4 {
|
||||||
@ -344,7 +311,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}(); txID != "" {
|
}(); txID != "" {
|
||||||
if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
||||||
h.logger.Error("次数卡虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
h.logger.Error("次数卡虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
||||||
} else {
|
} else {
|
||||||
h.logger.Info("次数卡虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
h.logger.Info("次数卡虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
||||||
@ -363,7 +330,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}(); txID != "" {
|
}(); txID != "" {
|
||||||
if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
||||||
h.logger.Error("对对碰虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
h.logger.Error("对对碰虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
||||||
} else {
|
} else {
|
||||||
h.logger.Info("对对碰虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
h.logger.Info("对对碰虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
||||||
@ -382,7 +349,7 @@ func (h *handler) WechatNotify() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}(); txID != "" {
|
}(); txID != "" {
|
||||||
if err := wechat.UploadVirtualShippingForBackground(bgCtx, wxConfig, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: configs.Get().Wechat.AppID, AppSecret: configs.Get().Wechat.AppSecret}, txID, ord.OrderNo, payerOpenid, itemsDesc); err != nil {
|
||||||
h.logger.Error("商户订单虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
h.logger.Error("商户订单虚拟发货失败", zap.Error(err), zap.String("order_no", ord.OrderNo))
|
||||||
} else {
|
} else {
|
||||||
h.logger.Info("商户订单虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
h.logger.Info("商户订单虚拟发货成功", zap.String("order_no", ord.OrderNo))
|
||||||
|
|||||||
@ -1,454 +0,0 @@
|
|||||||
package public
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/pkg/logger"
|
|
||||||
"bindbox-game/internal/repository/mysql"
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
douyinsvc "bindbox-game/internal/service/douyin"
|
|
||||||
gamesvc "bindbox-game/internal/service/game"
|
|
||||||
livestreamsvc "bindbox-game/internal/service/livestream"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type handler struct {
|
|
||||||
logger logger.CustomLogger
|
|
||||||
repo mysql.Repo
|
|
||||||
livestream livestreamsvc.Service
|
|
||||||
douyin douyinsvc.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// New 创建公开接口处理器
|
|
||||||
func New(l logger.CustomLogger, repo mysql.Repo, douyin douyinsvc.Service) *handler {
|
|
||||||
ticketSvc := gamesvc.NewTicketService(l, repo)
|
|
||||||
return &handler{
|
|
||||||
logger: l,
|
|
||||||
repo: repo,
|
|
||||||
livestream: livestreamsvc.New(l, repo, ticketSvc),
|
|
||||||
douyin: douyin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 直播间公开接口 ==========
|
|
||||||
|
|
||||||
type publicActivityResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
StreamerName string `json:"streamer_name"`
|
|
||||||
Status int32 `json:"status"`
|
|
||||||
StartTime string `json:"start_time,omitempty"`
|
|
||||||
EndTime string `json:"end_time,omitempty"`
|
|
||||||
Prizes []publicPrizeResponse `json:"prizes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicPrizeResponse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
Remaining int32 `json:"remaining"`
|
|
||||||
Probability string `json:"probability"`
|
|
||||||
Weight int32 `json:"weight"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamByAccessCode 根据访问码获取直播间活动详情
|
|
||||||
// @Summary 获取直播间活动详情(公开)
|
|
||||||
// @Description 根据访问码获取直播间活动和奖品信息,无需登录
|
|
||||||
// @Tags 公开接口.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param access_code path string true "访问码"
|
|
||||||
// @Success 200 {object} publicActivityResponse
|
|
||||||
// @Failure 404 {object} code.Failure
|
|
||||||
// @Router /api/public/livestream/{access_code} [get]
|
|
||||||
func (h *handler) GetLivestreamByAccessCode() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
accessCode := ctx.Param("access_code")
|
|
||||||
if accessCode == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在或已结束"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prizes, _ := h.livestream.ListPrizes(ctx.RequestContext(), activity.ID)
|
|
||||||
|
|
||||||
res := &publicActivityResponse{
|
|
||||||
ID: activity.ID,
|
|
||||||
Name: activity.Name,
|
|
||||||
StreamerName: activity.StreamerName,
|
|
||||||
Status: activity.Status,
|
|
||||||
Prizes: make([]publicPrizeResponse, len(prizes)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if !activity.StartTime.IsZero() {
|
|
||||||
res.StartTime = activity.StartTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !activity.EndTime.IsZero() {
|
|
||||||
res.EndTime = activity.EndTime.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算总权重 (仅统计有库存的)
|
|
||||||
var totalWeight int64
|
|
||||||
for _, p := range prizes {
|
|
||||||
if p.Remaining != 0 {
|
|
||||||
totalWeight += int64(p.Weight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, p := range prizes {
|
|
||||||
probStr := "0%"
|
|
||||||
if p.Remaining != 0 && totalWeight > 0 {
|
|
||||||
prob := (float64(p.Weight) / float64(totalWeight)) * 100
|
|
||||||
probStr = fmt.Sprintf("%.2f%%", prob)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Prizes[i] = publicPrizeResponse{
|
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
|
||||||
Image: p.Image,
|
|
||||||
Level: p.Level,
|
|
||||||
Remaining: p.Remaining,
|
|
||||||
Probability: probStr,
|
|
||||||
Weight: p.Weight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type publicDrawLogResponse struct {
|
|
||||||
PrizeName string `json:"prize_name"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
DouyinUserID string `json:"douyin_user_id"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listPublicDrawLogsResponse struct {
|
|
||||||
List []publicDrawLogResponse `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamWinners 获取中奖记录(公开)
|
|
||||||
// @Summary 获取直播间中奖记录(公开)
|
|
||||||
// @Description 根据访问码获取直播间中奖历史,无需登录
|
|
||||||
// @Tags 公开接口.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param access_code path string true "访问码"
|
|
||||||
// @Param page query int false "页码" default(1)
|
|
||||||
// @Param page_size query int false "每页数量" default(20)
|
|
||||||
// @Success 200 {object} listPublicDrawLogsResponse
|
|
||||||
// @Failure 404 {object} code.Failure
|
|
||||||
// @Router /api/public/livestream/{access_code}/winners [get]
|
|
||||||
func (h *handler) GetLivestreamWinners() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
accessCode := ctx.Param("access_code")
|
|
||||||
if accessCode == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
page := 1 // Default page 1
|
|
||||||
pageSize := 20 // Default pageSize 20
|
|
||||||
|
|
||||||
var startTime, endTime *time.Time
|
|
||||||
|
|
||||||
params := ctx.RequestInputParams()
|
|
||||||
if stStr := params.Get("start_time"); stStr != "" {
|
|
||||||
if t, err := time.Parse(time.RFC3339, stStr); err == nil {
|
|
||||||
startTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if etStr := params.Get("end_time"); etStr != "" {
|
|
||||||
if t, err := time.Parse(time.RFC3339, etStr); err == nil {
|
|
||||||
endTime = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, total, err := h.livestream.ListDrawLogs(ctx.RequestContext(), activity.ID, page, pageSize, startTime, endTime)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &listPublicDrawLogsResponse{
|
|
||||||
List: make([]publicDrawLogResponse, len(logs)),
|
|
||||||
Total: total,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, log := range logs {
|
|
||||||
// 隐藏部分抖音ID
|
|
||||||
maskedID := log.DouyinUserID
|
|
||||||
if len(maskedID) > 4 {
|
|
||||||
maskedID = maskedID[:2] + "****" + maskedID[len(maskedID)-2:]
|
|
||||||
}
|
|
||||||
|
|
||||||
res.List[i] = publicDrawLogResponse{
|
|
||||||
PrizeName: log.PrizeName,
|
|
||||||
Level: log.Level,
|
|
||||||
DouyinUserID: maskedID,
|
|
||||||
CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 直播间抽奖接口 ==========
|
|
||||||
|
|
||||||
type drawRequest struct {
|
|
||||||
DouyinOrderID string `json:"shop_order_id" binding:"required"` // 店铺订单号
|
|
||||||
DouyinUserID string `json:"douyin_user_id"` // 可选,兼容旧逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
type drawReceipt struct {
|
|
||||||
SeedVersion int32 `json:"seed_version"`
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Nonce int64 `json:"nonce"`
|
|
||||||
Signature string `json:"signature"`
|
|
||||||
Algorithm string `json:"algorithm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type drawResponse struct {
|
|
||||||
PrizeID int64 `json:"prize_id"`
|
|
||||||
PrizeName string `json:"prize_name"`
|
|
||||||
PrizeImage string `json:"prize_image"`
|
|
||||||
Level int32 `json:"level"`
|
|
||||||
SeedHash string `json:"seed_hash"`
|
|
||||||
UserNickname string `json:"user_nickname"`
|
|
||||||
Receipt *drawReceipt `json:"receipt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawLivestream 执行直播间抽奖
|
|
||||||
// @Summary 执行直播间抽奖(公开)
|
|
||||||
// @Description 根据访问码执行抽奖,需提供抖音用户ID
|
|
||||||
// @Tags 公开接口.直播间
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param access_code path string true "访问码"
|
|
||||||
// @Param body body drawRequest true "抽奖参数"
|
|
||||||
// @Success 200 {object} drawResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Failure 404 {object} code.Failure
|
|
||||||
// @Router /api/public/livestream/{access_code}/draw [post]
|
|
||||||
func (h *handler) DrawLivestream() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
accessCode := ctx.Param("access_code")
|
|
||||||
if accessCode == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req drawRequest
|
|
||||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10002, "参数格式错误"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10003, "活动不存在或已结束"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if activity.Status != 1 {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10004, "活动未开始"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. [核心重构] 根据店铺订单号查出本地记录并核销
|
|
||||||
var order model.DouyinOrders
|
|
||||||
db := h.repo.GetDbW().WithContext(ctx.RequestContext())
|
|
||||||
|
|
||||||
err = db.Where("shop_order_id = ?", req.DouyinOrderID).First(&order).Error
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10005, "订单不存在"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if order.RewardGranted >= int32(order.ProductCount) {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, "该订单已完成抽奖,请勿重复操作"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行抽奖
|
|
||||||
result, err := h.livestream.Draw(ctx.RequestContext(), livestreamsvc.DrawInput{
|
|
||||||
ActivityID: activity.ID,
|
|
||||||
DouyinOrderID: order.ID,
|
|
||||||
ShopOrderID: order.ShopOrderID,
|
|
||||||
DouyinUserID: order.DouyinUserID,
|
|
||||||
UserNickname: order.UserNickname,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// 检查是否为黑名单错误
|
|
||||||
if err.Error() == "该用户已被列入黑名单,无法开奖" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusForbidden, 10008, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10007, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记订单已核销 (增加已发放计数)
|
|
||||||
// 使用 GORM 表达式更新,确保并发安全
|
|
||||||
// update douyin_orders set reward_granted = reward_granted + 1, updated_at = now() where id = ?
|
|
||||||
if err := db.Model(&order).Update("reward_granted", gorm.Expr("reward_granted + 1")).Error; err != nil {
|
|
||||||
h.logger.Error("[Draw] 更新订单发放状态失败", zap.String("order_id", order.ShopOrderID), zap.Error(err))
|
|
||||||
// 注意:这里虽然更新失败,但已执行抽奖,可能会导致用户少一次抽奖机会(计数没加),但为了防止超发,宁可少发。
|
|
||||||
// 理想情况是放在事务中,但 livestream.Draw 内部可能有独立事务。
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &drawResponse{
|
|
||||||
PrizeID: result.Prize.ID,
|
|
||||||
PrizeName: result.Prize.Name,
|
|
||||||
PrizeImage: result.Prize.Image,
|
|
||||||
Level: result.Prize.Level,
|
|
||||||
SeedHash: result.SeedHash,
|
|
||||||
UserNickname: order.UserNickname,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充凭证信息
|
|
||||||
if result.Receipt != nil {
|
|
||||||
res.Receipt = &drawReceipt{
|
|
||||||
SeedVersion: result.Receipt.SeedVersion,
|
|
||||||
Timestamp: result.Receipt.Timestamp,
|
|
||||||
Nonce: result.Receipt.Nonce,
|
|
||||||
Signature: result.Receipt.Signature,
|
|
||||||
Algorithm: result.Receipt.Algorithm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncLivestreamOrders 触发全店订单同步并尝试发奖
|
|
||||||
func (h *handler) SyncLivestreamOrders() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
accessCode := ctx.Param("access_code")
|
|
||||||
if accessCode == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用服务执行全量扫描 (基于时间更新,覆盖最近1小时变化)
|
|
||||||
result, err := h.douyin.SyncAllOrders(ctx.RequestContext(), 1*time.Hour)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10004, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(map[string]any{
|
|
||||||
"message": "同步完成",
|
|
||||||
"total_fetched": result.TotalFetched,
|
|
||||||
"new_orders": result.NewOrders,
|
|
||||||
"matched_users": result.MatchedUsers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLivestreamPendingOrders 获取当前活动的待抽奖订单(严格模式:防止窜台)
|
|
||||||
func (h *handler) GetLivestreamPendingOrders() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
accessCode := ctx.Param("access_code")
|
|
||||||
|
|
||||||
if accessCode == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10001, "访问码不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 新增:获取活动信息,获取绑定的产品ID
|
|
||||||
activity, err := h.livestream.GetActivityByAccessCode(ctx.RequestContext(), accessCode)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusNotFound, 10002, "活动不存在或已结束"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 严格模式:如果活动未绑定产品ID,返回空列表,防止"窜台"
|
|
||||||
if activity.DouyinProductID == "" {
|
|
||||||
h.logger.Warn("[GetPendingOrders] 活动未绑定产品ID,返回空列表(防止窜台)",
|
|
||||||
zap.String("access_code", accessCode),
|
|
||||||
zap.Int64("activity_id", activity.ID))
|
|
||||||
|
|
||||||
// 返回空列表
|
|
||||||
type OrderWithBlacklist struct {
|
|
||||||
model.DouyinOrders
|
|
||||||
IsBlacklisted bool `json:"is_blacklisted"`
|
|
||||||
}
|
|
||||||
ctx.Payload([]OrderWithBlacklist{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// [核心优化] 自动同步:每次拉取待抽奖列表前,静默执行一次快速全局扫描 (最近 10 分钟)
|
|
||||||
_, _ = h.douyin.SyncAllOrders(ctx.RequestContext(), 10*time.Minute)
|
|
||||||
|
|
||||||
// ✅ 修改:添加产品ID过滤条件(核心修复,防止不同活动订单窜台)
|
|
||||||
var pendingOrders []model.DouyinOrders
|
|
||||||
db := h.repo.GetDbR().WithContext(ctx.RequestContext())
|
|
||||||
|
|
||||||
err = db.Where("order_status = 2 AND reward_granted < product_count AND douyin_product_id = ?",
|
|
||||||
activity.DouyinProductID).
|
|
||||||
Find(&pendingOrders).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 10003, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询黑名单用户
|
|
||||||
blacklistMap := make(map[string]bool)
|
|
||||||
if len(pendingOrders) > 0 {
|
|
||||||
var douyinUserIDs []string
|
|
||||||
for _, order := range pendingOrders {
|
|
||||||
if order.DouyinUserID != "" {
|
|
||||||
douyinUserIDs = append(douyinUserIDs, order.DouyinUserID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(douyinUserIDs) > 0 {
|
|
||||||
var blacklistUsers []model.DouyinBlacklist
|
|
||||||
db.Table("douyin_blacklist").
|
|
||||||
Where("douyin_user_id IN ? AND status = 1", douyinUserIDs).
|
|
||||||
Find(&blacklistUsers)
|
|
||||||
for _, bl := range blacklistUsers {
|
|
||||||
blacklistMap[bl.DouyinUserID] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造响应,包含黑名单状态
|
|
||||||
type OrderWithBlacklist struct {
|
|
||||||
model.DouyinOrders
|
|
||||||
IsBlacklisted bool `json:"is_blacklisted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]OrderWithBlacklist, len(pendingOrders))
|
|
||||||
for i, order := range pendingOrders {
|
|
||||||
result[i] = OrderWithBlacklist{
|
|
||||||
DouyinOrders: order,
|
|
||||||
IsBlacklisted: blacklistMap[order.DouyinUserID],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,8 +8,6 @@ import (
|
|||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/jwtoken"
|
"bindbox-game/internal/pkg/jwtoken"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addressShareSubmitRequest struct {
|
type addressShareSubmitRequest struct {
|
||||||
@ -60,9 +58,6 @@ func (h *handler) SubmitAddressShare() core.HandlerFunc {
|
|||||||
// 统一使用 ctx.RequestContext() 包含 context 内容
|
// 统一使用 ctx.RequestContext() 包含 context 内容
|
||||||
addrID, err := h.user.SubmitAddressShare(ctx.RequestContext(), req.ShareToken, req.Name, req.Mobile, req.Province, req.City, req.District, req.Address, submitUserID, &ip)
|
addrID, err := h.user.SubmitAddressShare(ctx.RequestContext(), req.ShareToken, req.Name, req.Mobile, req.Province, req.City, req.District, req.Address, submitUserID, &ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log the error for debugging
|
|
||||||
h.logger.Error("SubmitAddressShare API Error", zap.Error(err), zap.String("token_masked", req.ShareToken[:10]+"..."))
|
|
||||||
|
|
||||||
// 处理业务错误,映射到具体代码
|
// 处理业务错误,映射到具体代码
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
errorCode := 10024
|
errorCode := 10024
|
||||||
|
|||||||
@ -4,11 +4,7 @@ import (
|
|||||||
"bindbox-game/internal/pkg/logger"
|
"bindbox-game/internal/pkg/logger"
|
||||||
"bindbox-game/internal/repository/mysql"
|
"bindbox-game/internal/repository/mysql"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
"bindbox-game/internal/service/douyin"
|
|
||||||
gamesvc "bindbox-game/internal/service/game"
|
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
tasksvc "bindbox-game/internal/service/task_center"
|
tasksvc "bindbox-game/internal/service/task_center"
|
||||||
titlesvc "bindbox-game/internal/service/title"
|
|
||||||
usersvc "bindbox-game/internal/service/user"
|
usersvc "bindbox-game/internal/service/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,21 +14,9 @@ type handler struct {
|
|||||||
readDB *dao.Query
|
readDB *dao.Query
|
||||||
user usersvc.Service
|
user usersvc.Service
|
||||||
task tasksvc.Service
|
task tasksvc.Service
|
||||||
douyin douyin.Service
|
|
||||||
repo mysql.Repo
|
repo mysql.Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, db mysql.Repo, taskSvc tasksvc.Service) *handler {
|
func New(logger logger.CustomLogger, db mysql.Repo, taskSvc tasksvc.Service) *handler {
|
||||||
syscfgSvc := sysconfig.New(logger, db)
|
return &handler{logger: logger, writeDB: dao.Use(db.GetDbW()), readDB: dao.Use(db.GetDbR()), user: usersvc.New(logger, db), task: taskSvc, repo: db}
|
||||||
userSvc := usersvc.New(logger, db)
|
|
||||||
titleSvc := titlesvc.New(logger, db)
|
|
||||||
return &handler{
|
|
||||||
logger: logger,
|
|
||||||
writeDB: dao.Use(db.GetDbW()),
|
|
||||||
readDB: dao.Use(db.GetDbR()),
|
|
||||||
user: userSvc,
|
|
||||||
task: taskSvc,
|
|
||||||
douyin: douyin.New(logger, db, syscfgSvc, gamesvc.NewTicketService(logger, db), userSvc, titleSvc),
|
|
||||||
repo: db,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"bindbox-game/internal/code"
|
|
||||||
"bindbox-game/internal/pkg/core"
|
|
||||||
"bindbox-game/internal/pkg/validation"
|
|
||||||
)
|
|
||||||
|
|
||||||
type bindDouyinOrderRequest struct {
|
|
||||||
DouyinID string `json:"douyin_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type bindDouyinOrderResponse struct {
|
|
||||||
DouyinID string `json:"douyin_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindDouyinOrder 绑定抖音ID
|
|
||||||
// @Summary 绑定抖音ID
|
|
||||||
// @Description 输入抖音号(Buyer ID),绑定到当前用户
|
|
||||||
// @Tags APP端.用户
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Security LoginVerifyToken
|
|
||||||
// @Param RequestBody body bindDouyinOrderRequest true "请求参数"
|
|
||||||
// @Success 200 {object} bindDouyinOrderResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Router /api/app/users/douyin/bind [post]
|
|
||||||
func (h *handler) BindDouyinOrder() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(bindDouyinOrderRequest)
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.DouyinID == "" {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抖音号不能为空"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUserID := int64(ctx.SessionUserInfo().Id)
|
|
||||||
|
|
||||||
// 0. 检查当前用户信息
|
|
||||||
currentUser, err := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(currentUserID)).First()
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "获取用户信息失败"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经绑定了相同的 ID,直接返回成功
|
|
||||||
if currentUser.DouyinUserID == req.DouyinID {
|
|
||||||
ctx.Payload(&bindDouyinOrderResponse{DouyinID: req.DouyinID})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 检查该抖音号是否已被其他本地账号绑定
|
|
||||||
existedUser, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.DouyinUserID.Eq(req.DouyinID)).First()
|
|
||||||
if existedUser != nil && existedUser.ID != currentUserID {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "该抖音号已被其他账号绑定"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 更新本地用户表的 douyin_user_id
|
|
||||||
if _, err := h.writeDB.Users.WithContext(ctx.RequestContext()).Where(h.writeDB.Users.ID.Eq(currentUserID)).Updates(map[string]any{
|
|
||||||
"douyin_user_id": req.DouyinID,
|
|
||||||
}); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "更新用户信息失败"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(&bindDouyinOrderResponse{
|
|
||||||
DouyinID: req.DouyinID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -30,10 +30,8 @@ type couponItem struct {
|
|||||||
ValidStart string `json:"valid_start"`
|
ValidStart string `json:"valid_start"`
|
||||||
ValidEnd string `json:"valid_end"`
|
ValidEnd string `json:"valid_end"`
|
||||||
Status int32 `json:"status"`
|
Status int32 `json:"status"`
|
||||||
StatusDesc string `json:"status_desc"` // 状态描述:未使用、已用完、已过期
|
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
UsedAt string `json:"used_at,omitempty"` // 使用时间(已使用时返回)
|
UsedAt string `json:"used_at,omitempty"` // 使用时间(已使用时返回)
|
||||||
UsedAmount int64 `json:"used_amount"` // 已使用金额
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUserCoupons 查看用户优惠券
|
// ListUserCoupons 查看用户优惠券
|
||||||
@ -60,13 +58,13 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
userID := int64(ctx.SessionUserInfo().Id)
|
userID := int64(ctx.SessionUserInfo().Id)
|
||||||
|
|
||||||
// 状态:0未使用 1已使用 2已过期 (直接对接前端标准)
|
// 默认查询未使用的优惠券
|
||||||
status := int32(0)
|
status := int32(1)
|
||||||
if req.Status != nil {
|
if req.Status != nil && *req.Status > 0 {
|
||||||
status = *req.Status
|
status = *req.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
items, total, err := h.user.ListAppCoupons(ctx.RequestContext(), userID, status, req.Page, req.PageSize)
|
items, total, err := h.user.ListCouponsByStatus(ctx.RequestContext(), userID, status, req.Page, req.PageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10003, err.Error()))
|
||||||
return
|
return
|
||||||
@ -102,8 +100,14 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
rules := ""
|
rules := ""
|
||||||
if sc != nil {
|
if sc != nil {
|
||||||
name = sc.Name
|
name = sc.Name
|
||||||
|
// 金额券:amount 显示模板面值,remaining 显示当前余额
|
||||||
|
if sc.DiscountType == 1 {
|
||||||
amount = sc.DiscountValue
|
amount = sc.DiscountValue
|
||||||
remaining = it.BalanceAmount
|
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", it.ID).Scan(&remaining).Error
|
||||||
|
} else {
|
||||||
|
amount = sc.DiscountValue
|
||||||
|
remaining = sc.DiscountValue
|
||||||
|
}
|
||||||
rules = buildCouponRules(sc)
|
rules = buildCouponRules(sc)
|
||||||
}
|
}
|
||||||
vs := it.ValidStart.Format("2006-01-02 15:04:05")
|
vs := it.ValidStart.Format("2006-01-02 15:04:05")
|
||||||
@ -115,24 +119,7 @@ func (h *handler) ListUserCoupons() core.HandlerFunc {
|
|||||||
if !it.UsedAt.IsZero() {
|
if !it.UsedAt.IsZero() {
|
||||||
usedAt = it.UsedAt.Format("2006-01-02 15:04:05")
|
usedAt = it.UsedAt.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
statusDesc := "未使用"
|
vi := couponItem{ID: it.ID, Name: name, Amount: amount, Remaining: remaining, ValidStart: vs, ValidEnd: ve, Status: it.Status, Rules: rules, UsedAt: usedAt}
|
||||||
if it.Status == 2 {
|
|
||||||
if it.BalanceAmount == 0 {
|
|
||||||
statusDesc = "已使用"
|
|
||||||
} else {
|
|
||||||
statusDesc = "使用中"
|
|
||||||
}
|
|
||||||
} else if it.Status == 3 {
|
|
||||||
// 若面值等于余额,说明完全没用过,否则为“已到期”
|
|
||||||
sc, ok := mp[it.CouponID]
|
|
||||||
if ok && it.BalanceAmount < sc.DiscountValue {
|
|
||||||
statusDesc = "已到期"
|
|
||||||
} else {
|
|
||||||
statusDesc = "已过期"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
usedAmount := amount - remaining
|
|
||||||
vi := couponItem{ID: it.ID, Name: name, Amount: amount, Remaining: remaining, UsedAmount: usedAmount, ValidStart: vs, ValidEnd: ve, Status: it.Status, StatusDesc: statusDesc, Rules: rules, UsedAt: usedAt}
|
|
||||||
rsp.List = append(rsp.List, vi)
|
rsp.List = append(rsp.List, vi)
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== 用户次数卡 API ====================
|
// ==================== 用户次数卡 API ====================
|
||||||
@ -200,7 +199,6 @@ func (h *handler) GetGamePassPackages() core.HandlerFunc {
|
|||||||
type purchasePackageRequest struct {
|
type purchasePackageRequest struct {
|
||||||
PackageID int64 `json:"package_id" binding:"required"`
|
PackageID int64 `json:"package_id" binding:"required"`
|
||||||
Count int32 `json:"count"` // 购买数量
|
Count int32 `json:"count"` // 购买数量
|
||||||
CouponIDs []int64 `json:"coupon_ids"` // 优惠券ID列表
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type purchasePackageResponse struct {
|
type purchasePackageResponse struct {
|
||||||
@ -210,7 +208,7 @@ type purchasePackageResponse struct {
|
|||||||
|
|
||||||
// PurchaseGamePassPackage 购买次数卡套餐(创建订单)
|
// PurchaseGamePassPackage 购买次数卡套餐(创建订单)
|
||||||
// @Summary 购买次数卡套餐
|
// @Summary 购买次数卡套餐
|
||||||
// @Description 购买次数卡套餐,创建订单等待支付,支持使用优惠券
|
// @Description 购买次数卡套餐,创建订单等待支付
|
||||||
// @Tags APP端.用户
|
// @Tags APP端.用户
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@ -247,7 +245,7 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
|||||||
// Calculate total price
|
// Calculate total price
|
||||||
totalPrice := pkg.Price * int64(req.Count)
|
totalPrice := pkg.Price * int64(req.Count)
|
||||||
|
|
||||||
// 创建订单 (支持优惠券)
|
// 创建订单
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
orderNo := now.Format("20060102150405") + fmt.Sprintf("%04d", now.UnixNano()%10000)
|
orderNo := now.Format("20060102150405") + fmt.Sprintf("%04d", now.UnixNano()%10000)
|
||||||
order := &model.Orders{
|
order := &model.Orders{
|
||||||
@ -257,33 +255,11 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
|||||||
TotalAmount: totalPrice,
|
TotalAmount: totalPrice,
|
||||||
ActualAmount: totalPrice,
|
ActualAmount: totalPrice,
|
||||||
Status: 1, // 待支付
|
Status: 1, // 待支付
|
||||||
Remark: fmt.Sprintf("game_pass_package:%s|pkg_id:%d|count:%d", pkg.Name, pkg.ID, req.Count),
|
Remark: fmt.Sprintf("game_pass_package:%s|count:%d", pkg.Name, req.Count),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用优惠券 (如果有)
|
|
||||||
var appliedCouponVal int64
|
|
||||||
var couponID int64
|
|
||||||
|
|
||||||
if len(req.CouponIDs) > 0 {
|
|
||||||
couponID = req.CouponIDs[0]
|
|
||||||
// 调用优惠券应用函数
|
|
||||||
applied, err := h.applyCouponToGamePassOrder(ctx, order, userID, couponID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appliedCouponVal = applied
|
|
||||||
|
|
||||||
// 记录优惠券到订单
|
|
||||||
if appliedCouponVal > 0 {
|
|
||||||
order.CouponID = couponID
|
|
||||||
order.Remark += fmt.Sprintf("|coupon:%d", couponID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存订单
|
|
||||||
if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
||||||
Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).
|
Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).
|
||||||
Create(order); err != nil {
|
Create(order); err != nil {
|
||||||
@ -291,222 +267,14 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果使用了优惠券,记录到order_coupons表
|
// 在备注中记录套餐ID和数量
|
||||||
if appliedCouponVal > 0 {
|
remark := fmt.Sprintf("%s|pkg_id:%d|count:%d", order.Remark, pkg.ID, req.Count)
|
||||||
_ = h.writeDB.OrderCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
|
||||||
"INSERT IGNORE INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))",
|
|
||||||
order.ID, couponID, appliedCouponVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理0元订单
|
|
||||||
if order.ActualAmount == 0 {
|
|
||||||
order.Status = 2
|
|
||||||
order.PaidAt = now
|
|
||||||
h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
||||||
Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).
|
Where(h.writeDB.Orders.ID.Eq(order.ID)).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{"remark": remark})
|
||||||
h.writeDB.Orders.Status.ColumnName().String(): 2,
|
|
||||||
h.writeDB.Orders.PaidAt.ColumnName().String(): now,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 0元订单确认优惠券扣减
|
|
||||||
if appliedCouponVal > 0 {
|
|
||||||
h.confirmCouponUsage(ctx, couponID, order.ID, now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.OrderNo = order.OrderNo
|
res.OrderNo = order.OrderNo
|
||||||
res.Message = "订单创建成功,请完成支付"
|
res.Message = "订单创建成功,请完成支付"
|
||||||
ctx.Payload(res)
|
ctx.Payload(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 优惠券辅助函数 ====================
|
|
||||||
|
|
||||||
// applyCouponToGamePassOrder 应用优惠券到次卡购买订单
|
|
||||||
// 返回应用的优惠金额 (分)
|
|
||||||
func (h *handler) applyCouponToGamePassOrder(ctx core.Context, order *model.Orders, userID int64, userCouponID int64) (int64, error) {
|
|
||||||
// 使用 SELECT ... FOR UPDATE 锁定行
|
|
||||||
uc, _ := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Where(
|
|
||||||
h.writeDB.UserCoupons.ID.Eq(userCouponID),
|
|
||||||
h.writeDB.UserCoupons.UserID.Eq(userID),
|
|
||||||
).First()
|
|
||||||
|
|
||||||
if uc == nil {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查状态 (必须是可用状态)
|
|
||||||
if uc.Status != 1 {
|
|
||||||
return 0, fmt.Errorf("优惠券不可用")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取优惠券模板
|
|
||||||
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).
|
|
||||||
First()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if sc == nil {
|
|
||||||
return 0, fmt.Errorf("优惠券模板不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证有效期
|
|
||||||
if uc.ValidStart.After(now) {
|
|
||||||
return 0, fmt.Errorf("优惠券未到开始时间")
|
|
||||||
}
|
|
||||||
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
|
||||||
return 0, fmt.Errorf("优惠券已过期")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证使用范围 (次卡购买只支持全场券 scope_type=1)
|
|
||||||
if sc.ScopeType != 1 {
|
|
||||||
return 0, fmt.Errorf("次卡购买仅支持全场通用优惠券")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证门槛
|
|
||||||
if order.TotalAmount < sc.MinSpend {
|
|
||||||
return 0, fmt.Errorf("未达优惠券使用门槛")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 50% 封顶
|
|
||||||
cap := order.TotalAmount / 2
|
|
||||||
remainingCap := cap - order.DiscountAmount
|
|
||||||
if remainingCap <= 0 {
|
|
||||||
return 0, fmt.Errorf("已达优惠封顶限制")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算优惠金额
|
|
||||||
applied := int64(0)
|
|
||||||
switch sc.DiscountType {
|
|
||||||
case 1: // 金额券
|
|
||||||
bal := uc.BalanceAmount
|
|
||||||
if bal > 0 {
|
|
||||||
if bal > remainingCap {
|
|
||||||
applied = remainingCap
|
|
||||||
} else {
|
|
||||||
applied = bal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 2: // 满减券
|
|
||||||
applied = sc.DiscountValue
|
|
||||||
if applied > remainingCap {
|
|
||||||
applied = remainingCap
|
|
||||||
}
|
|
||||||
case 3: // 折扣券
|
|
||||||
rate := sc.DiscountValue
|
|
||||||
if rate < 0 {
|
|
||||||
rate = 0
|
|
||||||
}
|
|
||||||
if rate > 1000 {
|
|
||||||
rate = 1000
|
|
||||||
}
|
|
||||||
newAmt := order.ActualAmount * rate / 1000
|
|
||||||
d := order.ActualAmount - newAmt
|
|
||||||
if d > remainingCap {
|
|
||||||
applied = remainingCap
|
|
||||||
} else {
|
|
||||||
applied = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if applied > order.ActualAmount {
|
|
||||||
applied = order.ActualAmount
|
|
||||||
}
|
|
||||||
if applied <= 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新订单金额
|
|
||||||
order.ActualAmount -= applied
|
|
||||||
order.DiscountAmount += applied
|
|
||||||
|
|
||||||
// 扣减优惠券余额或标记为冻结
|
|
||||||
if sc.DiscountType == 1 {
|
|
||||||
// 金额券:直接扣余额
|
|
||||||
newBal := uc.BalanceAmount - applied
|
|
||||||
newStatus := int32(1)
|
|
||||||
if newBal <= 0 {
|
|
||||||
newBal = 0
|
|
||||||
newStatus = 2 // 已使用
|
|
||||||
}
|
|
||||||
res := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
|
||||||
"UPDATE user_coupons SET balance_amount = ?, status = ? WHERE id = ?",
|
|
||||||
newBal, newStatus, userCouponID)
|
|
||||||
if res.Error != nil {
|
|
||||||
return 0, res.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录流水
|
|
||||||
_ = h.writeDB.UserCouponLedger.WithContext(ctx.RequestContext()).Create(&model.UserCouponLedger{
|
|
||||||
UserID: userID,
|
|
||||||
UserCouponID: userCouponID,
|
|
||||||
ChangeAmount: -applied,
|
|
||||||
BalanceAfter: newBal,
|
|
||||||
OrderID: order.ID,
|
|
||||||
Action: "reserve", // 预扣
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 满减/折扣券:标记为冻结 (状态4)
|
|
||||||
res := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
|
||||||
"UPDATE user_coupons SET status = 4 WHERE id = ? AND status = 1", userCouponID)
|
|
||||||
if res.Error != nil {
|
|
||||||
return 0, res.Error
|
|
||||||
}
|
|
||||||
if res.RowsAffected == 0 {
|
|
||||||
return 0, fmt.Errorf("优惠券已被使用")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录流水
|
|
||||||
_ = h.writeDB.UserCouponLedger.WithContext(ctx.RequestContext()).Create(&model.UserCouponLedger{
|
|
||||||
UserID: userID,
|
|
||||||
UserCouponID: userCouponID,
|
|
||||||
ChangeAmount: 0,
|
|
||||||
BalanceAfter: 0,
|
|
||||||
OrderID: order.ID,
|
|
||||||
Action: "reserve",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return applied, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// confirmCouponUsage 确认优惠券使用 (支付成功后调用)
|
|
||||||
func (h *handler) confirmCouponUsage(ctx core.Context, userCouponID int64, orderID int64, paidAt time.Time) {
|
|
||||||
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.readDB.UserCoupons.ID.Eq(userCouponID)).
|
|
||||||
First()
|
|
||||||
if uc == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID)).
|
|
||||||
First()
|
|
||||||
if sc == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 金额券:余额已经扣减,只需记录used_order_id
|
|
||||||
if sc.DiscountType == 1 {
|
|
||||||
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.writeDB.UserCoupons.ID.Eq(userCouponID)).
|
|
||||||
Updates(map[string]any{
|
|
||||||
h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
|
||||||
h.writeDB.UserCoupons.UsedAt.ColumnName().String(): paidAt,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 满减/折扣券:状态从冻结(4)改为已使用(2)
|
|
||||||
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.writeDB.UserCoupons.ID.Eq(userCouponID)).
|
|
||||||
Updates(map[string]any{
|
|
||||||
h.writeDB.UserCoupons.Status.ColumnName().String(): 2,
|
|
||||||
h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
|
||||||
h.writeDB.UserCoupons.UsedAt.ColumnName().String(): paidAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ import (
|
|||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/proposal"
|
"bindbox-game/internal/proposal"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
usersvc "bindbox-game/internal/service/user"
|
usersvc "bindbox-game/internal/service/user"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -27,7 +25,6 @@ type weixinLoginResponse struct {
|
|||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Mobile string `json:"mobile"` // 新增手机号字段
|
|
||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
OpenID string `json:"openid"`
|
OpenID string `json:"openid"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@ -51,12 +48,8 @@ func (h *handler) WeixinLogin() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Use dynamic config
|
cfg := configs.Get()
|
||||||
wxCfgVal := sysconfig.GetDynamicConfig().GetWechat(ctx.RequestContext().Context)
|
wxcfg := &wechat.WechatConfig{AppID: cfg.Wechat.AppID, AppSecret: cfg.Wechat.AppSecret}
|
||||||
wxcfg := &wechat.WechatConfig{AppID: wxCfgVal.AppID, AppSecret: wxCfgVal.AppSecret}
|
|
||||||
|
|
||||||
fmt.Printf("DEBUG WeixinLogin: Using Config AppID=%s\n", wxcfg.AppID)
|
|
||||||
|
|
||||||
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
|
c2s, err := wechat.Code2Session(ctx.RequestContext().Context, wxcfg, req.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10006, err.Error()))
|
||||||
@ -81,7 +74,6 @@ func (h *handler) WeixinLogin() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rsp.Avatar = u.Avatar
|
rsp.Avatar = u.Avatar
|
||||||
rsp.Mobile = u.Mobile // 返回手机号
|
|
||||||
rsp.InviteCode = u.InviteCode
|
rsp.InviteCode = u.InviteCode
|
||||||
rsp.OpenID = c2s.OpenID
|
rsp.OpenID = c2s.OpenID
|
||||||
sessionUserInfo := proposal.SessionUserInfo{Id: int32(u.ID), UserName: u.Nickname, NickName: u.Nickname, IsSuper: 0, Platform: "APP"}
|
sessionUserInfo := proposal.SessionUserInfo{Id: int32(u.ID), UserName: u.Nickname, NickName: u.Nickname, IsSuper: 0, Platform: "APP"}
|
||||||
|
|||||||
@ -26,7 +26,6 @@ type douyinLoginResponse struct {
|
|||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Mobile string `json:"mobile"` // 新增手机号字段
|
|
||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
@ -71,7 +70,6 @@ func (h *handler) DouyinLogin() core.HandlerFunc {
|
|||||||
rsp.UserID = u.ID
|
rsp.UserID = u.ID
|
||||||
rsp.Nickname = u.Nickname
|
rsp.Nickname = u.Nickname
|
||||||
rsp.Avatar = u.Avatar
|
rsp.Avatar = u.Avatar
|
||||||
rsp.Mobile = u.Mobile // 返回手机号
|
|
||||||
rsp.InviteCode = u.InviteCode
|
rsp.InviteCode = u.InviteCode
|
||||||
|
|
||||||
// 触发邀请奖励逻辑
|
// 触发邀请奖励逻辑
|
||||||
|
|||||||
@ -3,12 +3,12 @@ package app
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/pay"
|
"bindbox-game/internal/pkg/pay"
|
||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
"bindbox-game/internal/repository/mysql/model"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type jsapiPreorderRequest struct {
|
type jsapiPreorderRequest struct {
|
||||||
@ -45,15 +45,11 @@ func (h *handler) WechatJSAPIPreorder() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok, err := pay.ValidateConfig(ctx.RequestContext()); !ok {
|
if ok, err := pay.ValidateConfig(); !ok {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140001, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140001, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Use dynamic configurations
|
c := configs.Get()
|
||||||
dynamicDC := sysconfig.GetDynamicConfig()
|
|
||||||
wxCfg := dynamicDC.GetWechat(ctx.RequestContext().Context)
|
|
||||||
wxPayCfg := dynamicDC.GetWechatPay(ctx.RequestContext().Context)
|
|
||||||
|
|
||||||
if req.OrderNo == "" || req.OpenID == "" {
|
if req.OrderNo == "" || req.OpenID == "" {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140002, "order_no/openid required"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140002, "order_no/openid required"))
|
||||||
return
|
return
|
||||||
@ -80,18 +76,18 @@ func (h *handler) WechatJSAPIPreorder() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140004, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140004, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pid, err := wc.JSAPIPrepay(ctx.RequestContext(), wxCfg.AppID, wxPayCfg.MchID, "订单"+req.OrderNo, req.OrderNo, order.ActualAmount, req.OpenID, wxPayCfg.NotifyURL)
|
pid, err := wc.JSAPIPrepay(ctx.RequestContext(), c.Wechat.AppID, c.WechatPay.MchID, "订单"+req.OrderNo, req.OrderNo, order.ActualAmount, req.OpenID, c.WechatPay.NotifyURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140005, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140005, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prepayID = pid
|
prepayID = pid
|
||||||
pre := &model.PaymentPreorders{OrderID: order.ID, OrderNo: order.OrderNo, OutTradeNo: order.OrderNo, PrepayID: prepayID, AmountTotal: order.ActualAmount, PayerOpenid: req.OpenID, NotifyURL: wxPayCfg.NotifyURL, Status: "created"}
|
pre := &model.PaymentPreorders{OrderID: order.ID, OrderNo: order.OrderNo, OutTradeNo: order.OrderNo, PrepayID: prepayID, AmountTotal: order.ActualAmount, PayerOpenid: req.OpenID, NotifyURL: c.WechatPay.NotifyURL, Status: "created"}
|
||||||
if err := h.writeDB.PaymentPreorders.WithContext(ctx.RequestContext()).Omit(h.writeDB.PaymentPreorders.ExpiredAt).Create(pre); err == nil {
|
if err := h.writeDB.PaymentPreorders.WithContext(ctx.RequestContext()).Omit(h.writeDB.PaymentPreorders.ExpiredAt).Create(pre); err == nil {
|
||||||
_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(order.ID)).Updates(map[string]any{h.readDB.Orders.PayPreorderID.ColumnName().String(): pre.ID})
|
_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(order.ID)).Updates(map[string]any{h.readDB.Orders.PayPreorderID.ColumnName().String(): pre.ID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ts, nonce, pkg, signType, paySign, err := pay.BuildJSAPIParams(ctx.RequestContext(), wxCfg.AppID, prepayID)
|
ts, nonce, pkg, signType, paySign, err := pay.BuildJSAPIParams(c.Wechat.AppID, prepayID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140003, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 140003, err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -3,12 +3,12 @@ package app
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/miniprogram"
|
"bindbox-game/internal/pkg/miniprogram"
|
||||||
"bindbox-game/internal/pkg/validation"
|
"bindbox-game/internal/pkg/validation"
|
||||||
"bindbox-game/internal/pkg/wechat"
|
"bindbox-game/internal/pkg/wechat"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -48,15 +48,11 @@ func (h *handler) BindPhone() core.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// cfg := configs.Get()
|
cfg := configs.Get()
|
||||||
// Use dynamic config
|
|
||||||
wxCfg := sysconfig.GetDynamicConfig().GetWechat(ctx.RequestContext().Context)
|
|
||||||
|
|
||||||
var tokenRes struct {
|
var tokenRes struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
}
|
}
|
||||||
if err := miniprogram.GetAccessToken(wxCfg.AppID, wxCfg.AppSecret, &tokenRes); err != nil || tokenRes.AccessToken == "" {
|
if err := miniprogram.GetAccessToken(cfg.Wechat.AppID, cfg.Wechat.AppSecret, &tokenRes); err != nil || tokenRes.AccessToken == "" {
|
||||||
h.logger.Error("获取微信access_token失败", zap.Error(err), zap.String("app_id", wxCfg.AppID), zap.String("app_secret", wxCfg.AppSecret))
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "获取微信access_token失败"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "获取微信access_token失败"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type listPointsResponse struct {
|
|||||||
List []*model.UserPointsLedger `json:"list"`
|
List []*model.UserPointsLedger `json:"list"`
|
||||||
}
|
}
|
||||||
type pointsBalanceResponse struct {
|
type pointsBalanceResponse struct {
|
||||||
Balance float64 `json:"balance"` // 积分(分/rate)
|
Balance int64 `json:"balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUserPoints 查看用户积分记录
|
// ListUserPoints 查看用户积分记录
|
||||||
@ -78,7 +78,7 @@ func (h *handler) GetUserPointsBalance() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10005, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10005, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rsp.Balance = h.user.CentsToPointsFloat(ctx.RequestContext(), total)
|
rsp.Balance = total
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,19 +52,13 @@ func (h *handler) RedeemPointsToCoupon() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150002, "only amount coupons supported"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150002, "only amount coupons supported"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// sc.DiscountValue 是优惠券面值(分),直接用于扣除
|
needPoints, _ := h.user.CentsToPoints(ctx.RequestContext(), sc.DiscountValue)
|
||||||
// 例如:30 元优惠券 = 3000 分
|
if needPoints <= 0 {
|
||||||
needCents := sc.DiscountValue
|
needPoints = 1
|
||||||
if needCents <= 0 {
|
|
||||||
needCents = 1
|
|
||||||
}
|
}
|
||||||
ledgerID, err := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needCents, "system_coupons", strconv.FormatInt(req.CouponID, 10), "redeem coupon", "redeem_coupon")
|
ledgerID, err := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needPoints, "system_coupons", strconv.FormatInt(req.CouponID, 10), "redeem coupon", "redeem_coupon")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150003, err.Error()))
|
||||||
if errMsg == "insufficient_points" {
|
|
||||||
errMsg = "积分不足,无法兑换"
|
|
||||||
}
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150003, errMsg))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
|
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
|
||||||
|
|||||||
@ -54,36 +54,17 @@ func (h *handler) RedeemPointsToProduct() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150101, "product not found"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150101, "product not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 检查商品库存
|
ptsPerUnit, _ := h.user.CentsToPoints(ctx.RequestContext(), prod.Price)
|
||||||
if prod.Stock <= 0 {
|
needPoints := ptsPerUnit * int64(req.Quantity)
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150105, "商品库存不足,请联系客服处理"))
|
if needPoints <= 0 {
|
||||||
return
|
needPoints = 1
|
||||||
}
|
}
|
||||||
// prod.Price 是商品价格(分),直接用于扣除
|
ledgerID, err := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needPoints, "products", strconv.FormatInt(req.ProductID, 10), "redeem product", "redeem_product")
|
||||||
// 例如:30 元商品 = 3000 分
|
|
||||||
needCents := prod.Price * int64(req.Quantity)
|
|
||||||
if needCents <= 0 {
|
|
||||||
needCents = 1
|
|
||||||
}
|
|
||||||
ledgerID, err := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needCents, "products", strconv.FormatInt(req.ProductID, 10), "redeem product", "redeem_product")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150102, err.Error()))
|
||||||
if errMsg == "insufficient_points" {
|
|
||||||
errMsg = "积分不足,无法完成兑换"
|
|
||||||
}
|
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150102, errMsg))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
resp, err := h.user.GrantReward(ctx.RequestContext(), userID, usersvc.GrantRewardRequest{ProductID: req.ProductID, Quantity: req.Quantity, Remark: prod.Name, PointsAmount: needPoints})
|
||||||
// Mall Direct Purchase (SourceType=1)
|
|
||||||
sourceType := int32(1)
|
|
||||||
resp, err := h.user.GrantReward(ctx.RequestContext(), userID, usersvc.GrantRewardRequest{
|
|
||||||
ProductID: req.ProductID,
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
Remark: prod.Name,
|
|
||||||
PointsAmount: needCents,
|
|
||||||
SourceType: &sourceType,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150103, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150103, err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -35,8 +35,6 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
balance, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
balance, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
||||||
// 转换为积分(浮点)用于显示
|
|
||||||
balancePoints := h.user.CentsToPointsFloat(ctx.RequestContext(), balance)
|
|
||||||
|
|
||||||
res := userItem{
|
res := userItem{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
@ -45,8 +43,7 @@ func (h *handler) GetUserProfile() core.HandlerFunc {
|
|||||||
InviteCode: user.InviteCode,
|
InviteCode: user.InviteCode,
|
||||||
InviterID: user.InviterID,
|
InviterID: user.InviterID,
|
||||||
Mobile: phone,
|
Mobile: phone,
|
||||||
DouyinUserID: user.DouyinUserID,
|
Balance: balance,
|
||||||
Balance: balancePoints,
|
|
||||||
}
|
}
|
||||||
ctx.Payload(res)
|
ctx.Payload(res)
|
||||||
}
|
}
|
||||||
@ -63,8 +60,7 @@ type userItem struct {
|
|||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
InviterID int64 `json:"inviter_id"`
|
InviterID int64 `json:"inviter_id"`
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
DouyinUserID string `json:"douyin_user_id"`
|
Balance int64 `json:"balance"` // Points
|
||||||
Balance float64 `json:"balance"` // 积分(分/rate)
|
|
||||||
}
|
}
|
||||||
type modifyUserResponse struct {
|
type modifyUserResponse struct {
|
||||||
User userItem `json:"user"`
|
User userItem `json:"user"`
|
||||||
@ -105,7 +101,6 @@ func (h *handler) ModifyUser() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
balance, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
balance, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
|
||||||
balancePoints := h.user.CentsToPointsFloat(ctx.RequestContext(), balance)
|
|
||||||
|
|
||||||
rsp.User = userItem{
|
rsp.User = userItem{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
@ -114,8 +109,7 @@ func (h *handler) ModifyUser() core.HandlerFunc {
|
|||||||
InviteCode: item.InviteCode,
|
InviteCode: item.InviteCode,
|
||||||
InviterID: item.InviterID,
|
InviterID: item.InviterID,
|
||||||
Mobile: maskedPhone,
|
Mobile: maskedPhone,
|
||||||
DouyinUserID: item.DouyinUserID,
|
Balance: balance,
|
||||||
Balance: balancePoints,
|
|
||||||
}
|
}
|
||||||
ctx.Payload(rsp)
|
ctx.Payload(rsp)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,164 +0,0 @@
|
|||||||
package wechat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"mini-chat/internal/code"
|
|
||||||
"mini-chat/internal/pkg/core"
|
|
||||||
"mini-chat/internal/pkg/miniprogram"
|
|
||||||
"mini-chat/internal/pkg/validation"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type templateRequest struct {
|
|
||||||
AppID string `json:"app_id" binding:"required"` // 微信小程序 AppID
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
AppID string `json:"app_id"` // 小程序 AppID
|
|
||||||
TemplateID string `json:"template_id"` // 模板 ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTemplate 获取微信小程序模板ID
|
|
||||||
// @Summary 获取微信小程序模板ID
|
|
||||||
// @Description 根据 AppID 获取微信小程序的模板ID
|
|
||||||
// @Tags 微信
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body templateRequest true "请求参数"
|
|
||||||
// @Success 200 {object} templateResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Failure 404 {object} code.Failure
|
|
||||||
// @Failure 500 {object} code.Failure
|
|
||||||
// @Router /api/wechat/template [post]
|
|
||||||
func (h *handler) GetTemplate() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(templateRequest)
|
|
||||||
res := new(templateResponse)
|
|
||||||
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
code.ParamBindError,
|
|
||||||
validation.Error(err),
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据 AppID 查询小程序信息
|
|
||||||
miniProgram, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
|
|
||||||
Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)).
|
|
||||||
First()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
ctx.AbortWithError(core.Error(
|
|
||||||
http.StatusNotFound,
|
|
||||||
code.ServerError,
|
|
||||||
fmt.Sprintf("未找到 AppID 为 %s 的小程序", req.AppID),
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.logger.Error(fmt.Sprintf("查询小程序信息失败: %s", err.Error()))
|
|
||||||
ctx.AbortWithError(core.Error(
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
code.ServerError,
|
|
||||||
"查询小程序信息失败",
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查模板ID是否存在
|
|
||||||
if miniProgram.TemplateID == "" {
|
|
||||||
ctx.AbortWithError(core.Error(
|
|
||||||
http.StatusNotFound,
|
|
||||||
code.ServerError,
|
|
||||||
"该小程序未配置模板ID",
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Success = true
|
|
||||||
res.Message = "获取模板ID成功"
|
|
||||||
res.AppID = miniProgram.AppID
|
|
||||||
res.TemplateID = miniProgram.TemplateID
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendSubscribeMessageRequest struct {
|
|
||||||
AppID string `json:"app_id" binding:"required"` // 微信小程序 AppID
|
|
||||||
TemplateID string `json:"template_id" binding:"required"` // 模板 ID
|
|
||||||
AppSecret string `json:"app_secret" binding:"required"` // 小程序 AppSecret
|
|
||||||
Touser string `json:"touser" binding:"required"` // 接收者(用户)的 openid
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendSubscribeMessageResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendSubscribeMessage 发送订阅消息
|
|
||||||
// @Summary 发送订阅消息
|
|
||||||
// @Description 根据模板ID发送订阅消息
|
|
||||||
// @Tags 微信
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body sendSubscribeMessageRequest true "请求参数"
|
|
||||||
// @Success 200 {object} sendSubscribeMessageResponse
|
|
||||||
// @Failure 400 {object} code.Failure
|
|
||||||
// @Failure 404 {object} code.Failure
|
|
||||||
// @Failure 500 {object} code.Failure
|
|
||||||
// @Router /api/wechat/subscribe [post]
|
|
||||||
func (h *handler) SendSubscribeMessage() core.HandlerFunc {
|
|
||||||
return func(ctx core.Context) {
|
|
||||||
req := new(sendSubscribeMessageRequest)
|
|
||||||
res := new(sendSubscribeMessageResponse)
|
|
||||||
|
|
||||||
// 参数绑定和验证
|
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
|
||||||
ctx.AbortWithError(core.Error(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
code.ParamBindError,
|
|
||||||
validation.Error(err),
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送模版消息
|
|
||||||
accessToken, err := h.servicesMiniProgram.GetAccessToken(req.AppID, req.AppSecret, ctx)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error(fmt.Sprintf("获取access_token失败: %s", err.Error()))
|
|
||||||
} else {
|
|
||||||
sendSubscribeMessageRequest := new(miniprogram.SendSubscribeMessageRequest)
|
|
||||||
sendSubscribeMessageRequest.Touser = req.Touser
|
|
||||||
sendSubscribeMessageRequest.TemplateID = req.TemplateID
|
|
||||||
sendSubscribeMessageRequest.Page = "pages/index/detail?url=1"
|
|
||||||
sendSubscribeMessageRequest.MiniprogramState = "formal" // 需要改成正式版 目前是体验版 跳转小程序类型:developer 为开发版;trial为体验版;formal 为正式版;默认为正式版
|
|
||||||
sendSubscribeMessageRequest.Lang = "zh_CN"
|
|
||||||
sendSubscribeMessageRequest.Data.Thing1.Value = "留言提醒"
|
|
||||||
sendSubscribeMessageRequest.Data.Time2.Value = time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
sendSubscribeMessageRequest.Data.Thing3.Value = "您有一条新的消息..."
|
|
||||||
|
|
||||||
sendSubscribeMessageResponse := new(miniprogram.SendSubscribeMessageResponse)
|
|
||||||
err = miniprogram.SendSubscribeMessage(accessToken, sendSubscribeMessageRequest, sendSubscribeMessageResponse)
|
|
||||||
if err != nil {
|
|
||||||
res.Success = false
|
|
||||||
res.Message = "发送订阅消息失败" + err.Error()
|
|
||||||
h.logger.Error(fmt.Sprintf("发送模版消息失败: %s", err.Error()))
|
|
||||||
} else {
|
|
||||||
res.Success = true
|
|
||||||
res.Message = "订阅消息发送成功"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Payload(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,8 +20,6 @@ const (
|
|||||||
ParamBindError = 10102
|
ParamBindError = 10102
|
||||||
JWTAuthVerifyError = 10103
|
JWTAuthVerifyError = 10103
|
||||||
UploadError = 10104
|
UploadError = 10104
|
||||||
ForbiddenError = 10105
|
|
||||||
AuthorizationError = 10106
|
|
||||||
|
|
||||||
AdminLoginError = 20101
|
AdminLoginError = 20101
|
||||||
CreateAdminError = 20207
|
CreateAdminError = 20207
|
||||||
|
|||||||
@ -200,7 +200,6 @@ type Mux interface {
|
|||||||
ServeHTTP(w http.ResponseWriter, req *http.Request)
|
ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||||
Group(relativePath string, handlers ...HandlerFunc) RouterGroup
|
Group(relativePath string, handlers ...HandlerFunc) RouterGroup
|
||||||
Routes() gin.RoutesInfo
|
Routes() gin.RoutesInfo
|
||||||
Engine() *gin.Engine
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mux struct {
|
type mux struct {
|
||||||
@ -221,10 +220,6 @@ func (m *mux) Routes() gin.RoutesInfo {
|
|||||||
return m.engine.Routes()
|
return m.engine.Routes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mux) Engine() *gin.Engine {
|
|
||||||
return m.engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(logger logger.CustomLogger, options ...Option) (Mux, error) {
|
func New(logger logger.CustomLogger, options ...Option) (Mux, error) {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, errors.New("logger required")
|
return nil, errors.New("logger required")
|
||||||
|
|||||||
5
internal/pkg/env/env.go
vendored
5
internal/pkg/env/env.go
vendored
@ -3,7 +3,6 @@ package env
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@ -66,10 +65,6 @@ func setup() {
|
|||||||
val = *envFlag
|
val = *envFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
val = os.Getenv("ACTIVE_ENV")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.ToLower(strings.TrimSpace(val)) {
|
switch strings.ToLower(strings.TrimSpace(val)) {
|
||||||
case "dev":
|
case "dev":
|
||||||
active = dev
|
active = dev
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import (
|
|||||||
|
|
||||||
"bindbox-game/internal/pkg/httpclient"
|
"bindbox-game/internal/pkg/httpclient"
|
||||||
pkgutils "bindbox-game/internal/pkg/utils"
|
pkgutils "bindbox-game/internal/pkg/utils"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WechatNotifyConfig 微信通知配置
|
// WechatNotifyConfig 微信通知配置
|
||||||
@ -32,8 +30,13 @@ type LotteryResultNotificationRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LotteryResultNotificationData 开奖结果通知数据字段
|
// LotteryResultNotificationData 开奖结果通知数据字段
|
||||||
// 使用 map 支持动态字段类型,根据模板灵活配置
|
// 根据微信订阅消息模板字段定义
|
||||||
type LotteryResultNotificationData map[string]DataValue
|
// thing1: 活动名称, phrase3: 中奖结果, thing4: 温馨提示
|
||||||
|
type LotteryResultNotificationData struct {
|
||||||
|
Thing1 DataValue `json:"thing1"` // 活动名称
|
||||||
|
Phrase3 DataValue `json:"phrase3"` // 中奖结果
|
||||||
|
Thing4 DataValue `json:"thing4"` // 温馨提示
|
||||||
|
}
|
||||||
|
|
||||||
// DataValue 数据值包装
|
// DataValue 数据值包装
|
||||||
type DataValue struct {
|
type DataValue struct {
|
||||||
@ -105,24 +108,20 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
|
|||||||
// 获取 access_token
|
// 获取 access_token
|
||||||
accessToken, err := getAccessToken(ctx, cfg.AppID, cfg.AppSecret)
|
accessToken, err := getAccessToken(ctx, cfg.AppID, cfg.AppSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("[开奖通知] 获取access_token失败", zap.Error(err), zap.String("openid", openid))
|
fmt.Printf("[开奖通知] 获取access_token失败: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 活动名称限制长度(thing类型不超过20个字符)
|
// 活动名称限制长度(thing类型不超过20个字符)
|
||||||
activityName = pkgutils.TruncateRunes(activityName, 20)
|
activityName = pkgutils.TruncateRunes(activityName, 20)
|
||||||
|
|
||||||
// 活动结果:展示奖品列表
|
// 构建中奖结果描述(phrase类型限制5个汉字以内)
|
||||||
|
// 由于奖品名称通常较长,phrase3 放不下,改为固定文案 "恭喜中奖"
|
||||||
|
// 将奖品名称放入 Thing4 (温馨提示),限制 20 字符
|
||||||
|
resultPhrase := "恭喜中奖"
|
||||||
|
|
||||||
rewardsStr := strings.Join(rewardNames, ",")
|
rewardsStr := strings.Join(rewardNames, ",")
|
||||||
if rewardsStr == "" {
|
warmTips := pkgutils.TruncateRunes(rewardsStr, 20)
|
||||||
rewardsStr = "无奖励"
|
|
||||||
}
|
|
||||||
// thing类型限制20字符
|
|
||||||
resultVal := pkgutils.TruncateRunes(rewardsStr, 20)
|
|
||||||
|
|
||||||
// 当前进度:固定为"已发货"
|
|
||||||
progress := "已发货"
|
|
||||||
|
|
||||||
// 使用模板字段:thing6=活动名称, thing8=当前进度, thing9=活动结果
|
|
||||||
req := &LotteryResultNotificationRequest{
|
req := &LotteryResultNotificationRequest{
|
||||||
Touser: openid,
|
Touser: openid,
|
||||||
TemplateID: cfg.LotteryResultTemplateID,
|
TemplateID: cfg.LotteryResultTemplateID,
|
||||||
@ -130,13 +129,13 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
|
|||||||
MiniprogramState: "formal", // 正式版
|
MiniprogramState: "formal", // 正式版
|
||||||
Lang: "zh_CN",
|
Lang: "zh_CN",
|
||||||
Data: LotteryResultNotificationData{
|
Data: LotteryResultNotificationData{
|
||||||
"thing6": {Value: activityName}, // 活动名称
|
Thing1: DataValue{Value: activityName}, // 活动名称
|
||||||
"thing8": {Value: progress}, // 当前进度
|
Phrase3: DataValue{Value: resultPhrase}, // 中奖结果
|
||||||
"thing9": {Value: resultVal}, // 活动结果
|
Thing4: DataValue{Value: warmTips}, // 温馨提示(中奖奖品)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("[开奖通知] 尝试发送", zap.String("openid", openid), zap.String("activity", activityName), zap.Strings("rewards", rewardNames))
|
fmt.Printf("[开奖通知] 尝试发送 openid=%s activity=%s rewards=%v\n", openid, activityName, rewardNames)
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", accessToken)
|
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", accessToken)
|
||||||
@ -146,13 +145,13 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
|
|||||||
SetBody(req).
|
SetBody(req).
|
||||||
Post(url)
|
Post(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("[开奖通知] 发送失败", zap.Error(err), zap.String("openid", openid))
|
fmt.Printf("[开奖通知] 发送失败: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result LotteryResultNotificationResponse
|
var result LotteryResultNotificationResponse
|
||||||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||||||
zap.L().Error("[开奖通知] 解析响应失败", zap.Error(err), zap.String("body", string(resp.Body())))
|
fmt.Printf("[开奖通知] 解析响应失败: %v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,10 +159,10 @@ func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig,
|
|||||||
// 常见错误码:
|
// 常见错误码:
|
||||||
// 43101: 用户拒绝接受消息
|
// 43101: 用户拒绝接受消息
|
||||||
// 47003: 模板参数不准确
|
// 47003: 模板参数不准确
|
||||||
zap.L().Warn("[开奖通知] 发送失败", zap.Int("errcode", result.Errcode), zap.String("errmsg", result.Errmsg), zap.String("openid", openid))
|
fmt.Printf("[开奖通知] 发送失败 errcode=%d errmsg=%s\n", result.Errcode, result.Errmsg)
|
||||||
return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg)
|
return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("[开奖通知] ✅ 发送成功", zap.String("openid", openid))
|
fmt.Printf("[开奖通知] ✅ 发送成功 openid=%s\n", openid)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
package otel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/propagation"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Middleware 返回 Gin 中间件,用于自动创建 span
|
|
||||||
func Middleware(serviceName string) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// 从请求头提取 trace context
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
propagator := propagation.NewCompositeTextMapPropagator(
|
|
||||||
propagation.TraceContext{},
|
|
||||||
propagation.Baggage{},
|
|
||||||
)
|
|
||||||
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(c.Request.Header))
|
|
||||||
|
|
||||||
// 创建 span
|
|
||||||
spanName := c.Request.Method + " " + c.FullPath()
|
|
||||||
if c.FullPath() == "" {
|
|
||||||
spanName = c.Request.Method + " " + c.Request.URL.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, span := Tracer().Start(ctx, spanName,
|
|
||||||
trace.WithSpanKind(trace.SpanKindServer),
|
|
||||||
trace.WithAttributes(
|
|
||||||
semconv.HTTPRequestMethodKey.String(c.Request.Method),
|
|
||||||
semconv.URLFull(c.Request.URL.String()),
|
|
||||||
semconv.HTTPRoute(c.FullPath()),
|
|
||||||
semconv.ServerAddress(c.Request.Host),
|
|
||||||
attribute.String("http.client_ip", c.ClientIP()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
defer span.End()
|
|
||||||
|
|
||||||
// 将新的 context 放入请求
|
|
||||||
c.Request = c.Request.WithContext(ctx)
|
|
||||||
|
|
||||||
// 记录开始时间
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// 执行后续处理
|
|
||||||
c.Next()
|
|
||||||
|
|
||||||
// 记录响应信息
|
|
||||||
duration := time.Since(start)
|
|
||||||
statusCode := c.Writer.Status()
|
|
||||||
|
|
||||||
span.SetAttributes(
|
|
||||||
semconv.HTTPResponseStatusCode(statusCode),
|
|
||||||
attribute.Int64("http.response_size", int64(c.Writer.Size())),
|
|
||||||
attribute.Float64("http.duration_ms", float64(duration.Milliseconds())),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 如果有错误,记录错误信息
|
|
||||||
if len(c.Errors) > 0 {
|
|
||||||
span.SetAttributes(attribute.String("error.message", c.Errors.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
// Package otel 提供 OpenTelemetry 链路追踪功能
|
|
||||||
package otel
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
||||||
"go.opentelemetry.io/otel/propagation"
|
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tracer trace.Tracer
|
|
||||||
|
|
||||||
// Config OpenTelemetry 配置
|
|
||||||
type Config struct {
|
|
||||||
ServiceName string
|
|
||||||
ServiceVersion string
|
|
||||||
Environment string
|
|
||||||
Endpoint string // Tempo OTLP HTTP endpoint, e.g., "tempo:4318"
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init 初始化 OpenTelemetry
|
|
||||||
// 返回 shutdown 函数,在程序退出时调用
|
|
||||||
func Init(cfg Config) (func(context.Context) error, error) {
|
|
||||||
if !cfg.Enabled {
|
|
||||||
// 如果未启用,返回空的 shutdown 函数
|
|
||||||
tracer = otel.Tracer(cfg.ServiceName)
|
|
||||||
return func(ctx context.Context) error { return nil }, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 创建 OTLP HTTP exporter
|
|
||||||
client := otlptracehttp.NewClient(
|
|
||||||
otlptracehttp.WithEndpoint(cfg.Endpoint),
|
|
||||||
otlptracehttp.WithInsecure(), // 内网使用,不需要 TLS
|
|
||||||
)
|
|
||||||
exporter, err := otlptrace.New(ctx, client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 resource
|
|
||||||
res, err := resource.Merge(
|
|
||||||
resource.Default(),
|
|
||||||
resource.NewWithAttributes(
|
|
||||||
semconv.SchemaURL,
|
|
||||||
semconv.ServiceName(cfg.ServiceName),
|
|
||||||
semconv.ServiceVersion(cfg.ServiceVersion),
|
|
||||||
attribute.String("environment", cfg.Environment),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 TracerProvider
|
|
||||||
tp := sdktrace.NewTracerProvider(
|
|
||||||
sdktrace.WithBatcher(exporter,
|
|
||||||
sdktrace.WithBatchTimeout(5*time.Second),
|
|
||||||
),
|
|
||||||
sdktrace.WithResource(res),
|
|
||||||
sdktrace.WithSampler(sdktrace.AlwaysSample()), // 生产环境可改为采样
|
|
||||||
)
|
|
||||||
|
|
||||||
// 设置全局 TracerProvider 和 Propagator
|
|
||||||
otel.SetTracerProvider(tp)
|
|
||||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
|
||||||
propagation.TraceContext{},
|
|
||||||
propagation.Baggage{},
|
|
||||||
))
|
|
||||||
|
|
||||||
tracer = tp.Tracer(cfg.ServiceName)
|
|
||||||
|
|
||||||
return tp.Shutdown, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tracer 获取全局 Tracer
|
|
||||||
func Tracer() trace.Tracer {
|
|
||||||
if tracer == nil {
|
|
||||||
tracer = otel.Tracer("bindbox-game")
|
|
||||||
}
|
|
||||||
return tracer
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartSpan 开始一个新的 span
|
|
||||||
func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
|
||||||
return Tracer().Start(ctx, name, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpanFromContext 从 context 获取当前 span
|
|
||||||
func SpanFromContext(ctx context.Context) trace.Span {
|
|
||||||
return trace.SpanFromContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddEvent 为当前 span 添加事件
|
|
||||||
func AddEvent(ctx context.Context, name string, attrs ...attribute.KeyValue) {
|
|
||||||
span := trace.SpanFromContext(ctx)
|
|
||||||
span.AddEvent(name, trace.WithAttributes(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetError 设置 span 错误状态
|
|
||||||
func SetError(ctx context.Context, err error) {
|
|
||||||
span := trace.SpanFromContext(ctx)
|
|
||||||
span.RecordError(err)
|
|
||||||
}
|
|
||||||
@ -1,19 +1,17 @@
|
|||||||
package pay
|
package pay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"bindbox-game/configs"
|
||||||
|
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||||
refundsvc "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
refundsvc "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||||
|
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WechatPayClient struct {
|
type WechatPayClient struct {
|
||||||
@ -27,62 +25,6 @@ var (
|
|||||||
clientErr error
|
clientErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadPrivateKeyFromBase64 从 Base64 编码的私钥内容创建 RSA 私钥
|
|
||||||
func LoadPrivateKeyFromBase64(base64Key string) (*rsa.PrivateKey, error) {
|
|
||||||
// 解码 Base64
|
|
||||||
keyBytes, err := base64.StdEncoding.DecodeString(base64Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to decode base64 private key: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 PEM
|
|
||||||
block, _ := pem.Decode(keyBytes)
|
|
||||||
if block == nil {
|
|
||||||
return nil, errors.New("invalid private key PEM format")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试 PKCS8 格式
|
|
||||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
// 尝试 PKCS1 格式
|
|
||||||
rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
||||||
if err2 != nil {
|
|
||||||
return nil, errors.New("failed to parse private key: " + err.Error())
|
|
||||||
}
|
|
||||||
return rsaKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rsaKey, ok := key.(*rsa.PrivateKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("private key is not RSA type")
|
|
||||||
}
|
|
||||||
return rsaKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPublicKeyFromBase64 从 Base64 编码的公钥内容创建 RSA 公钥
|
|
||||||
func LoadPublicKeyFromBase64(base64Key string) (*rsa.PublicKey, error) {
|
|
||||||
keyBytes, err := base64.StdEncoding.DecodeString(base64Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to decode base64 public key: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
block, _ := pem.Decode(keyBytes)
|
|
||||||
if block == nil {
|
|
||||||
return nil, errors.New("invalid public key PEM format")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to parse public key: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
rsaPub, ok := pub.(*rsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("public key is not RSA type")
|
|
||||||
}
|
|
||||||
return rsaPub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWechatPayClient 获取微信支付客户端(单例模式)
|
// NewWechatPayClient 获取微信支付客户端(单例模式)
|
||||||
// 首次调用会初始化客户端,后续调用直接返回缓存的实例
|
// 首次调用会初始化客户端,后续调用直接返回缓存的实例
|
||||||
func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
|
func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
|
||||||
@ -96,66 +38,35 @@ func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initWechatPayClient 初始化微信支付客户端(内部实现)
|
// initWechatPayClient 初始化微信支付客户端(内部实现)
|
||||||
// 优先使用动态配置中的 Base64 私钥内容,fallback 到静态配置的文件路径
|
|
||||||
func initWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
|
func initWechatPayClient(ctx context.Context) (*WechatPayClient, error) {
|
||||||
// 必须从动态配置获取
|
cfg := configs.Get()
|
||||||
var dynamicCfg *sysconfig.WechatPayConfig
|
if cfg.WechatPay.ApiV3Key == "" {
|
||||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
return nil, errors.New("wechat pay config incomplete")
|
||||||
cfg := dc.GetWechatPay(ctx)
|
|
||||||
dynamicCfg = &cfg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dynamicCfg == nil {
|
|
||||||
return nil, errors.New("wechat pay dynamic config missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
mchID := dynamicCfg.MchID
|
|
||||||
serialNo := dynamicCfg.SerialNo
|
|
||||||
apiV3Key := dynamicCfg.ApiV3Key
|
|
||||||
|
|
||||||
if apiV3Key == "" {
|
|
||||||
return nil, errors.New("wechat pay config incomplete: api_v3_key missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
if mchID == "" || serialNo == "" {
|
|
||||||
return nil, errors.New("wechat pay config incomplete: mchid or serial_no missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载私钥:动态配置 Base64 内容
|
|
||||||
var mchPrivateKey *rsa.PrivateKey
|
|
||||||
var err error
|
|
||||||
if dynamicCfg.PrivateKey != "" {
|
|
||||||
mchPrivateKey, err = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("read private key from dynamic config err:" + err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("wechat pay private key not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建客户端选项
|
|
||||||
var opts []core.ClientOption
|
var opts []core.ClientOption
|
||||||
|
if cfg.WechatPay.PublicKeyID != "" && cfg.WechatPay.PublicKeyPath != "" {
|
||||||
// 检查是否有公钥配置(新版验签方式)
|
if cfg.WechatPay.MchID == "" || cfg.WechatPay.SerialNo == "" || cfg.WechatPay.PrivateKeyPath == "" {
|
||||||
publicKeyID := dynamicCfg.PublicKeyID
|
return nil, errors.New("wechat pay config incomplete")
|
||||||
|
}
|
||||||
if publicKeyID != "" {
|
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(cfg.WechatPay.PrivateKeyPath)
|
||||||
// 使用公钥验签模式
|
|
||||||
var pubKey *rsa.PublicKey
|
|
||||||
if dynamicCfg.PublicKey != "" {
|
|
||||||
pubKey, err = LoadPublicKeyFromBase64(dynamicCfg.PublicKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("read public key from dynamic config err:" + err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
pubKey, err := utils.LoadPublicKeyWithPath(cfg.WechatPay.PublicKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts = []core.ClientOption{option.WithWechatPayPublicKeyAuthCipher(cfg.WechatPay.MchID, cfg.WechatPay.SerialNo, mchPrivateKey, cfg.WechatPay.PublicKeyID, pubKey)}
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("wechat pay public key not configured")
|
if cfg.WechatPay.MchID == "" || cfg.WechatPay.SerialNo == "" || cfg.WechatPay.PrivateKeyPath == "" {
|
||||||
|
return nil, errors.New("wechat pay config incomplete")
|
||||||
}
|
}
|
||||||
opts = []core.ClientOption{option.WithWechatPayPublicKeyAuthCipher(mchID, serialNo, mchPrivateKey, publicKeyID, pubKey)}
|
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(cfg.WechatPay.PrivateKeyPath)
|
||||||
} else {
|
if err != nil {
|
||||||
// 使用自动证书模式
|
return nil, err
|
||||||
opts = []core.ClientOption{option.WithWechatPayAutoAuthCipher(mchID, serialNo, mchPrivateKey, apiV3Key)}
|
}
|
||||||
|
opts = []core.ClientOption{option.WithWechatPayAutoAuthCipher(cfg.WechatPay.MchID, cfg.WechatPay.SerialNo, mchPrivateKey, cfg.WechatPay.ApiV3Key)}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := core.NewClient(ctx, opts...)
|
client, err := core.NewClient(ctx, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package pay
|
package pay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto"
|
"crypto"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@ -17,54 +16,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bindbox-game/configs"
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/service/sysconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 私钥缓存 - 避免每次请求都重新加载
|
// 私钥缓存 - 避免每次请求都从磁盘读取
|
||||||
var (
|
var (
|
||||||
cachedRSAKey *rsa.PrivateKey
|
cachedRSAKey *rsa.PrivateKey
|
||||||
rsaKeyOnce sync.Once
|
rsaKeyOnce sync.Once
|
||||||
rsaKeyLoadErr error
|
rsaKeyLoadErr error
|
||||||
rsaKeyLoadFrom string // "dynamic" 或 "file"
|
rsaKeyConfigPath string // 记录加载时的路径,用于检测配置变更
|
||||||
)
|
)
|
||||||
|
|
||||||
// getCachedRSAKeyForSign 获取缓存的RSA私钥用于签名
|
// loadRSAPrivateKey 从磁盘加载私钥(内部函数,仅在首次调用时执行)
|
||||||
// 优先使用动态配置中的 Base64 私钥内容,fallback 到静态文件路径
|
func loadRSAPrivateKey(keyPath string) (*rsa.PrivateKey, error) {
|
||||||
func getCachedRSAKeyForSign(ctx context.Context) (*rsa.PrivateKey, error) {
|
|
||||||
rsaKeyOnce.Do(func() {
|
|
||||||
staticCfg := configs.Get()
|
|
||||||
|
|
||||||
// 尝试从动态配置获取
|
|
||||||
var dynamicCfg *sysconfig.WechatPayConfig
|
|
||||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
|
||||||
cfg := dc.GetWechatPay(ctx)
|
|
||||||
dynamicCfg = &cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先动态配置的 Base64 内容
|
|
||||||
if dynamicCfg != nil && dynamicCfg.PrivateKey != "" {
|
|
||||||
cachedRSAKey, rsaKeyLoadErr = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey)
|
|
||||||
if rsaKeyLoadErr == nil {
|
|
||||||
rsaKeyLoadFrom = "dynamic"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback 到静态文件路径
|
|
||||||
if staticCfg.WechatPay.PrivateKeyPath != "" {
|
|
||||||
cachedRSAKey, rsaKeyLoadErr = loadRSAPrivateKeyFromFile(staticCfg.WechatPay.PrivateKeyPath)
|
|
||||||
if rsaKeyLoadErr == nil {
|
|
||||||
rsaKeyLoadFrom = "file"
|
|
||||||
}
|
|
||||||
} else if rsaKeyLoadErr == nil {
|
|
||||||
rsaKeyLoadErr = errors.New("wechat pay private key not configured")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return cachedRSAKey, rsaKeyLoadErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadRSAPrivateKeyFromFile 从磁盘加载私钥(内部函数)
|
|
||||||
func loadRSAPrivateKeyFromFile(keyPath string) (*rsa.PrivateKey, error) {
|
|
||||||
b, err := os.ReadFile(keyPath)
|
b, err := os.ReadFile(keyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -89,13 +52,31 @@ func loadRSAPrivateKeyFromFile(keyPath string) (*rsa.PrivateKey, error) {
|
|||||||
return rsaKey, nil
|
return rsaKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCachedRSAKey 获取缓存的RSA私钥
|
||||||
|
func getCachedRSAKey(keyPath string) (*rsa.PrivateKey, error) {
|
||||||
|
rsaKeyOnce.Do(func() {
|
||||||
|
rsaKeyConfigPath = keyPath
|
||||||
|
cachedRSAKey, rsaKeyLoadErr = loadRSAPrivateKey(keyPath)
|
||||||
|
})
|
||||||
|
// 如果配置路径变更(理论上不应该发生),返回错误以提示重启
|
||||||
|
if rsaKeyConfigPath != keyPath {
|
||||||
|
return nil, errors.New("private key path changed, please restart the server")
|
||||||
|
}
|
||||||
|
return cachedRSAKey, rsaKeyLoadErr
|
||||||
|
}
|
||||||
|
|
||||||
// BuildJSAPIParams 为小程序支付构造客户端参数
|
// BuildJSAPIParams 为小程序支付构造客户端参数
|
||||||
// 入参:ctx(上下文)、appid(微信小程序AppID)、prepayID(统一下单返回的prepay_id)
|
// 入参:appid(微信小程序AppID)、prepayID(统一下单返回的prepay_id)
|
||||||
// 返回:timeStamp、nonceStr、package(格式为"prepay_id=***" )、signType(固定"RSA")、paySign(RSA-SHA256签名)
|
// 返回:timeStamp、nonceStr、package(格式为"prepay_id=***" )、signType(固定"RSA")、paySign(RSA-SHA256签名)
|
||||||
// 错误:当私钥读取或签名失败时返回错误
|
// 错误:当私钥读取或签名失败时返回错误
|
||||||
func BuildJSAPIParams(ctx context.Context, appid string, prepayID string) (timeStamp string, nonceStr string, pkg string, signType string, paySign string, err error) {
|
func BuildJSAPIParams(appid string, prepayID string) (timeStamp string, nonceStr string, pkg string, signType string, paySign string, err error) {
|
||||||
// 使用缓存的私钥,优先动态配置
|
cfg := configs.Get()
|
||||||
rsaKey, err := getCachedRSAKeyForSign(ctx)
|
if cfg.WechatPay.PrivateKeyPath == "" {
|
||||||
|
return "", "", "", "", "", errors.New("wechat pay private key path not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用缓存的私钥,避免每次都从磁盘读取
|
||||||
|
rsaKey, err := getCachedRSAKey(cfg.WechatPay.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", "", "", err
|
return "", "", "", "", "", err
|
||||||
}
|
}
|
||||||
@ -122,34 +103,14 @@ func BuildJSAPIParams(ctx context.Context, appid string, prepayID string) (timeS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 校验微信支付必要配置
|
// ValidateConfig 校验微信支付必要配置
|
||||||
// 入参:ctx(上下文)
|
// 入参:无
|
||||||
// 返回:true表示配置齐全;false表示缺失,并附带错误信息
|
// 返回:true表示配置齐全;false表示缺失,并附带错误信息
|
||||||
func ValidateConfig(ctx context.Context) (bool, error) {
|
func ValidateConfig() (bool, error) {
|
||||||
// 检查动态配置
|
c := configs.Get()
|
||||||
var dynamicCfg *sysconfig.WechatPayConfig
|
if c.Wechat.AppID == "" {
|
||||||
var wxCfg *sysconfig.WechatConfig
|
|
||||||
|
|
||||||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
|
||||||
pCfg := dc.GetWechatPay(ctx)
|
|
||||||
dynamicCfg = &pCfg
|
|
||||||
wCfg := dc.GetWechat(ctx)
|
|
||||||
wxCfg = &wCfg
|
|
||||||
}
|
|
||||||
|
|
||||||
if wxCfg == nil || wxCfg.AppID == "" {
|
|
||||||
return false, errors.New("wechat app_id missing")
|
return false, errors.New("wechat app_id missing")
|
||||||
}
|
}
|
||||||
|
if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" || c.WechatPay.ApiV3Key == "" {
|
||||||
if dynamicCfg == nil {
|
|
||||||
return false, errors.New("wechat pay config incomplete")
|
|
||||||
}
|
|
||||||
|
|
||||||
mchID := dynamicCfg.MchID
|
|
||||||
serialNo := dynamicCfg.SerialNo
|
|
||||||
apiV3Key := dynamicCfg.ApiV3Key
|
|
||||||
hasPrivateKey := dynamicCfg.PrivateKey != ""
|
|
||||||
|
|
||||||
if mchID == "" || serialNo == "" || !hasPrivateKey || apiV3Key == "" {
|
|
||||||
return false, errors.New("wechat pay config incomplete")
|
return false, errors.New("wechat pay config incomplete")
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|||||||
@ -1,25 +1,19 @@
|
|||||||
package points
|
package points
|
||||||
|
|
||||||
import "math"
|
func CentsToPoints(cents int64, rate int64) int64 {
|
||||||
|
if cents <= 0 || rate <= 0 { return 0 }
|
||||||
// CentsToPoints converts monetary value (in cents) to points based on the exchange rate (X Points per 1 Yuan).
|
return cents * rate
|
||||||
// Now: 1 Yuan = 100 Cents.
|
|
||||||
// If Rate = 1 (1 Point = 1 Yuan), then 100 Cents = 1 Point.
|
|
||||||
// Formula: points = (cents * rate) / 100
|
|
||||||
func CentsToPoints(cents int64, rate float64) int64 {
|
|
||||||
if rate <= 0 {
|
|
||||||
rate = 1
|
|
||||||
}
|
|
||||||
// Use rounding to avoid precision loss on division
|
|
||||||
return int64(math.Round((float64(cents) * rate) / 100.0))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PointsToCents converts points to monetary value (in cents).
|
func PointsToCents(points int64, rate int64) int64 {
|
||||||
// If Rate = 1 (1 Point = 1 Yuan), then 1 Point = 100 Cents.
|
if points <= 0 || rate <= 0 { return 0 }
|
||||||
// Formula: cents = (points * 100) / rate
|
return points / rate
|
||||||
func PointsToCents(points int64, rate float64) int64 {
|
|
||||||
if rate <= 0 {
|
|
||||||
rate = 1
|
|
||||||
}
|
}
|
||||||
return int64(math.Round((float64(points) * 100.0) / rate))
|
|
||||||
|
func RefundPointsAmount(pointsAmountCents int64, refundedCents int64, totalPaidCents int64, rate int64) int64 {
|
||||||
|
if pointsAmountCents <= 0 || refundedCents <= 0 || totalPaidCents <= 0 || rate <= 0 { return 0 }
|
||||||
|
if refundedCents > totalPaidCents { refundedCents = totalPaidCents }
|
||||||
|
targetCents := (pointsAmountCents * refundedCents) / totalPaidCents
|
||||||
|
return targetCents / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,22 +3,20 @@ package points
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestCentsToPoints_DefaultRate(t *testing.T) {
|
func TestCentsToPoints_DefaultRate(t *testing.T) {
|
||||||
if got := CentsToPoints(100, 1); got != 1 {
|
if got := CentsToPoints(12345, 1); got != 12345 {
|
||||||
t.Fatalf("expected 1, got %d", got)
|
t.Fatalf("expected 12345, got %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPointsToCents_DefaultRate(t *testing.T) {
|
func TestPointsToCents_DefaultRate(t *testing.T) {
|
||||||
if got := PointsToCents(1, 1); got != 100 {
|
if got := PointsToCents(100, 1); got != 100 {
|
||||||
t.Fatalf("expected 100, got %d", got)
|
t.Fatalf("expected 100, got %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefundPointsAmount(t *testing.T) {
|
func TestRefundPointsAmount(t *testing.T) {
|
||||||
// 100 Points used. Refund 25 Yuan out of 100 Yuan paid.
|
pts := RefundPointsAmount(5000, 2500, 10000, 1)
|
||||||
// Expect 25 Points back.
|
if pts != 12 {
|
||||||
pts := RefundPointsAmount(100, 2500, 10000, 1)
|
t.Fatalf("expected 12, got %d", pts)
|
||||||
if pts != 25 {
|
|
||||||
t.Fatalf("expected 25, got %d", pts)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ type Code2SessionResponse struct {
|
|||||||
|
|
||||||
func Code2Session(ctx context.Context, config *WechatConfig, code string) (*Code2SessionResponse, error) {
|
func Code2Session(ctx context.Context, config *WechatConfig, code string) (*Code2SessionResponse, error) {
|
||||||
if config == nil || config.AppID == "" || config.AppSecret == "" {
|
if config == nil || config.AppID == "" || config.AppSecret == "" {
|
||||||
fmt.Printf("DEBUG: Code2Session Config Missing: %+v\n", config)
|
|
||||||
return nil, fmt.Errorf("微信配置缺失")
|
return nil, fmt.Errorf("微信配置缺失")
|
||||||
}
|
}
|
||||||
if code == "" {
|
if code == "" {
|
||||||
|
|||||||
@ -65,18 +65,6 @@ func uploadVirtualShippingInternal(ctx core.Context, accessToken string, key ord
|
|||||||
if itemDesc == "" {
|
if itemDesc == "" {
|
||||||
return fmt.Errorf("参数缺失")
|
return fmt.Errorf("参数缺失")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Check if already shipped to avoid invalid request
|
|
||||||
state, err := GetOrderShippingStatus(context.Background(), accessToken, key)
|
|
||||||
if err == nil {
|
|
||||||
if state >= 2 && state <= 4 {
|
|
||||||
fmt.Printf("[虚拟发货] 订单已发货/完成(state=%d),跳过上传 order_key=%+v\n", state, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("[虚拟发货] 查询订单状态失败: %v, 继续尝试发货\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody := &uploadShippingInfoRequest{
|
reqBody := &uploadShippingInfoRequest{
|
||||||
OrderKey: key,
|
OrderKey: key,
|
||||||
LogisticsType: 3,
|
LogisticsType: 3,
|
||||||
@ -253,56 +241,6 @@ func UploadVirtualShippingForBackground(ctx context.Context, config *WechatConfi
|
|||||||
return uploadVirtualShippingInternalBackground(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, time.Now())
|
return uploadVirtualShippingInternalBackground(ctx, accessToken, orderKey{OrderNumberType: 1, MchID: mchID, OutTradeNo: outTradeNo}, payerOpenid, itemDesc, time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrderShippingStatusResponse 查询订单发货状态响应
|
|
||||||
type GetOrderShippingStatusResponse struct {
|
|
||||||
ErrCode int `json:"errcode"`
|
|
||||||
ErrMsg string `json:"errmsg"`
|
|
||||||
Order struct {
|
|
||||||
OrderState int `json:"order_state"` // 1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款
|
|
||||||
} `json:"order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOrderShippingStatus 查询订单发货状态
|
|
||||||
// 返回: orderState (1: 待发货, 2: 已发货, 3: 确认收货, 4: 交易完成, 5: 已退款), error
|
|
||||||
func GetOrderShippingStatus(ctx context.Context, accessToken string, key orderKey) (int, error) {
|
|
||||||
if accessToken == "" {
|
|
||||||
return 0, fmt.Errorf("access_token 不能为空")
|
|
||||||
}
|
|
||||||
// 文档: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html#三、查询订单发货状态
|
|
||||||
// get_order 接口参数是扁平的,不使用 order_key 结构
|
|
||||||
reqBody := map[string]any{}
|
|
||||||
if key.TransactionID != "" {
|
|
||||||
reqBody["transaction_id"] = key.TransactionID
|
|
||||||
} else {
|
|
||||||
reqBody["merchant_id"] = key.MchID
|
|
||||||
reqBody["merchant_trade_no"] = key.OutTradeNo
|
|
||||||
}
|
|
||||||
b, _ := json.Marshal(reqBody)
|
|
||||||
|
|
||||||
// fmt.Printf("[虚拟发货-查询] 请求 get_order order_key=%+v\n", key) // Debug log
|
|
||||||
client := httpclient.GetHttpClient()
|
|
||||||
resp, err := client.R().
|
|
||||||
SetQueryParam("access_token", accessToken).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(b).
|
|
||||||
Post("https://api.weixin.qq.com/wxa/sec/order/get_order")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
var r GetOrderShippingStatusResponse
|
|
||||||
if err := json.Unmarshal(resp.Body(), &r); err != nil {
|
|
||||||
return 0, fmt.Errorf("解析响应失败: %v", err)
|
|
||||||
}
|
|
||||||
if r.ErrCode != 0 {
|
|
||||||
// 10060001 = 支付单不存在,视为待发货(或未知的)
|
|
||||||
if r.ErrCode == 10060001 {
|
|
||||||
return 0, nil // Not found
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", r.ErrCode, r.ErrMsg)
|
|
||||||
}
|
|
||||||
return r.Order.OrderState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// uploadVirtualShippingInternalBackground 后台虚拟发货内部实现(无 core.Context)
|
// uploadVirtualShippingInternalBackground 后台虚拟发货内部实现(无 core.Context)
|
||||||
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
|
func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken string, key orderKey, payerOpenid string, itemDesc string, uploadTime time.Time) error {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
@ -311,22 +249,6 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
|||||||
if itemDesc == "" {
|
if itemDesc == "" {
|
||||||
return fmt.Errorf("参数缺失")
|
return fmt.Errorf("参数缺失")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Check if already shipped to avoid invalid request
|
|
||||||
state, err := GetOrderShippingStatus(ctx, accessToken, key)
|
|
||||||
if err == nil {
|
|
||||||
if state >= 2 && state <= 4 {
|
|
||||||
fmt.Printf("[虚拟发货-后台] 订单已发货/完成(state=%d),跳过上传 order_key=%+v\n", state, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Log error but continue to try upload? Or just return error?
|
|
||||||
// If query fails, maybe we should try upload anyway or just log warning.
|
|
||||||
// Let's log warning and continue.
|
|
||||||
fmt.Printf("[虚拟发货-后台] 查询订单状态失败: %v, 继续尝试发货\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Upload shipping info
|
|
||||||
reqBody := &uploadShippingInfoRequest{
|
reqBody := &uploadShippingInfoRequest{
|
||||||
OrderKey: key,
|
OrderKey: key,
|
||||||
LogisticsType: 3,
|
LogisticsType: 3,
|
||||||
@ -353,11 +275,6 @@ func uploadVirtualShippingInternalBackground(ctx context.Context, accessToken st
|
|||||||
return fmt.Errorf("解析响应失败: %v", err)
|
return fmt.Errorf("解析响应失败: %v", err)
|
||||||
}
|
}
|
||||||
if cr.ErrCode != 0 {
|
if cr.ErrCode != 0 {
|
||||||
// 10060003 = 订单已发货 (Redundant check if status check above passed but state changed or query returned 0)
|
|
||||||
if cr.ErrCode == 10060003 {
|
|
||||||
fmt.Printf("[虚拟发货-后台] 微信返回已发货(10060003),视为成功\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
|
return fmt.Errorf("微信返回错误: errcode=%d, errmsg=%s", cr.ErrCode, cr.ErrMsg)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -1,344 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
|
|
||||||
"gorm.io/plugin/dbresolver"
|
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newDouyinBlacklist(db *gorm.DB, opts ...gen.DOOption) douyinBlacklist {
|
|
||||||
_douyinBlacklist := douyinBlacklist{}
|
|
||||||
|
|
||||||
_douyinBlacklist.douyinBlacklistDo.UseDB(db, opts...)
|
|
||||||
_douyinBlacklist.douyinBlacklistDo.UseModel(&model.DouyinBlacklist{})
|
|
||||||
|
|
||||||
tableName := _douyinBlacklist.douyinBlacklistDo.TableName()
|
|
||||||
_douyinBlacklist.ALL = field.NewAsterisk(tableName)
|
|
||||||
_douyinBlacklist.ID = field.NewInt64(tableName, "id")
|
|
||||||
_douyinBlacklist.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
|
||||||
_douyinBlacklist.Reason = field.NewString(tableName, "reason")
|
|
||||||
_douyinBlacklist.OperatorID = field.NewInt64(tableName, "operator_id")
|
|
||||||
_douyinBlacklist.Status = field.NewInt32(tableName, "status")
|
|
||||||
_douyinBlacklist.CreatedAt = field.NewTime(tableName, "created_at")
|
|
||||||
_douyinBlacklist.UpdatedAt = field.NewTime(tableName, "updated_at")
|
|
||||||
|
|
||||||
_douyinBlacklist.fillFieldMap()
|
|
||||||
|
|
||||||
return _douyinBlacklist
|
|
||||||
}
|
|
||||||
|
|
||||||
// douyinBlacklist 抖音用户黑名单表
|
|
||||||
type douyinBlacklist struct {
|
|
||||||
douyinBlacklistDo
|
|
||||||
|
|
||||||
ALL field.Asterisk
|
|
||||||
ID field.Int64 // 主键ID
|
|
||||||
DouyinUserID field.String // 抖音用户ID
|
|
||||||
Reason field.String // 拉黑原因
|
|
||||||
OperatorID field.Int64 // 操作人ID
|
|
||||||
Status field.Int32 // 状态: 1=生效, 0=已解除
|
|
||||||
CreatedAt field.Time // 创建时间
|
|
||||||
UpdatedAt field.Time // 更新时间
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklist) Table(newTableName string) *douyinBlacklist {
|
|
||||||
d.douyinBlacklistDo.UseTable(newTableName)
|
|
||||||
return d.updateTableName(newTableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklist) As(alias string) *douyinBlacklist {
|
|
||||||
d.douyinBlacklistDo.DO = *(d.douyinBlacklistDo.As(alias).(*gen.DO))
|
|
||||||
return d.updateTableName(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinBlacklist) updateTableName(table string) *douyinBlacklist {
|
|
||||||
d.ALL = field.NewAsterisk(table)
|
|
||||||
d.ID = field.NewInt64(table, "id")
|
|
||||||
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
|
||||||
d.Reason = field.NewString(table, "reason")
|
|
||||||
d.OperatorID = field.NewInt64(table, "operator_id")
|
|
||||||
d.Status = field.NewInt32(table, "status")
|
|
||||||
d.CreatedAt = field.NewTime(table, "created_at")
|
|
||||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
|
||||||
|
|
||||||
d.fillFieldMap()
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinBlacklist) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|
||||||
_f, ok := d.fieldMap[fieldName]
|
|
||||||
if !ok || _f == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
_oe, ok := _f.(field.OrderExpr)
|
|
||||||
return _oe, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinBlacklist) fillFieldMap() {
|
|
||||||
d.fieldMap = make(map[string]field.Expr, 7)
|
|
||||||
d.fieldMap["id"] = d.ID
|
|
||||||
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
|
||||||
d.fieldMap["reason"] = d.Reason
|
|
||||||
d.fieldMap["operator_id"] = d.OperatorID
|
|
||||||
d.fieldMap["status"] = d.Status
|
|
||||||
d.fieldMap["created_at"] = d.CreatedAt
|
|
||||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklist) clone(db *gorm.DB) douyinBlacklist {
|
|
||||||
d.douyinBlacklistDo.ReplaceConnPool(db.Statement.ConnPool)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklist) replaceDB(db *gorm.DB) douyinBlacklist {
|
|
||||||
d.douyinBlacklistDo.ReplaceDB(db)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
type douyinBlacklistDo struct{ gen.DO }
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Debug() *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Debug())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) WithContext(ctx context.Context) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.WithContext(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) ReadDB() *douyinBlacklistDo {
|
|
||||||
return d.Clauses(dbresolver.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) WriteDB() *douyinBlacklistDo {
|
|
||||||
return d.Clauses(dbresolver.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Session(config *gorm.Session) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Session(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Clauses(conds ...clause.Expression) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Clauses(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Returning(value interface{}, columns ...string) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Returning(value, columns...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Not(conds ...gen.Condition) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Not(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Or(conds ...gen.Condition) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Or(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Select(conds ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Select(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Where(conds ...gen.Condition) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Where(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Order(conds ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Order(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Distinct(cols ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Distinct(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Omit(cols ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Omit(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Join(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Join(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) LeftJoin(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.LeftJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) RightJoin(table schema.Tabler, on ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.RightJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Group(cols ...field.Expr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Group(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Having(conds ...gen.Condition) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Having(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Limit(limit int) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Limit(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Offset(offset int) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Offset(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Scopes(funcs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Unscoped() *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Unscoped())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Create(values ...*model.DouyinBlacklist) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.DO.Create(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) CreateInBatches(values []*model.DouyinBlacklist, batchSize int) error {
|
|
||||||
return d.DO.CreateInBatches(values, batchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save : !!! underlying implementation is different with GORM
|
|
||||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
|
||||||
func (d douyinBlacklistDo) Save(values ...*model.DouyinBlacklist) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.DO.Save(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) First() (*model.DouyinBlacklist, error) {
|
|
||||||
if result, err := d.DO.First(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinBlacklist), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Take() (*model.DouyinBlacklist, error) {
|
|
||||||
if result, err := d.DO.Take(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinBlacklist), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Last() (*model.DouyinBlacklist, error) {
|
|
||||||
if result, err := d.DO.Last(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinBlacklist), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Find() ([]*model.DouyinBlacklist, error) {
|
|
||||||
result, err := d.DO.Find()
|
|
||||||
return result.([]*model.DouyinBlacklist), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.DouyinBlacklist, err error) {
|
|
||||||
buf := make([]*model.DouyinBlacklist, 0, batchSize)
|
|
||||||
err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
|
||||||
defer func() { results = append(results, buf...) }()
|
|
||||||
return fc(tx, batch)
|
|
||||||
})
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) FindInBatches(result *[]*model.DouyinBlacklist, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
|
||||||
return d.DO.FindInBatches(result, batchSize, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Attrs(attrs ...field.AssignExpr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Attrs(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Assign(attrs ...field.AssignExpr) *douyinBlacklistDo {
|
|
||||||
return d.withDO(d.DO.Assign(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Joins(fields ...field.RelationField) *douyinBlacklistDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
d = *d.withDO(d.DO.Joins(_f))
|
|
||||||
}
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Preload(fields ...field.RelationField) *douyinBlacklistDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
d = *d.withDO(d.DO.Preload(_f))
|
|
||||||
}
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) FirstOrInit() (*model.DouyinBlacklist, error) {
|
|
||||||
if result, err := d.DO.FirstOrInit(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinBlacklist), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) FirstOrCreate() (*model.DouyinBlacklist, error) {
|
|
||||||
if result, err := d.DO.FirstOrCreate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinBlacklist), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) FindByPage(offset int, limit int) (result []*model.DouyinBlacklist, count int64, err error) {
|
|
||||||
result, err = d.Offset(offset).Limit(limit).Find()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
|
||||||
count = int64(size + offset)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err = d.Offset(-1).Limit(-1).Count()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
|
||||||
count, err = d.Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Offset(offset).Limit(limit).Scan(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Scan(result interface{}) (err error) {
|
|
||||||
return d.DO.Scan(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinBlacklistDo) Delete(models ...*model.DouyinBlacklist) (result gen.ResultInfo, err error) {
|
|
||||||
return d.DO.Delete(models)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinBlacklistDo) withDO(do gen.Dao) *douyinBlacklistDo {
|
|
||||||
d.DO = *do.(*gen.DO)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
@ -29,20 +29,16 @@ func newDouyinOrders(db *gorm.DB, opts ...gen.DOOption) douyinOrders {
|
|||||||
_douyinOrders.ALL = field.NewAsterisk(tableName)
|
_douyinOrders.ALL = field.NewAsterisk(tableName)
|
||||||
_douyinOrders.ID = field.NewInt64(tableName, "id")
|
_douyinOrders.ID = field.NewInt64(tableName, "id")
|
||||||
_douyinOrders.ShopOrderID = field.NewString(tableName, "shop_order_id")
|
_douyinOrders.ShopOrderID = field.NewString(tableName, "shop_order_id")
|
||||||
_douyinOrders.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
|
||||||
_douyinOrders.OrderStatus = field.NewInt32(tableName, "order_status")
|
_douyinOrders.OrderStatus = field.NewInt32(tableName, "order_status")
|
||||||
_douyinOrders.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
_douyinOrders.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
||||||
_douyinOrders.LocalUserID = field.NewString(tableName, "local_user_id")
|
_douyinOrders.LocalUserID = field.NewString(tableName, "local_user_id")
|
||||||
_douyinOrders.ActualReceiveAmount = field.NewInt64(tableName, "actual_receive_amount")
|
_douyinOrders.ActualReceiveAmount = field.NewInt64(tableName, "actual_receive_amount")
|
||||||
_douyinOrders.ActualPayAmount = field.NewInt64(tableName, "actual_pay_amount")
|
|
||||||
_douyinOrders.PayTypeDesc = field.NewString(tableName, "pay_type_desc")
|
_douyinOrders.PayTypeDesc = field.NewString(tableName, "pay_type_desc")
|
||||||
_douyinOrders.Remark = field.NewString(tableName, "remark")
|
_douyinOrders.Remark = field.NewString(tableName, "remark")
|
||||||
_douyinOrders.UserNickname = field.NewString(tableName, "user_nickname")
|
_douyinOrders.UserNickname = field.NewString(tableName, "user_nickname")
|
||||||
_douyinOrders.RawData = field.NewString(tableName, "raw_data")
|
_douyinOrders.RawData = field.NewString(tableName, "raw_data")
|
||||||
_douyinOrders.CreatedAt = field.NewTime(tableName, "created_at")
|
_douyinOrders.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
_douyinOrders.UpdatedAt = field.NewTime(tableName, "updated_at")
|
_douyinOrders.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||||
_douyinOrders.RewardGranted = field.NewBool(tableName, "reward_granted")
|
|
||||||
_douyinOrders.ProductCount = field.NewInt32(tableName, "product_count")
|
|
||||||
|
|
||||||
_douyinOrders.fillFieldMap()
|
_douyinOrders.fillFieldMap()
|
||||||
|
|
||||||
@ -56,20 +52,16 @@ type douyinOrders struct {
|
|||||||
ALL field.Asterisk
|
ALL field.Asterisk
|
||||||
ID field.Int64
|
ID field.Int64
|
||||||
ShopOrderID field.String // 抖店订单号
|
ShopOrderID field.String // 抖店订单号
|
||||||
DouyinProductID field.String // 关联商品ID
|
|
||||||
OrderStatus field.Int32 // 订单状态: 5=已完成
|
OrderStatus field.Int32 // 订单状态: 5=已完成
|
||||||
DouyinUserID field.String // 抖店用户ID
|
DouyinUserID field.String // 抖店用户ID
|
||||||
LocalUserID field.String // 匹配到的本地用户ID
|
LocalUserID field.String // 匹配到的本地用户ID
|
||||||
ActualReceiveAmount field.Int64 // 实收金额(分)
|
ActualReceiveAmount field.Int64 // 实收金额(分)
|
||||||
ActualPayAmount field.Int64 // 实付金额(分)
|
|
||||||
PayTypeDesc field.String // 支付方式描述
|
PayTypeDesc field.String // 支付方式描述
|
||||||
Remark field.String // 备注
|
Remark field.String // 备注
|
||||||
UserNickname field.String // 抖音昵称
|
UserNickname field.String // 抖音昵称
|
||||||
RawData field.String // 原始响应数据
|
RawData field.String // 原始响应数据
|
||||||
CreatedAt field.Time
|
CreatedAt field.Time
|
||||||
UpdatedAt field.Time
|
UpdatedAt field.Time
|
||||||
RewardGranted field.Bool // 奖励已发放: 0=否, 1=是
|
|
||||||
ProductCount field.Int32 // 商品数量
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -88,20 +80,16 @@ func (d *douyinOrders) updateTableName(table string) *douyinOrders {
|
|||||||
d.ALL = field.NewAsterisk(table)
|
d.ALL = field.NewAsterisk(table)
|
||||||
d.ID = field.NewInt64(table, "id")
|
d.ID = field.NewInt64(table, "id")
|
||||||
d.ShopOrderID = field.NewString(table, "shop_order_id")
|
d.ShopOrderID = field.NewString(table, "shop_order_id")
|
||||||
d.DouyinProductID = field.NewString(table, "douyin_product_id")
|
|
||||||
d.OrderStatus = field.NewInt32(table, "order_status")
|
d.OrderStatus = field.NewInt32(table, "order_status")
|
||||||
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
d.DouyinUserID = field.NewString(table, "douyin_user_id")
|
||||||
d.LocalUserID = field.NewString(table, "local_user_id")
|
d.LocalUserID = field.NewString(table, "local_user_id")
|
||||||
d.ActualReceiveAmount = field.NewInt64(table, "actual_receive_amount")
|
d.ActualReceiveAmount = field.NewInt64(table, "actual_receive_amount")
|
||||||
d.ActualPayAmount = field.NewInt64(table, "actual_pay_amount")
|
|
||||||
d.PayTypeDesc = field.NewString(table, "pay_type_desc")
|
d.PayTypeDesc = field.NewString(table, "pay_type_desc")
|
||||||
d.Remark = field.NewString(table, "remark")
|
d.Remark = field.NewString(table, "remark")
|
||||||
d.UserNickname = field.NewString(table, "user_nickname")
|
d.UserNickname = field.NewString(table, "user_nickname")
|
||||||
d.RawData = field.NewString(table, "raw_data")
|
d.RawData = field.NewString(table, "raw_data")
|
||||||
d.CreatedAt = field.NewTime(table, "created_at")
|
d.CreatedAt = field.NewTime(table, "created_at")
|
||||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
d.UpdatedAt = field.NewTime(table, "updated_at")
|
||||||
d.RewardGranted = field.NewBool(table, "reward_granted")
|
|
||||||
d.ProductCount = field.NewInt32(table, "product_count")
|
|
||||||
|
|
||||||
d.fillFieldMap()
|
d.fillFieldMap()
|
||||||
|
|
||||||
@ -118,23 +106,19 @@ func (d *douyinOrders) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *douyinOrders) fillFieldMap() {
|
func (d *douyinOrders) fillFieldMap() {
|
||||||
d.fieldMap = make(map[string]field.Expr, 16)
|
d.fieldMap = make(map[string]field.Expr, 12)
|
||||||
d.fieldMap["id"] = d.ID
|
d.fieldMap["id"] = d.ID
|
||||||
d.fieldMap["shop_order_id"] = d.ShopOrderID
|
d.fieldMap["shop_order_id"] = d.ShopOrderID
|
||||||
d.fieldMap["douyin_product_id"] = d.DouyinProductID
|
|
||||||
d.fieldMap["order_status"] = d.OrderStatus
|
d.fieldMap["order_status"] = d.OrderStatus
|
||||||
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
d.fieldMap["douyin_user_id"] = d.DouyinUserID
|
||||||
d.fieldMap["local_user_id"] = d.LocalUserID
|
d.fieldMap["local_user_id"] = d.LocalUserID
|
||||||
d.fieldMap["actual_receive_amount"] = d.ActualReceiveAmount
|
d.fieldMap["actual_receive_amount"] = d.ActualReceiveAmount
|
||||||
d.fieldMap["actual_pay_amount"] = d.ActualPayAmount
|
|
||||||
d.fieldMap["pay_type_desc"] = d.PayTypeDesc
|
d.fieldMap["pay_type_desc"] = d.PayTypeDesc
|
||||||
d.fieldMap["remark"] = d.Remark
|
d.fieldMap["remark"] = d.Remark
|
||||||
d.fieldMap["user_nickname"] = d.UserNickname
|
d.fieldMap["user_nickname"] = d.UserNickname
|
||||||
d.fieldMap["raw_data"] = d.RawData
|
d.fieldMap["raw_data"] = d.RawData
|
||||||
d.fieldMap["created_at"] = d.CreatedAt
|
d.fieldMap["created_at"] = d.CreatedAt
|
||||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
d.fieldMap["updated_at"] = d.UpdatedAt
|
||||||
d.fieldMap["reward_granted"] = d.RewardGranted
|
|
||||||
d.fieldMap["product_count"] = d.ProductCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d douyinOrders) clone(db *gorm.DB) douyinOrders {
|
func (d douyinOrders) clone(db *gorm.DB) douyinOrders {
|
||||||
|
|||||||
@ -1,352 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
|
|
||||||
"gorm.io/plugin/dbresolver"
|
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newDouyinProductRewards(db *gorm.DB, opts ...gen.DOOption) douyinProductRewards {
|
|
||||||
_douyinProductRewards := douyinProductRewards{}
|
|
||||||
|
|
||||||
_douyinProductRewards.douyinProductRewardsDo.UseDB(db, opts...)
|
|
||||||
_douyinProductRewards.douyinProductRewardsDo.UseModel(&model.DouyinProductRewards{})
|
|
||||||
|
|
||||||
tableName := _douyinProductRewards.douyinProductRewardsDo.TableName()
|
|
||||||
_douyinProductRewards.ALL = field.NewAsterisk(tableName)
|
|
||||||
_douyinProductRewards.ID = field.NewInt64(tableName, "id")
|
|
||||||
_douyinProductRewards.ProductID = field.NewString(tableName, "product_id")
|
|
||||||
_douyinProductRewards.ProductName = field.NewString(tableName, "product_name")
|
|
||||||
_douyinProductRewards.RewardType = field.NewString(tableName, "reward_type")
|
|
||||||
_douyinProductRewards.RewardPayload = field.NewString(tableName, "reward_payload")
|
|
||||||
_douyinProductRewards.Quantity = field.NewInt32(tableName, "quantity")
|
|
||||||
_douyinProductRewards.Status = field.NewInt32(tableName, "status")
|
|
||||||
_douyinProductRewards.CreatedAt = field.NewTime(tableName, "created_at")
|
|
||||||
_douyinProductRewards.UpdatedAt = field.NewTime(tableName, "updated_at")
|
|
||||||
|
|
||||||
_douyinProductRewards.fillFieldMap()
|
|
||||||
|
|
||||||
return _douyinProductRewards
|
|
||||||
}
|
|
||||||
|
|
||||||
// douyinProductRewards 抖店商品奖励规则
|
|
||||||
type douyinProductRewards struct {
|
|
||||||
douyinProductRewardsDo
|
|
||||||
|
|
||||||
ALL field.Asterisk
|
|
||||||
ID field.Int64
|
|
||||||
ProductID field.String // 抖店商品ID
|
|
||||||
ProductName field.String // 商品名称
|
|
||||||
RewardType field.String // 奖励类型
|
|
||||||
RewardPayload field.String // 奖励参数JSON
|
|
||||||
Quantity field.Int32 // 发放数量
|
|
||||||
Status field.Int32 // 状态: 1=启用 0=禁用
|
|
||||||
CreatedAt field.Time
|
|
||||||
UpdatedAt field.Time
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewards) Table(newTableName string) *douyinProductRewards {
|
|
||||||
d.douyinProductRewardsDo.UseTable(newTableName)
|
|
||||||
return d.updateTableName(newTableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewards) As(alias string) *douyinProductRewards {
|
|
||||||
d.douyinProductRewardsDo.DO = *(d.douyinProductRewardsDo.As(alias).(*gen.DO))
|
|
||||||
return d.updateTableName(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinProductRewards) updateTableName(table string) *douyinProductRewards {
|
|
||||||
d.ALL = field.NewAsterisk(table)
|
|
||||||
d.ID = field.NewInt64(table, "id")
|
|
||||||
d.ProductID = field.NewString(table, "product_id")
|
|
||||||
d.ProductName = field.NewString(table, "product_name")
|
|
||||||
d.RewardType = field.NewString(table, "reward_type")
|
|
||||||
d.RewardPayload = field.NewString(table, "reward_payload")
|
|
||||||
d.Quantity = field.NewInt32(table, "quantity")
|
|
||||||
d.Status = field.NewInt32(table, "status")
|
|
||||||
d.CreatedAt = field.NewTime(table, "created_at")
|
|
||||||
d.UpdatedAt = field.NewTime(table, "updated_at")
|
|
||||||
|
|
||||||
d.fillFieldMap()
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinProductRewards) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|
||||||
_f, ok := d.fieldMap[fieldName]
|
|
||||||
if !ok || _f == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
_oe, ok := _f.(field.OrderExpr)
|
|
||||||
return _oe, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinProductRewards) fillFieldMap() {
|
|
||||||
d.fieldMap = make(map[string]field.Expr, 9)
|
|
||||||
d.fieldMap["id"] = d.ID
|
|
||||||
d.fieldMap["product_id"] = d.ProductID
|
|
||||||
d.fieldMap["product_name"] = d.ProductName
|
|
||||||
d.fieldMap["reward_type"] = d.RewardType
|
|
||||||
d.fieldMap["reward_payload"] = d.RewardPayload
|
|
||||||
d.fieldMap["quantity"] = d.Quantity
|
|
||||||
d.fieldMap["status"] = d.Status
|
|
||||||
d.fieldMap["created_at"] = d.CreatedAt
|
|
||||||
d.fieldMap["updated_at"] = d.UpdatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewards) clone(db *gorm.DB) douyinProductRewards {
|
|
||||||
d.douyinProductRewardsDo.ReplaceConnPool(db.Statement.ConnPool)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewards) replaceDB(db *gorm.DB) douyinProductRewards {
|
|
||||||
d.douyinProductRewardsDo.ReplaceDB(db)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
type douyinProductRewardsDo struct{ gen.DO }
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Debug() *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Debug())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) WithContext(ctx context.Context) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.WithContext(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) ReadDB() *douyinProductRewardsDo {
|
|
||||||
return d.Clauses(dbresolver.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) WriteDB() *douyinProductRewardsDo {
|
|
||||||
return d.Clauses(dbresolver.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Session(config *gorm.Session) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Session(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Clauses(conds ...clause.Expression) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Clauses(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Returning(value interface{}, columns ...string) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Returning(value, columns...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Not(conds ...gen.Condition) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Not(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Or(conds ...gen.Condition) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Or(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Select(conds ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Select(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Where(conds ...gen.Condition) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Where(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Order(conds ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Order(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Distinct(cols ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Distinct(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Omit(cols ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Omit(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Join(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Join(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.LeftJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) RightJoin(table schema.Tabler, on ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.RightJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Group(cols ...field.Expr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Group(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Having(conds ...gen.Condition) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Having(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Limit(limit int) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Limit(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Offset(offset int) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Offset(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Scopes(funcs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Unscoped() *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Unscoped())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Create(values ...*model.DouyinProductRewards) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.DO.Create(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) CreateInBatches(values []*model.DouyinProductRewards, batchSize int) error {
|
|
||||||
return d.DO.CreateInBatches(values, batchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save : !!! underlying implementation is different with GORM
|
|
||||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
|
||||||
func (d douyinProductRewardsDo) Save(values ...*model.DouyinProductRewards) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.DO.Save(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) First() (*model.DouyinProductRewards, error) {
|
|
||||||
if result, err := d.DO.First(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinProductRewards), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Take() (*model.DouyinProductRewards, error) {
|
|
||||||
if result, err := d.DO.Take(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinProductRewards), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Last() (*model.DouyinProductRewards, error) {
|
|
||||||
if result, err := d.DO.Last(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinProductRewards), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Find() ([]*model.DouyinProductRewards, error) {
|
|
||||||
result, err := d.DO.Find()
|
|
||||||
return result.([]*model.DouyinProductRewards), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.DouyinProductRewards, err error) {
|
|
||||||
buf := make([]*model.DouyinProductRewards, 0, batchSize)
|
|
||||||
err = d.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
|
||||||
defer func() { results = append(results, buf...) }()
|
|
||||||
return fc(tx, batch)
|
|
||||||
})
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) FindInBatches(result *[]*model.DouyinProductRewards, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
|
||||||
return d.DO.FindInBatches(result, batchSize, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Attrs(attrs ...field.AssignExpr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Attrs(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Assign(attrs ...field.AssignExpr) *douyinProductRewardsDo {
|
|
||||||
return d.withDO(d.DO.Assign(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Joins(fields ...field.RelationField) *douyinProductRewardsDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
d = *d.withDO(d.DO.Joins(_f))
|
|
||||||
}
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Preload(fields ...field.RelationField) *douyinProductRewardsDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
d = *d.withDO(d.DO.Preload(_f))
|
|
||||||
}
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) FirstOrInit() (*model.DouyinProductRewards, error) {
|
|
||||||
if result, err := d.DO.FirstOrInit(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinProductRewards), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) FirstOrCreate() (*model.DouyinProductRewards, error) {
|
|
||||||
if result, err := d.DO.FirstOrCreate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.DouyinProductRewards), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) FindByPage(offset int, limit int) (result []*model.DouyinProductRewards, count int64, err error) {
|
|
||||||
result, err = d.Offset(offset).Limit(limit).Find()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
|
||||||
count = int64(size + offset)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err = d.Offset(-1).Limit(-1).Count()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
|
||||||
count, err = d.Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.Offset(offset).Limit(limit).Scan(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Scan(result interface{}) (err error) {
|
|
||||||
return d.DO.Scan(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d douyinProductRewardsDo) Delete(models ...*model.DouyinProductRewards) (result gen.ResultInfo, err error) {
|
|
||||||
return d.DO.Delete(models)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *douyinProductRewardsDo) withDO(do gen.Dao) *douyinProductRewardsDo {
|
|
||||||
d.DO = *do.(*gen.DO)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
@ -28,15 +28,10 @@ var (
|
|||||||
AuditRollbackLogs *auditRollbackLogs
|
AuditRollbackLogs *auditRollbackLogs
|
||||||
Banner *banner
|
Banner *banner
|
||||||
Channels *channels
|
Channels *channels
|
||||||
DouyinBlacklist *douyinBlacklist
|
|
||||||
DouyinOrders *douyinOrders
|
DouyinOrders *douyinOrders
|
||||||
DouyinProductRewards *douyinProductRewards
|
|
||||||
GamePassPackages *gamePassPackages
|
GamePassPackages *gamePassPackages
|
||||||
GameTicketLogs *gameTicketLogs
|
GameTicketLogs *gameTicketLogs
|
||||||
IssuePositionClaims *issuePositionClaims
|
IssuePositionClaims *issuePositionClaims
|
||||||
LivestreamActivities *livestreamActivities
|
|
||||||
LivestreamDrawLogs *livestreamDrawLogs
|
|
||||||
LivestreamPrizes *livestreamPrizes
|
|
||||||
LogOperation *logOperation
|
LogOperation *logOperation
|
||||||
LogRequest *logRequest
|
LogRequest *logRequest
|
||||||
LotteryRefundLogs *lotteryRefundLogs
|
LotteryRefundLogs *lotteryRefundLogs
|
||||||
@ -100,15 +95,10 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||||||
AuditRollbackLogs = &Q.AuditRollbackLogs
|
AuditRollbackLogs = &Q.AuditRollbackLogs
|
||||||
Banner = &Q.Banner
|
Banner = &Q.Banner
|
||||||
Channels = &Q.Channels
|
Channels = &Q.Channels
|
||||||
DouyinBlacklist = &Q.DouyinBlacklist
|
|
||||||
DouyinOrders = &Q.DouyinOrders
|
DouyinOrders = &Q.DouyinOrders
|
||||||
DouyinProductRewards = &Q.DouyinProductRewards
|
|
||||||
GamePassPackages = &Q.GamePassPackages
|
GamePassPackages = &Q.GamePassPackages
|
||||||
GameTicketLogs = &Q.GameTicketLogs
|
GameTicketLogs = &Q.GameTicketLogs
|
||||||
IssuePositionClaims = &Q.IssuePositionClaims
|
IssuePositionClaims = &Q.IssuePositionClaims
|
||||||
LivestreamActivities = &Q.LivestreamActivities
|
|
||||||
LivestreamDrawLogs = &Q.LivestreamDrawLogs
|
|
||||||
LivestreamPrizes = &Q.LivestreamPrizes
|
|
||||||
LogOperation = &Q.LogOperation
|
LogOperation = &Q.LogOperation
|
||||||
LogRequest = &Q.LogRequest
|
LogRequest = &Q.LogRequest
|
||||||
LotteryRefundLogs = &Q.LotteryRefundLogs
|
LotteryRefundLogs = &Q.LotteryRefundLogs
|
||||||
@ -173,15 +163,10 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||||||
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
AuditRollbackLogs: newAuditRollbackLogs(db, opts...),
|
||||||
Banner: newBanner(db, opts...),
|
Banner: newBanner(db, opts...),
|
||||||
Channels: newChannels(db, opts...),
|
Channels: newChannels(db, opts...),
|
||||||
DouyinBlacklist: newDouyinBlacklist(db, opts...),
|
|
||||||
DouyinOrders: newDouyinOrders(db, opts...),
|
DouyinOrders: newDouyinOrders(db, opts...),
|
||||||
DouyinProductRewards: newDouyinProductRewards(db, opts...),
|
|
||||||
GamePassPackages: newGamePassPackages(db, opts...),
|
GamePassPackages: newGamePassPackages(db, opts...),
|
||||||
GameTicketLogs: newGameTicketLogs(db, opts...),
|
GameTicketLogs: newGameTicketLogs(db, opts...),
|
||||||
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
IssuePositionClaims: newIssuePositionClaims(db, opts...),
|
||||||
LivestreamActivities: newLivestreamActivities(db, opts...),
|
|
||||||
LivestreamDrawLogs: newLivestreamDrawLogs(db, opts...),
|
|
||||||
LivestreamPrizes: newLivestreamPrizes(db, opts...),
|
|
||||||
LogOperation: newLogOperation(db, opts...),
|
LogOperation: newLogOperation(db, opts...),
|
||||||
LogRequest: newLogRequest(db, opts...),
|
LogRequest: newLogRequest(db, opts...),
|
||||||
LotteryRefundLogs: newLotteryRefundLogs(db, opts...),
|
LotteryRefundLogs: newLotteryRefundLogs(db, opts...),
|
||||||
@ -247,15 +232,10 @@ type Query struct {
|
|||||||
AuditRollbackLogs auditRollbackLogs
|
AuditRollbackLogs auditRollbackLogs
|
||||||
Banner banner
|
Banner banner
|
||||||
Channels channels
|
Channels channels
|
||||||
DouyinBlacklist douyinBlacklist
|
|
||||||
DouyinOrders douyinOrders
|
DouyinOrders douyinOrders
|
||||||
DouyinProductRewards douyinProductRewards
|
|
||||||
GamePassPackages gamePassPackages
|
GamePassPackages gamePassPackages
|
||||||
GameTicketLogs gameTicketLogs
|
GameTicketLogs gameTicketLogs
|
||||||
IssuePositionClaims issuePositionClaims
|
IssuePositionClaims issuePositionClaims
|
||||||
LivestreamActivities livestreamActivities
|
|
||||||
LivestreamDrawLogs livestreamDrawLogs
|
|
||||||
LivestreamPrizes livestreamPrizes
|
|
||||||
LogOperation logOperation
|
LogOperation logOperation
|
||||||
LogRequest logRequest
|
LogRequest logRequest
|
||||||
LotteryRefundLogs lotteryRefundLogs
|
LotteryRefundLogs lotteryRefundLogs
|
||||||
@ -322,15 +302,10 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
AuditRollbackLogs: q.AuditRollbackLogs.clone(db),
|
||||||
Banner: q.Banner.clone(db),
|
Banner: q.Banner.clone(db),
|
||||||
Channels: q.Channels.clone(db),
|
Channels: q.Channels.clone(db),
|
||||||
DouyinBlacklist: q.DouyinBlacklist.clone(db),
|
|
||||||
DouyinOrders: q.DouyinOrders.clone(db),
|
DouyinOrders: q.DouyinOrders.clone(db),
|
||||||
DouyinProductRewards: q.DouyinProductRewards.clone(db),
|
|
||||||
GamePassPackages: q.GamePassPackages.clone(db),
|
GamePassPackages: q.GamePassPackages.clone(db),
|
||||||
GameTicketLogs: q.GameTicketLogs.clone(db),
|
GameTicketLogs: q.GameTicketLogs.clone(db),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
IssuePositionClaims: q.IssuePositionClaims.clone(db),
|
||||||
LivestreamActivities: q.LivestreamActivities.clone(db),
|
|
||||||
LivestreamDrawLogs: q.LivestreamDrawLogs.clone(db),
|
|
||||||
LivestreamPrizes: q.LivestreamPrizes.clone(db),
|
|
||||||
LogOperation: q.LogOperation.clone(db),
|
LogOperation: q.LogOperation.clone(db),
|
||||||
LogRequest: q.LogRequest.clone(db),
|
LogRequest: q.LogRequest.clone(db),
|
||||||
LotteryRefundLogs: q.LotteryRefundLogs.clone(db),
|
LotteryRefundLogs: q.LotteryRefundLogs.clone(db),
|
||||||
@ -404,15 +379,10 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
AuditRollbackLogs: q.AuditRollbackLogs.replaceDB(db),
|
||||||
Banner: q.Banner.replaceDB(db),
|
Banner: q.Banner.replaceDB(db),
|
||||||
Channels: q.Channels.replaceDB(db),
|
Channels: q.Channels.replaceDB(db),
|
||||||
DouyinBlacklist: q.DouyinBlacklist.replaceDB(db),
|
|
||||||
DouyinOrders: q.DouyinOrders.replaceDB(db),
|
DouyinOrders: q.DouyinOrders.replaceDB(db),
|
||||||
DouyinProductRewards: q.DouyinProductRewards.replaceDB(db),
|
|
||||||
GamePassPackages: q.GamePassPackages.replaceDB(db),
|
GamePassPackages: q.GamePassPackages.replaceDB(db),
|
||||||
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
|
||||||
LivestreamActivities: q.LivestreamActivities.replaceDB(db),
|
|
||||||
LivestreamDrawLogs: q.LivestreamDrawLogs.replaceDB(db),
|
|
||||||
LivestreamPrizes: q.LivestreamPrizes.replaceDB(db),
|
|
||||||
LogOperation: q.LogOperation.replaceDB(db),
|
LogOperation: q.LogOperation.replaceDB(db),
|
||||||
LogRequest: q.LogRequest.replaceDB(db),
|
LogRequest: q.LogRequest.replaceDB(db),
|
||||||
LotteryRefundLogs: q.LotteryRefundLogs.replaceDB(db),
|
LotteryRefundLogs: q.LotteryRefundLogs.replaceDB(db),
|
||||||
@ -476,15 +446,10 @@ type queryCtx struct {
|
|||||||
AuditRollbackLogs *auditRollbackLogsDo
|
AuditRollbackLogs *auditRollbackLogsDo
|
||||||
Banner *bannerDo
|
Banner *bannerDo
|
||||||
Channels *channelsDo
|
Channels *channelsDo
|
||||||
DouyinBlacklist *douyinBlacklistDo
|
|
||||||
DouyinOrders *douyinOrdersDo
|
DouyinOrders *douyinOrdersDo
|
||||||
DouyinProductRewards *douyinProductRewardsDo
|
|
||||||
GamePassPackages *gamePassPackagesDo
|
GamePassPackages *gamePassPackagesDo
|
||||||
GameTicketLogs *gameTicketLogsDo
|
GameTicketLogs *gameTicketLogsDo
|
||||||
IssuePositionClaims *issuePositionClaimsDo
|
IssuePositionClaims *issuePositionClaimsDo
|
||||||
LivestreamActivities *livestreamActivitiesDo
|
|
||||||
LivestreamDrawLogs *livestreamDrawLogsDo
|
|
||||||
LivestreamPrizes *livestreamPrizesDo
|
|
||||||
LogOperation *logOperationDo
|
LogOperation *logOperationDo
|
||||||
LogRequest *logRequestDo
|
LogRequest *logRequestDo
|
||||||
LotteryRefundLogs *lotteryRefundLogsDo
|
LotteryRefundLogs *lotteryRefundLogsDo
|
||||||
@ -548,15 +513,10 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||||||
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
AuditRollbackLogs: q.AuditRollbackLogs.WithContext(ctx),
|
||||||
Banner: q.Banner.WithContext(ctx),
|
Banner: q.Banner.WithContext(ctx),
|
||||||
Channels: q.Channels.WithContext(ctx),
|
Channels: q.Channels.WithContext(ctx),
|
||||||
DouyinBlacklist: q.DouyinBlacklist.WithContext(ctx),
|
|
||||||
DouyinOrders: q.DouyinOrders.WithContext(ctx),
|
DouyinOrders: q.DouyinOrders.WithContext(ctx),
|
||||||
DouyinProductRewards: q.DouyinProductRewards.WithContext(ctx),
|
|
||||||
GamePassPackages: q.GamePassPackages.WithContext(ctx),
|
GamePassPackages: q.GamePassPackages.WithContext(ctx),
|
||||||
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
|
||||||
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
|
||||||
LivestreamActivities: q.LivestreamActivities.WithContext(ctx),
|
|
||||||
LivestreamDrawLogs: q.LivestreamDrawLogs.WithContext(ctx),
|
|
||||||
LivestreamPrizes: q.LivestreamPrizes.WithContext(ctx),
|
|
||||||
LogOperation: q.LogOperation.WithContext(ctx),
|
LogOperation: q.LogOperation.WithContext(ctx),
|
||||||
LogRequest: q.LogRequest.WithContext(ctx),
|
LogRequest: q.LogRequest.WithContext(ctx),
|
||||||
LotteryRefundLogs: q.LotteryRefundLogs.WithContext(ctx),
|
LotteryRefundLogs: q.LotteryRefundLogs.WithContext(ctx),
|
||||||
|
|||||||
@ -1,384 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
|
|
||||||
"gorm.io/plugin/dbresolver"
|
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newLivestreamActivities(db *gorm.DB, opts ...gen.DOOption) livestreamActivities {
|
|
||||||
_livestreamActivities := livestreamActivities{}
|
|
||||||
|
|
||||||
_livestreamActivities.livestreamActivitiesDo.UseDB(db, opts...)
|
|
||||||
_livestreamActivities.livestreamActivitiesDo.UseModel(&model.LivestreamActivities{})
|
|
||||||
|
|
||||||
tableName := _livestreamActivities.livestreamActivitiesDo.TableName()
|
|
||||||
_livestreamActivities.ALL = field.NewAsterisk(tableName)
|
|
||||||
_livestreamActivities.ID = field.NewInt64(tableName, "id")
|
|
||||||
_livestreamActivities.Name = field.NewString(tableName, "name")
|
|
||||||
_livestreamActivities.StreamerName = field.NewString(tableName, "streamer_name")
|
|
||||||
_livestreamActivities.StreamerContact = field.NewString(tableName, "streamer_contact")
|
|
||||||
_livestreamActivities.AccessCode = field.NewString(tableName, "access_code")
|
|
||||||
_livestreamActivities.DouyinProductID = field.NewString(tableName, "douyin_product_id")
|
|
||||||
_livestreamActivities.Status = field.NewInt32(tableName, "status")
|
|
||||||
_livestreamActivities.CommitmentAlgo = field.NewString(tableName, "commitment_algo")
|
|
||||||
_livestreamActivities.CommitmentSeedMaster = field.NewBytes(tableName, "commitment_seed_master")
|
|
||||||
_livestreamActivities.CommitmentSeedHash = field.NewBytes(tableName, "commitment_seed_hash")
|
|
||||||
_livestreamActivities.CommitmentStateVersion = field.NewInt32(tableName, "commitment_state_version")
|
|
||||||
_livestreamActivities.StartTime = field.NewTime(tableName, "start_time")
|
|
||||||
_livestreamActivities.EndTime = field.NewTime(tableName, "end_time")
|
|
||||||
_livestreamActivities.CreatedAt = field.NewTime(tableName, "created_at")
|
|
||||||
_livestreamActivities.UpdatedAt = field.NewTime(tableName, "updated_at")
|
|
||||||
_livestreamActivities.DeletedAt = field.NewField(tableName, "deleted_at")
|
|
||||||
_livestreamActivities.TicketPrice = field.NewInt32(tableName, "ticket_price")
|
|
||||||
|
|
||||||
_livestreamActivities.fillFieldMap()
|
|
||||||
|
|
||||||
return _livestreamActivities
|
|
||||||
}
|
|
||||||
|
|
||||||
// livestreamActivities 直播间活动表
|
|
||||||
type livestreamActivities struct {
|
|
||||||
livestreamActivitiesDo
|
|
||||||
|
|
||||||
ALL field.Asterisk
|
|
||||||
ID field.Int64 // 主键ID
|
|
||||||
Name field.String // 活动名称
|
|
||||||
StreamerName field.String // 主播名称
|
|
||||||
StreamerContact field.String // 主播联系方式
|
|
||||||
AccessCode field.String // 唯一访问码
|
|
||||||
DouyinProductID field.String // 关联抖店商品ID
|
|
||||||
Status field.Int32 // 状态:1进行中 2已结束
|
|
||||||
CommitmentAlgo field.String // 承诺算法版本
|
|
||||||
CommitmentSeedMaster field.Bytes // 主种子(32字节)
|
|
||||||
CommitmentSeedHash field.Bytes // 种子SHA256哈希
|
|
||||||
CommitmentStateVersion field.Int32 // 状态版本
|
|
||||||
StartTime field.Time // 开始时间
|
|
||||||
EndTime field.Time // 结束时间
|
|
||||||
CreatedAt field.Time // 创建时间
|
|
||||||
UpdatedAt field.Time // 更新时间
|
|
||||||
DeletedAt field.Field // 删除时间
|
|
||||||
TicketPrice field.Int32
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivities) Table(newTableName string) *livestreamActivities {
|
|
||||||
l.livestreamActivitiesDo.UseTable(newTableName)
|
|
||||||
return l.updateTableName(newTableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivities) As(alias string) *livestreamActivities {
|
|
||||||
l.livestreamActivitiesDo.DO = *(l.livestreamActivitiesDo.As(alias).(*gen.DO))
|
|
||||||
return l.updateTableName(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamActivities) updateTableName(table string) *livestreamActivities {
|
|
||||||
l.ALL = field.NewAsterisk(table)
|
|
||||||
l.ID = field.NewInt64(table, "id")
|
|
||||||
l.Name = field.NewString(table, "name")
|
|
||||||
l.StreamerName = field.NewString(table, "streamer_name")
|
|
||||||
l.StreamerContact = field.NewString(table, "streamer_contact")
|
|
||||||
l.AccessCode = field.NewString(table, "access_code")
|
|
||||||
l.DouyinProductID = field.NewString(table, "douyin_product_id")
|
|
||||||
l.Status = field.NewInt32(table, "status")
|
|
||||||
l.CommitmentAlgo = field.NewString(table, "commitment_algo")
|
|
||||||
l.CommitmentSeedMaster = field.NewBytes(table, "commitment_seed_master")
|
|
||||||
l.CommitmentSeedHash = field.NewBytes(table, "commitment_seed_hash")
|
|
||||||
l.CommitmentStateVersion = field.NewInt32(table, "commitment_state_version")
|
|
||||||
l.StartTime = field.NewTime(table, "start_time")
|
|
||||||
l.EndTime = field.NewTime(table, "end_time")
|
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
|
||||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
|
||||||
l.DeletedAt = field.NewField(table, "deleted_at")
|
|
||||||
l.TicketPrice = field.NewInt32(table, "ticket_price")
|
|
||||||
|
|
||||||
l.fillFieldMap()
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamActivities) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|
||||||
_f, ok := l.fieldMap[fieldName]
|
|
||||||
if !ok || _f == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
_oe, ok := _f.(field.OrderExpr)
|
|
||||||
return _oe, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamActivities) fillFieldMap() {
|
|
||||||
l.fieldMap = make(map[string]field.Expr, 17)
|
|
||||||
l.fieldMap["id"] = l.ID
|
|
||||||
l.fieldMap["name"] = l.Name
|
|
||||||
l.fieldMap["streamer_name"] = l.StreamerName
|
|
||||||
l.fieldMap["streamer_contact"] = l.StreamerContact
|
|
||||||
l.fieldMap["access_code"] = l.AccessCode
|
|
||||||
l.fieldMap["douyin_product_id"] = l.DouyinProductID
|
|
||||||
l.fieldMap["status"] = l.Status
|
|
||||||
l.fieldMap["commitment_algo"] = l.CommitmentAlgo
|
|
||||||
l.fieldMap["commitment_seed_master"] = l.CommitmentSeedMaster
|
|
||||||
l.fieldMap["commitment_seed_hash"] = l.CommitmentSeedHash
|
|
||||||
l.fieldMap["commitment_state_version"] = l.CommitmentStateVersion
|
|
||||||
l.fieldMap["start_time"] = l.StartTime
|
|
||||||
l.fieldMap["end_time"] = l.EndTime
|
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
|
||||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
|
||||||
l.fieldMap["deleted_at"] = l.DeletedAt
|
|
||||||
l.fieldMap["ticket_price"] = l.TicketPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivities) clone(db *gorm.DB) livestreamActivities {
|
|
||||||
l.livestreamActivitiesDo.ReplaceConnPool(db.Statement.ConnPool)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivities) replaceDB(db *gorm.DB) livestreamActivities {
|
|
||||||
l.livestreamActivitiesDo.ReplaceDB(db)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamActivitiesDo struct{ gen.DO }
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Debug() *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Debug())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) WithContext(ctx context.Context) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.WithContext(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) ReadDB() *livestreamActivitiesDo {
|
|
||||||
return l.Clauses(dbresolver.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) WriteDB() *livestreamActivitiesDo {
|
|
||||||
return l.Clauses(dbresolver.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Session(config *gorm.Session) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Session(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Clauses(conds ...clause.Expression) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Clauses(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Returning(value interface{}, columns ...string) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Returning(value, columns...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Not(conds ...gen.Condition) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Not(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Or(conds ...gen.Condition) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Or(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Select(conds ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Select(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Where(conds ...gen.Condition) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Where(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Order(conds ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Order(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Distinct(cols ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Distinct(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Omit(cols ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Omit(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Join(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Join(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.LeftJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.RightJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Group(cols ...field.Expr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Group(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Having(conds ...gen.Condition) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Having(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Limit(limit int) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Limit(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Offset(offset int) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Offset(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Scopes(funcs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Unscoped() *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Unscoped())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Create(values ...*model.LivestreamActivities) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Create(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) CreateInBatches(values []*model.LivestreamActivities, batchSize int) error {
|
|
||||||
return l.DO.CreateInBatches(values, batchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save : !!! underlying implementation is different with GORM
|
|
||||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
|
||||||
func (l livestreamActivitiesDo) Save(values ...*model.LivestreamActivities) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Save(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) First() (*model.LivestreamActivities, error) {
|
|
||||||
if result, err := l.DO.First(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamActivities), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Take() (*model.LivestreamActivities, error) {
|
|
||||||
if result, err := l.DO.Take(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamActivities), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Last() (*model.LivestreamActivities, error) {
|
|
||||||
if result, err := l.DO.Last(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamActivities), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Find() ([]*model.LivestreamActivities, error) {
|
|
||||||
result, err := l.DO.Find()
|
|
||||||
return result.([]*model.LivestreamActivities), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamActivities, err error) {
|
|
||||||
buf := make([]*model.LivestreamActivities, 0, batchSize)
|
|
||||||
err = l.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
|
||||||
defer func() { results = append(results, buf...) }()
|
|
||||||
return fc(tx, batch)
|
|
||||||
})
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) FindInBatches(result *[]*model.LivestreamActivities, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
|
||||||
return l.DO.FindInBatches(result, batchSize, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Attrs(attrs ...field.AssignExpr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Attrs(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Assign(attrs ...field.AssignExpr) *livestreamActivitiesDo {
|
|
||||||
return l.withDO(l.DO.Assign(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Joins(fields ...field.RelationField) *livestreamActivitiesDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Joins(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Preload(fields ...field.RelationField) *livestreamActivitiesDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Preload(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) FirstOrInit() (*model.LivestreamActivities, error) {
|
|
||||||
if result, err := l.DO.FirstOrInit(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamActivities), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) FirstOrCreate() (*model.LivestreamActivities, error) {
|
|
||||||
if result, err := l.DO.FirstOrCreate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamActivities), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) FindByPage(offset int, limit int) (result []*model.LivestreamActivities, count int64, err error) {
|
|
||||||
result, err = l.Offset(offset).Limit(limit).Find()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
|
||||||
count = int64(size + offset)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err = l.Offset(-1).Limit(-1).Count()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
|
||||||
count, err = l.Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.Offset(offset).Limit(limit).Scan(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Scan(result interface{}) (err error) {
|
|
||||||
return l.DO.Scan(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamActivitiesDo) Delete(models ...*model.LivestreamActivities) (result gen.ResultInfo, err error) {
|
|
||||||
return l.DO.Delete(models)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamActivitiesDo) withDO(do gen.Dao) *livestreamActivitiesDo {
|
|
||||||
l.DO = *do.(*gen.DO)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
@ -1,380 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
|
|
||||||
"gorm.io/plugin/dbresolver"
|
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newLivestreamDrawLogs(db *gorm.DB, opts ...gen.DOOption) livestreamDrawLogs {
|
|
||||||
_livestreamDrawLogs := livestreamDrawLogs{}
|
|
||||||
|
|
||||||
_livestreamDrawLogs.livestreamDrawLogsDo.UseDB(db, opts...)
|
|
||||||
_livestreamDrawLogs.livestreamDrawLogsDo.UseModel(&model.LivestreamDrawLogs{})
|
|
||||||
|
|
||||||
tableName := _livestreamDrawLogs.livestreamDrawLogsDo.TableName()
|
|
||||||
_livestreamDrawLogs.ALL = field.NewAsterisk(tableName)
|
|
||||||
_livestreamDrawLogs.ID = field.NewInt64(tableName, "id")
|
|
||||||
_livestreamDrawLogs.ActivityID = field.NewInt64(tableName, "activity_id")
|
|
||||||
_livestreamDrawLogs.PrizeID = field.NewInt64(tableName, "prize_id")
|
|
||||||
_livestreamDrawLogs.DouyinOrderID = field.NewInt64(tableName, "douyin_order_id")
|
|
||||||
_livestreamDrawLogs.ShopOrderID = field.NewString(tableName, "shop_order_id")
|
|
||||||
_livestreamDrawLogs.LocalUserID = field.NewInt64(tableName, "local_user_id")
|
|
||||||
_livestreamDrawLogs.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
|
||||||
_livestreamDrawLogs.UserNickname = field.NewString(tableName, "user_nickname")
|
|
||||||
_livestreamDrawLogs.PrizeName = field.NewString(tableName, "prize_name")
|
|
||||||
_livestreamDrawLogs.Level = field.NewInt32(tableName, "level")
|
|
||||||
_livestreamDrawLogs.SeedHash = field.NewString(tableName, "seed_hash")
|
|
||||||
_livestreamDrawLogs.RandValue = field.NewInt64(tableName, "rand_value")
|
|
||||||
_livestreamDrawLogs.WeightsTotal = field.NewInt64(tableName, "weights_total")
|
|
||||||
_livestreamDrawLogs.CreatedAt = field.NewTime(tableName, "created_at")
|
|
||||||
_livestreamDrawLogs.IsGranted = field.NewBool(tableName, "is_granted")
|
|
||||||
_livestreamDrawLogs.IsRefunded = field.NewInt32(tableName, "is_refunded")
|
|
||||||
|
|
||||||
_livestreamDrawLogs.fillFieldMap()
|
|
||||||
|
|
||||||
return _livestreamDrawLogs
|
|
||||||
}
|
|
||||||
|
|
||||||
// livestreamDrawLogs 直播间中奖记录表
|
|
||||||
type livestreamDrawLogs struct {
|
|
||||||
livestreamDrawLogsDo
|
|
||||||
|
|
||||||
ALL field.Asterisk
|
|
||||||
ID field.Int64 // 主键ID
|
|
||||||
ActivityID field.Int64 // 关联livestream_activities.id
|
|
||||||
PrizeID field.Int64 // 关联livestream_prizes.id
|
|
||||||
DouyinOrderID field.Int64 // 关联douyin_orders.id
|
|
||||||
ShopOrderID field.String // 抖店订单号
|
|
||||||
LocalUserID field.Int64 // 本地用户ID
|
|
||||||
DouyinUserID field.String // 抖音用户ID
|
|
||||||
UserNickname field.String // 用户昵称
|
|
||||||
PrizeName field.String // 中奖奖品名称快照
|
|
||||||
Level field.Int32 // 奖品等级
|
|
||||||
SeedHash field.String // 哈希种子
|
|
||||||
RandValue field.Int64 // 随机值
|
|
||||||
WeightsTotal field.Int64 // 权重总和
|
|
||||||
CreatedAt field.Time // 中奖时间
|
|
||||||
IsGranted field.Bool // 是否已发放奖品
|
|
||||||
IsRefunded field.Int32 // 订单是否已退款
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogs) Table(newTableName string) *livestreamDrawLogs {
|
|
||||||
l.livestreamDrawLogsDo.UseTable(newTableName)
|
|
||||||
return l.updateTableName(newTableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogs) As(alias string) *livestreamDrawLogs {
|
|
||||||
l.livestreamDrawLogsDo.DO = *(l.livestreamDrawLogsDo.As(alias).(*gen.DO))
|
|
||||||
return l.updateTableName(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamDrawLogs) updateTableName(table string) *livestreamDrawLogs {
|
|
||||||
l.ALL = field.NewAsterisk(table)
|
|
||||||
l.ID = field.NewInt64(table, "id")
|
|
||||||
l.ActivityID = field.NewInt64(table, "activity_id")
|
|
||||||
l.PrizeID = field.NewInt64(table, "prize_id")
|
|
||||||
l.DouyinOrderID = field.NewInt64(table, "douyin_order_id")
|
|
||||||
l.ShopOrderID = field.NewString(table, "shop_order_id")
|
|
||||||
l.LocalUserID = field.NewInt64(table, "local_user_id")
|
|
||||||
l.DouyinUserID = field.NewString(table, "douyin_user_id")
|
|
||||||
l.UserNickname = field.NewString(table, "user_nickname")
|
|
||||||
l.PrizeName = field.NewString(table, "prize_name")
|
|
||||||
l.Level = field.NewInt32(table, "level")
|
|
||||||
l.SeedHash = field.NewString(table, "seed_hash")
|
|
||||||
l.RandValue = field.NewInt64(table, "rand_value")
|
|
||||||
l.WeightsTotal = field.NewInt64(table, "weights_total")
|
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
|
||||||
l.IsGranted = field.NewBool(table, "is_granted")
|
|
||||||
l.IsRefunded = field.NewInt32(table, "is_refunded")
|
|
||||||
|
|
||||||
l.fillFieldMap()
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamDrawLogs) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|
||||||
_f, ok := l.fieldMap[fieldName]
|
|
||||||
if !ok || _f == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
_oe, ok := _f.(field.OrderExpr)
|
|
||||||
return _oe, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamDrawLogs) fillFieldMap() {
|
|
||||||
l.fieldMap = make(map[string]field.Expr, 16)
|
|
||||||
l.fieldMap["id"] = l.ID
|
|
||||||
l.fieldMap["activity_id"] = l.ActivityID
|
|
||||||
l.fieldMap["prize_id"] = l.PrizeID
|
|
||||||
l.fieldMap["douyin_order_id"] = l.DouyinOrderID
|
|
||||||
l.fieldMap["shop_order_id"] = l.ShopOrderID
|
|
||||||
l.fieldMap["local_user_id"] = l.LocalUserID
|
|
||||||
l.fieldMap["douyin_user_id"] = l.DouyinUserID
|
|
||||||
l.fieldMap["user_nickname"] = l.UserNickname
|
|
||||||
l.fieldMap["prize_name"] = l.PrizeName
|
|
||||||
l.fieldMap["level"] = l.Level
|
|
||||||
l.fieldMap["seed_hash"] = l.SeedHash
|
|
||||||
l.fieldMap["rand_value"] = l.RandValue
|
|
||||||
l.fieldMap["weights_total"] = l.WeightsTotal
|
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
|
||||||
l.fieldMap["is_granted"] = l.IsGranted
|
|
||||||
l.fieldMap["is_refunded"] = l.IsRefunded
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogs) clone(db *gorm.DB) livestreamDrawLogs {
|
|
||||||
l.livestreamDrawLogsDo.ReplaceConnPool(db.Statement.ConnPool)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogs) replaceDB(db *gorm.DB) livestreamDrawLogs {
|
|
||||||
l.livestreamDrawLogsDo.ReplaceDB(db)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamDrawLogsDo struct{ gen.DO }
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Debug() *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Debug())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) WithContext(ctx context.Context) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.WithContext(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) ReadDB() *livestreamDrawLogsDo {
|
|
||||||
return l.Clauses(dbresolver.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) WriteDB() *livestreamDrawLogsDo {
|
|
||||||
return l.Clauses(dbresolver.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Session(config *gorm.Session) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Session(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Clauses(conds ...clause.Expression) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Clauses(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Returning(value interface{}, columns ...string) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Returning(value, columns...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Not(conds ...gen.Condition) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Not(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Or(conds ...gen.Condition) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Or(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Select(conds ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Select(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Where(conds ...gen.Condition) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Where(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Order(conds ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Order(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Distinct(cols ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Distinct(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Omit(cols ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Omit(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Join(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Join(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.LeftJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.RightJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Group(cols ...field.Expr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Group(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Having(conds ...gen.Condition) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Having(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Limit(limit int) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Limit(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Offset(offset int) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Offset(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Scopes(funcs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Unscoped() *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Unscoped())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Create(values ...*model.LivestreamDrawLogs) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Create(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) CreateInBatches(values []*model.LivestreamDrawLogs, batchSize int) error {
|
|
||||||
return l.DO.CreateInBatches(values, batchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save : !!! underlying implementation is different with GORM
|
|
||||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
|
||||||
func (l livestreamDrawLogsDo) Save(values ...*model.LivestreamDrawLogs) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Save(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) First() (*model.LivestreamDrawLogs, error) {
|
|
||||||
if result, err := l.DO.First(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamDrawLogs), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Take() (*model.LivestreamDrawLogs, error) {
|
|
||||||
if result, err := l.DO.Take(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamDrawLogs), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Last() (*model.LivestreamDrawLogs, error) {
|
|
||||||
if result, err := l.DO.Last(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamDrawLogs), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Find() ([]*model.LivestreamDrawLogs, error) {
|
|
||||||
result, err := l.DO.Find()
|
|
||||||
return result.([]*model.LivestreamDrawLogs), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamDrawLogs, err error) {
|
|
||||||
buf := make([]*model.LivestreamDrawLogs, 0, batchSize)
|
|
||||||
err = l.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
|
||||||
defer func() { results = append(results, buf...) }()
|
|
||||||
return fc(tx, batch)
|
|
||||||
})
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) FindInBatches(result *[]*model.LivestreamDrawLogs, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
|
||||||
return l.DO.FindInBatches(result, batchSize, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Attrs(attrs ...field.AssignExpr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Attrs(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Assign(attrs ...field.AssignExpr) *livestreamDrawLogsDo {
|
|
||||||
return l.withDO(l.DO.Assign(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Joins(fields ...field.RelationField) *livestreamDrawLogsDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Joins(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Preload(fields ...field.RelationField) *livestreamDrawLogsDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Preload(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) FirstOrInit() (*model.LivestreamDrawLogs, error) {
|
|
||||||
if result, err := l.DO.FirstOrInit(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamDrawLogs), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) FirstOrCreate() (*model.LivestreamDrawLogs, error) {
|
|
||||||
if result, err := l.DO.FirstOrCreate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamDrawLogs), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) FindByPage(offset int, limit int) (result []*model.LivestreamDrawLogs, count int64, err error) {
|
|
||||||
result, err = l.Offset(offset).Limit(limit).Find()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
|
||||||
count = int64(size + offset)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err = l.Offset(-1).Limit(-1).Count()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
|
||||||
count, err = l.Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.Offset(offset).Limit(limit).Scan(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Scan(result interface{}) (err error) {
|
|
||||||
return l.DO.Scan(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamDrawLogsDo) Delete(models ...*model.LivestreamDrawLogs) (result gen.ResultInfo, err error) {
|
|
||||||
return l.DO.Delete(models)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamDrawLogsDo) withDO(do gen.Dao) *livestreamDrawLogsDo {
|
|
||||||
l.DO = *do.(*gen.DO)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
@ -1,368 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
|
|
||||||
"gorm.io/gen"
|
|
||||||
"gorm.io/gen/field"
|
|
||||||
|
|
||||||
"gorm.io/plugin/dbresolver"
|
|
||||||
|
|
||||||
"bindbox-game/internal/repository/mysql/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newLivestreamPrizes(db *gorm.DB, opts ...gen.DOOption) livestreamPrizes {
|
|
||||||
_livestreamPrizes := livestreamPrizes{}
|
|
||||||
|
|
||||||
_livestreamPrizes.livestreamPrizesDo.UseDB(db, opts...)
|
|
||||||
_livestreamPrizes.livestreamPrizesDo.UseModel(&model.LivestreamPrizes{})
|
|
||||||
|
|
||||||
tableName := _livestreamPrizes.livestreamPrizesDo.TableName()
|
|
||||||
_livestreamPrizes.ALL = field.NewAsterisk(tableName)
|
|
||||||
_livestreamPrizes.ID = field.NewInt64(tableName, "id")
|
|
||||||
_livestreamPrizes.ActivityID = field.NewInt64(tableName, "activity_id")
|
|
||||||
_livestreamPrizes.Name = field.NewString(tableName, "name")
|
|
||||||
_livestreamPrizes.Image = field.NewString(tableName, "image")
|
|
||||||
_livestreamPrizes.Weight = field.NewInt32(tableName, "weight")
|
|
||||||
_livestreamPrizes.Quantity = field.NewInt32(tableName, "quantity")
|
|
||||||
_livestreamPrizes.Remaining = field.NewInt32(tableName, "remaining")
|
|
||||||
_livestreamPrizes.Level = field.NewInt32(tableName, "level")
|
|
||||||
_livestreamPrizes.ProductID = field.NewInt64(tableName, "product_id")
|
|
||||||
_livestreamPrizes.Sort = field.NewInt32(tableName, "sort")
|
|
||||||
_livestreamPrizes.CreatedAt = field.NewTime(tableName, "created_at")
|
|
||||||
_livestreamPrizes.UpdatedAt = field.NewTime(tableName, "updated_at")
|
|
||||||
_livestreamPrizes.CostPrice = field.NewInt64(tableName, "cost_price")
|
|
||||||
|
|
||||||
_livestreamPrizes.fillFieldMap()
|
|
||||||
|
|
||||||
return _livestreamPrizes
|
|
||||||
}
|
|
||||||
|
|
||||||
// livestreamPrizes 直播间奖品表
|
|
||||||
type livestreamPrizes struct {
|
|
||||||
livestreamPrizesDo
|
|
||||||
|
|
||||||
ALL field.Asterisk
|
|
||||||
ID field.Int64 // 主键ID
|
|
||||||
ActivityID field.Int64 // 关联livestream_activities.id
|
|
||||||
Name field.String // 奖品名称
|
|
||||||
Image field.String // 奖品图片
|
|
||||||
Weight field.Int32 // 抽奖权重
|
|
||||||
Quantity field.Int32 // 库存数量(-1=无限)
|
|
||||||
Remaining field.Int32 // 剩余数量
|
|
||||||
Level field.Int32 // 奖品等级
|
|
||||||
ProductID field.Int64 // 关联系统商品ID
|
|
||||||
Sort field.Int32 // 排序
|
|
||||||
CreatedAt field.Time // 创建时间
|
|
||||||
UpdatedAt field.Time // 更新时间
|
|
||||||
CostPrice field.Int64 // 成本价(分)
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizes) Table(newTableName string) *livestreamPrizes {
|
|
||||||
l.livestreamPrizesDo.UseTable(newTableName)
|
|
||||||
return l.updateTableName(newTableName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizes) As(alias string) *livestreamPrizes {
|
|
||||||
l.livestreamPrizesDo.DO = *(l.livestreamPrizesDo.As(alias).(*gen.DO))
|
|
||||||
return l.updateTableName(alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamPrizes) updateTableName(table string) *livestreamPrizes {
|
|
||||||
l.ALL = field.NewAsterisk(table)
|
|
||||||
l.ID = field.NewInt64(table, "id")
|
|
||||||
l.ActivityID = field.NewInt64(table, "activity_id")
|
|
||||||
l.Name = field.NewString(table, "name")
|
|
||||||
l.Image = field.NewString(table, "image")
|
|
||||||
l.Weight = field.NewInt32(table, "weight")
|
|
||||||
l.Quantity = field.NewInt32(table, "quantity")
|
|
||||||
l.Remaining = field.NewInt32(table, "remaining")
|
|
||||||
l.Level = field.NewInt32(table, "level")
|
|
||||||
l.ProductID = field.NewInt64(table, "product_id")
|
|
||||||
l.Sort = field.NewInt32(table, "sort")
|
|
||||||
l.CreatedAt = field.NewTime(table, "created_at")
|
|
||||||
l.UpdatedAt = field.NewTime(table, "updated_at")
|
|
||||||
l.CostPrice = field.NewInt64(table, "cost_price")
|
|
||||||
|
|
||||||
l.fillFieldMap()
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamPrizes) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|
||||||
_f, ok := l.fieldMap[fieldName]
|
|
||||||
if !ok || _f == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
_oe, ok := _f.(field.OrderExpr)
|
|
||||||
return _oe, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamPrizes) fillFieldMap() {
|
|
||||||
l.fieldMap = make(map[string]field.Expr, 13)
|
|
||||||
l.fieldMap["id"] = l.ID
|
|
||||||
l.fieldMap["activity_id"] = l.ActivityID
|
|
||||||
l.fieldMap["name"] = l.Name
|
|
||||||
l.fieldMap["image"] = l.Image
|
|
||||||
l.fieldMap["weight"] = l.Weight
|
|
||||||
l.fieldMap["quantity"] = l.Quantity
|
|
||||||
l.fieldMap["remaining"] = l.Remaining
|
|
||||||
l.fieldMap["level"] = l.Level
|
|
||||||
l.fieldMap["product_id"] = l.ProductID
|
|
||||||
l.fieldMap["sort"] = l.Sort
|
|
||||||
l.fieldMap["created_at"] = l.CreatedAt
|
|
||||||
l.fieldMap["updated_at"] = l.UpdatedAt
|
|
||||||
l.fieldMap["cost_price"] = l.CostPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizes) clone(db *gorm.DB) livestreamPrizes {
|
|
||||||
l.livestreamPrizesDo.ReplaceConnPool(db.Statement.ConnPool)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizes) replaceDB(db *gorm.DB) livestreamPrizes {
|
|
||||||
l.livestreamPrizesDo.ReplaceDB(db)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
type livestreamPrizesDo struct{ gen.DO }
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Debug() *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Debug())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) WithContext(ctx context.Context) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.WithContext(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) ReadDB() *livestreamPrizesDo {
|
|
||||||
return l.Clauses(dbresolver.Read)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) WriteDB() *livestreamPrizesDo {
|
|
||||||
return l.Clauses(dbresolver.Write)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Session(config *gorm.Session) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Session(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Clauses(conds ...clause.Expression) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Clauses(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Returning(value interface{}, columns ...string) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Returning(value, columns...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Not(conds ...gen.Condition) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Not(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Or(conds ...gen.Condition) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Or(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Select(conds ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Select(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Where(conds ...gen.Condition) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Where(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Order(conds ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Order(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Distinct(cols ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Distinct(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Omit(cols ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Omit(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Join(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Join(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) LeftJoin(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.LeftJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) RightJoin(table schema.Tabler, on ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.RightJoin(table, on...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Group(cols ...field.Expr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Group(cols...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Having(conds ...gen.Condition) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Having(conds...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Limit(limit int) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Limit(limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Offset(offset int) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Offset(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Scopes(funcs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Unscoped() *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Unscoped())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Create(values ...*model.LivestreamPrizes) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Create(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) CreateInBatches(values []*model.LivestreamPrizes, batchSize int) error {
|
|
||||||
return l.DO.CreateInBatches(values, batchSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save : !!! underlying implementation is different with GORM
|
|
||||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
|
||||||
func (l livestreamPrizesDo) Save(values ...*model.LivestreamPrizes) error {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return l.DO.Save(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) First() (*model.LivestreamPrizes, error) {
|
|
||||||
if result, err := l.DO.First(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamPrizes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Take() (*model.LivestreamPrizes, error) {
|
|
||||||
if result, err := l.DO.Take(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamPrizes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Last() (*model.LivestreamPrizes, error) {
|
|
||||||
if result, err := l.DO.Last(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamPrizes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Find() ([]*model.LivestreamPrizes, error) {
|
|
||||||
result, err := l.DO.Find()
|
|
||||||
return result.([]*model.LivestreamPrizes), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.LivestreamPrizes, err error) {
|
|
||||||
buf := make([]*model.LivestreamPrizes, 0, batchSize)
|
|
||||||
err = l.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
|
||||||
defer func() { results = append(results, buf...) }()
|
|
||||||
return fc(tx, batch)
|
|
||||||
})
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) FindInBatches(result *[]*model.LivestreamPrizes, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
|
||||||
return l.DO.FindInBatches(result, batchSize, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Attrs(attrs ...field.AssignExpr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Attrs(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Assign(attrs ...field.AssignExpr) *livestreamPrizesDo {
|
|
||||||
return l.withDO(l.DO.Assign(attrs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Joins(fields ...field.RelationField) *livestreamPrizesDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Joins(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Preload(fields ...field.RelationField) *livestreamPrizesDo {
|
|
||||||
for _, _f := range fields {
|
|
||||||
l = *l.withDO(l.DO.Preload(_f))
|
|
||||||
}
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) FirstOrInit() (*model.LivestreamPrizes, error) {
|
|
||||||
if result, err := l.DO.FirstOrInit(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamPrizes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) FirstOrCreate() (*model.LivestreamPrizes, error) {
|
|
||||||
if result, err := l.DO.FirstOrCreate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return result.(*model.LivestreamPrizes), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) FindByPage(offset int, limit int) (result []*model.LivestreamPrizes, count int64, err error) {
|
|
||||||
result, err = l.Offset(offset).Limit(limit).Find()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
|
||||||
count = int64(size + offset)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
count, err = l.Offset(-1).Limit(-1).Count()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
|
||||||
count, err = l.Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.Offset(offset).Limit(limit).Scan(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Scan(result interface{}) (err error) {
|
|
||||||
return l.DO.Scan(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l livestreamPrizesDo) Delete(models ...*model.LivestreamPrizes) (result gen.ResultInfo, err error) {
|
|
||||||
return l.DO.Delete(models)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *livestreamPrizesDo) withDO(do gen.Dao) *livestreamPrizesDo {
|
|
||||||
l.DO = *do.(*gen.DO)
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
@ -51,7 +51,7 @@ func newShippingRecords(db *gorm.DB, opts ...gen.DOOption) shippingRecords {
|
|||||||
return _shippingRecords
|
return _shippingRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
// shippingRecords 发货记录表
|
// shippingRecords 发货记录(合并:单表)
|
||||||
type shippingRecords struct {
|
type shippingRecords struct {
|
||||||
shippingRecordsDo
|
shippingRecordsDo
|
||||||
|
|
||||||
|
|||||||
@ -32,8 +32,6 @@ func newSystemConfigs(db *gorm.DB, opts ...gen.DOOption) systemConfigs {
|
|||||||
_systemConfigs.UpdatedAt = field.NewTime(tableName, "updated_at")
|
_systemConfigs.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||||
_systemConfigs.DeletedAt = field.NewField(tableName, "deleted_at")
|
_systemConfigs.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||||
_systemConfigs.ConfigKey = field.NewString(tableName, "config_key")
|
_systemConfigs.ConfigKey = field.NewString(tableName, "config_key")
|
||||||
_systemConfigs.ConfigGroup = field.NewString(tableName, "config_group")
|
|
||||||
_systemConfigs.IsEncrypted = field.NewBool(tableName, "is_encrypted")
|
|
||||||
_systemConfigs.ConfigValue = field.NewString(tableName, "config_value")
|
_systemConfigs.ConfigValue = field.NewString(tableName, "config_value")
|
||||||
_systemConfigs.Remark = field.NewString(tableName, "remark")
|
_systemConfigs.Remark = field.NewString(tableName, "remark")
|
||||||
|
|
||||||
@ -52,8 +50,6 @@ type systemConfigs struct {
|
|||||||
UpdatedAt field.Time
|
UpdatedAt field.Time
|
||||||
DeletedAt field.Field
|
DeletedAt field.Field
|
||||||
ConfigKey field.String
|
ConfigKey field.String
|
||||||
ConfigGroup field.String
|
|
||||||
IsEncrypted field.Bool
|
|
||||||
ConfigValue field.String
|
ConfigValue field.String
|
||||||
Remark field.String
|
Remark field.String
|
||||||
|
|
||||||
@ -77,8 +73,6 @@ func (s *systemConfigs) updateTableName(table string) *systemConfigs {
|
|||||||
s.UpdatedAt = field.NewTime(table, "updated_at")
|
s.UpdatedAt = field.NewTime(table, "updated_at")
|
||||||
s.DeletedAt = field.NewField(table, "deleted_at")
|
s.DeletedAt = field.NewField(table, "deleted_at")
|
||||||
s.ConfigKey = field.NewString(table, "config_key")
|
s.ConfigKey = field.NewString(table, "config_key")
|
||||||
s.ConfigGroup = field.NewString(table, "config_group")
|
|
||||||
s.IsEncrypted = field.NewBool(table, "is_encrypted")
|
|
||||||
s.ConfigValue = field.NewString(table, "config_value")
|
s.ConfigValue = field.NewString(table, "config_value")
|
||||||
s.Remark = field.NewString(table, "remark")
|
s.Remark = field.NewString(table, "remark")
|
||||||
|
|
||||||
@ -97,14 +91,12 @@ func (s *systemConfigs) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemConfigs) fillFieldMap() {
|
func (s *systemConfigs) fillFieldMap() {
|
||||||
s.fieldMap = make(map[string]field.Expr, 9)
|
s.fieldMap = make(map[string]field.Expr, 7)
|
||||||
s.fieldMap["id"] = s.ID
|
s.fieldMap["id"] = s.ID
|
||||||
s.fieldMap["created_at"] = s.CreatedAt
|
s.fieldMap["created_at"] = s.CreatedAt
|
||||||
s.fieldMap["updated_at"] = s.UpdatedAt
|
s.fieldMap["updated_at"] = s.UpdatedAt
|
||||||
s.fieldMap["deleted_at"] = s.DeletedAt
|
s.fieldMap["deleted_at"] = s.DeletedAt
|
||||||
s.fieldMap["config_key"] = s.ConfigKey
|
s.fieldMap["config_key"] = s.ConfigKey
|
||||||
s.fieldMap["config_group"] = s.ConfigGroup
|
|
||||||
s.fieldMap["is_encrypted"] = s.IsEncrypted
|
|
||||||
s.fieldMap["config_value"] = s.ConfigValue
|
s.fieldMap["config_value"] = s.ConfigValue
|
||||||
s.fieldMap["remark"] = s.Remark
|
s.fieldMap["remark"] = s.Remark
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,6 @@ func newUsers(db *gorm.DB, opts ...gen.DOOption) users {
|
|||||||
_users.Status = field.NewInt32(tableName, "status")
|
_users.Status = field.NewInt32(tableName, "status")
|
||||||
_users.DouyinID = field.NewString(tableName, "douyin_id")
|
_users.DouyinID = field.NewString(tableName, "douyin_id")
|
||||||
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
_users.ChannelID = field.NewInt64(tableName, "channel_id")
|
||||||
_users.DouyinUserID = field.NewString(tableName, "douyin_user_id")
|
|
||||||
_users.Remark = field.NewString(tableName, "remark")
|
|
||||||
|
|
||||||
_users.fillFieldMap()
|
_users.fillFieldMap()
|
||||||
|
|
||||||
@ -68,8 +66,6 @@ type users struct {
|
|||||||
Status field.Int32 // 状态:1正常 2禁用
|
Status field.Int32 // 状态:1正常 2禁用
|
||||||
DouyinID field.String
|
DouyinID field.String
|
||||||
ChannelID field.Int64 // 渠道ID
|
ChannelID field.Int64 // 渠道ID
|
||||||
DouyinUserID field.String
|
|
||||||
Remark field.String // 管ç†å‘˜å¤‡æ³¨
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@ -100,8 +96,6 @@ func (u *users) updateTableName(table string) *users {
|
|||||||
u.Status = field.NewInt32(table, "status")
|
u.Status = field.NewInt32(table, "status")
|
||||||
u.DouyinID = field.NewString(table, "douyin_id")
|
u.DouyinID = field.NewString(table, "douyin_id")
|
||||||
u.ChannelID = field.NewInt64(table, "channel_id")
|
u.ChannelID = field.NewInt64(table, "channel_id")
|
||||||
u.DouyinUserID = field.NewString(table, "douyin_user_id")
|
|
||||||
u.Remark = field.NewString(table, "remark")
|
|
||||||
|
|
||||||
u.fillFieldMap()
|
u.fillFieldMap()
|
||||||
|
|
||||||
@ -118,7 +112,7 @@ func (u *users) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *users) fillFieldMap() {
|
func (u *users) fillFieldMap() {
|
||||||
u.fieldMap = make(map[string]field.Expr, 16)
|
u.fieldMap = make(map[string]field.Expr, 14)
|
||||||
u.fieldMap["id"] = u.ID
|
u.fieldMap["id"] = u.ID
|
||||||
u.fieldMap["created_at"] = u.CreatedAt
|
u.fieldMap["created_at"] = u.CreatedAt
|
||||||
u.fieldMap["updated_at"] = u.UpdatedAt
|
u.fieldMap["updated_at"] = u.UpdatedAt
|
||||||
@ -133,8 +127,6 @@ func (u *users) fillFieldMap() {
|
|||||||
u.fieldMap["status"] = u.Status
|
u.fieldMap["status"] = u.Status
|
||||||
u.fieldMap["douyin_id"] = u.DouyinID
|
u.fieldMap["douyin_id"] = u.DouyinID
|
||||||
u.fieldMap["channel_id"] = u.ChannelID
|
u.fieldMap["channel_id"] = u.ChannelID
|
||||||
u.fieldMap["douyin_user_id"] = u.DouyinUserID
|
|
||||||
u.fieldMap["remark"] = u.Remark
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u users) clone(db *gorm.DB) users {
|
func (u users) clone(db *gorm.DB) users {
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TableNameDouyinBlacklist = "douyin_blacklist"
|
|
||||||
|
|
||||||
// DouyinBlacklist 抖音用户黑名单表
|
|
||||||
type DouyinBlacklist struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
|
||||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖音用户ID" json:"douyin_user_id"` // 抖音用户ID
|
|
||||||
Reason string `gorm:"column:reason;comment:拉黑原因" json:"reason"` // 拉黑原因
|
|
||||||
OperatorID int64 `gorm:"column:operator_id;comment:操作人ID" json:"operator_id"` // 操作人ID
|
|
||||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=生效, 0=已解除" json:"status"` // 状态: 1=生效, 0=已解除
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName DouyinBlacklist's table name
|
|
||||||
func (*DouyinBlacklist) TableName() string {
|
|
||||||
return TableNameDouyinBlacklist
|
|
||||||
}
|
|
||||||
@ -14,20 +14,16 @@ const TableNameDouyinOrders = "douyin_orders"
|
|||||||
type DouyinOrders struct {
|
type DouyinOrders struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||||
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
ShopOrderID string `gorm:"column:shop_order_id;not null;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
||||||
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联商品ID" json:"douyin_product_id"` // 关联商品ID
|
|
||||||
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
OrderStatus int32 `gorm:"column:order_status;not null;comment:订单状态: 5=已完成" json:"order_status"` // 订单状态: 5=已完成
|
||||||
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
DouyinUserID string `gorm:"column:douyin_user_id;not null;comment:抖店用户ID" json:"douyin_user_id"` // 抖店用户ID
|
||||||
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
LocalUserID string `gorm:"column:local_user_id;default:0;comment:匹配到的本地用户ID" json:"local_user_id"` // 匹配到的本地用户ID
|
||||||
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
ActualReceiveAmount int64 `gorm:"column:actual_receive_amount;comment:实收金额(分)" json:"actual_receive_amount"` // 实收金额(分)
|
||||||
ActualPayAmount int64 `gorm:"column:actual_pay_amount;comment:实付金额(分)" json:"actual_pay_amount"` // 实付金额(分)
|
|
||||||
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
PayTypeDesc string `gorm:"column:pay_type_desc;comment:支付方式描述" json:"pay_type_desc"` // 支付方式描述
|
||||||
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
|
||||||
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
UserNickname string `gorm:"column:user_nickname;comment:抖音昵称" json:"user_nickname"` // 抖音昵称
|
||||||
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
RawData string `gorm:"column:raw_data;comment:原始响应数据" json:"raw_data"` // 原始响应数据
|
||||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
||||||
RewardGranted int32 `gorm:"column:reward_granted;not null;comment:奖励已发放: 0=否, 1=是" json:"reward_granted"` // 奖励已发放: 0=否, 1=是
|
|
||||||
ProductCount int32 `gorm:"column:product_count;not null;default:1;comment:商品数量" json:"product_count"` // 商品数量
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName DouyinOrders's table name
|
// TableName DouyinOrders's table name
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TableNameDouyinProductRewards = "douyin_product_rewards"
|
|
||||||
|
|
||||||
// DouyinProductRewards 抖店商品奖励规则
|
|
||||||
type DouyinProductRewards struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
|
||||||
ProductID string `gorm:"column:product_id;not null;comment:抖店商品ID" json:"product_id"` // 抖店商品ID
|
|
||||||
ProductName string `gorm:"column:product_name;not null;comment:商品名称" json:"product_name"` // 商品名称
|
|
||||||
ActivityID int64 `gorm:"column:activity_id;comment:关联直播活动ID" json:"activity_id"` // 关联直播活动ID (可选)
|
|
||||||
RewardType string `gorm:"column:reward_type;not null;comment:奖励类型" json:"reward_type"` // 奖励类型
|
|
||||||
RewardPayload string `gorm:"column:reward_payload;comment:奖励参数JSON" json:"reward_payload"` // 奖励参数JSON
|
|
||||||
Quantity int32 `gorm:"column:quantity;not null;default:1;comment:发放数量" json:"quantity"` // 发放数量
|
|
||||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态: 1=启用 0=禁用" json:"status"` // 状态: 1=启用 0=禁用
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName DouyinProductRewards's table name
|
|
||||||
func (*DouyinProductRewards) TableName() string {
|
|
||||||
return TableNameDouyinProductRewards
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TableNameLivestreamActivities = "livestream_activities"
|
|
||||||
|
|
||||||
// LivestreamActivities 直播间活动表
|
|
||||||
type LivestreamActivities struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
|
||||||
Name string `gorm:"column:name;not null;comment:活动名称" json:"name"` // 活动名称
|
|
||||||
StreamerName string `gorm:"column:streamer_name;comment:主播名称" json:"streamer_name"` // 主播名称
|
|
||||||
StreamerContact string `gorm:"column:streamer_contact;comment:主播联系方式" json:"streamer_contact"` // 主播联系方式
|
|
||||||
AccessCode string `gorm:"column:access_code;not null;comment:唯一访问码" json:"access_code"` // 唯一访问码
|
|
||||||
DouyinProductID string `gorm:"column:douyin_product_id;comment:关联抖店商品ID" json:"douyin_product_id"` // 关联抖店商品ID
|
|
||||||
OrderRewardType string `gorm:"column:order_reward_type;default:'';comment:下单奖励类型: flip_card/minesweeper" json:"order_reward_type"` // 下单奖励类型
|
|
||||||
OrderRewardQuantity int32 `gorm:"column:order_reward_quantity;default:1;comment:下单奖励数量: 1-100" json:"order_reward_quantity"` // 下单奖励数量
|
|
||||||
Status int32 `gorm:"column:status;not null;default:1;comment:状态:1进行中 2已结束" json:"status"` // 状态:1进行中 2已结束
|
|
||||||
CommitmentAlgo string `gorm:"column:commitment_algo;default:commit-v1;comment:承诺算法版本" json:"commitment_algo"` // 承诺算法版本
|
|
||||||
CommitmentSeedMaster []byte `gorm:"column:commitment_seed_master;comment:主种子(32字节)" json:"commitment_seed_master"` // 主种子(32字节)
|
|
||||||
CommitmentSeedHash []byte `gorm:"column:commitment_seed_hash;comment:种子SHA256哈希" json:"commitment_seed_hash"` // 种子SHA256哈希
|
|
||||||
CommitmentStateVersion int32 `gorm:"column:commitment_state_version;comment:状态版本" json:"commitment_state_version"` // 状态版本
|
|
||||||
StartTime time.Time `gorm:"column:start_time;comment:开始时间" json:"start_time"` // 开始时间
|
|
||||||
EndTime time.Time `gorm:"column:end_time;comment:结束时间" json:"end_time"` // 结束时间
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;comment:删除时间" json:"deleted_at"` // 删除时间
|
|
||||||
TicketPrice int32 `gorm:"column:ticket_price" json:"ticket_price"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName LivestreamActivities's table name
|
|
||||||
func (*LivestreamActivities) TableName() string {
|
|
||||||
return TableNameLivestreamActivities
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TableNameLivestreamDrawLogs = "livestream_draw_logs"
|
|
||||||
|
|
||||||
// LivestreamDrawLogs 直播间中奖记录表
|
|
||||||
type LivestreamDrawLogs struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
|
||||||
ActivityID int64 `gorm:"column:activity_id;not null;comment:关联livestream_activities.id" json:"activity_id"` // 关联livestream_activities.id
|
|
||||||
PrizeID int64 `gorm:"column:prize_id;not null;comment:关联livestream_prizes.id" json:"prize_id"` // 关联livestream_prizes.id
|
|
||||||
DouyinOrderID int64 `gorm:"column:douyin_order_id;comment:关联douyin_orders.id" json:"douyin_order_id"` // 关联douyin_orders.id
|
|
||||||
ShopOrderID string `gorm:"column:shop_order_id;comment:抖店订单号" json:"shop_order_id"` // 抖店订单号
|
|
||||||
LocalUserID int64 `gorm:"column:local_user_id;comment:本地用户ID" json:"local_user_id"` // 本地用户ID
|
|
||||||
DouyinUserID string `gorm:"column:douyin_user_id;comment:抖音用户ID" json:"douyin_user_id"` // 抖音用户ID
|
|
||||||
UserNickname string `gorm:"column:user_nickname;comment:用户昵称" json:"user_nickname"` // 用户昵称
|
|
||||||
PrizeName string `gorm:"column:prize_name;comment:中奖奖品名称快照" json:"prize_name"` // 中奖奖品名称快照
|
|
||||||
Level int32 `gorm:"column:level;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
|
||||||
SeedHash string `gorm:"column:seed_hash;comment:哈希种子" json:"seed_hash"` // 哈希种子
|
|
||||||
RandValue int64 `gorm:"column:rand_value;comment:随机值" json:"rand_value"` // 随机值
|
|
||||||
WeightsTotal int64 `gorm:"column:weights_total;comment:权重总和" json:"weights_total"` // 权重总和
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:中奖时间" json:"created_at"` // 中奖时间
|
|
||||||
IsGranted bool `gorm:"column:is_granted;comment:是否已发放奖品" json:"is_granted"` // 是否已发放奖品
|
|
||||||
IsRefunded int32 `gorm:"column:is_refunded;comment:订单是否已退款" json:"is_refunded"` // 订单是否已退款
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName LivestreamDrawLogs's table name
|
|
||||||
func (*LivestreamDrawLogs) TableName() string {
|
|
||||||
return TableNameLivestreamDrawLogs
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TableNameLivestreamPrizes = "livestream_prizes"
|
|
||||||
|
|
||||||
// LivestreamPrizes 直播间奖品表
|
|
||||||
type LivestreamPrizes struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
|
|
||||||
ActivityID int64 `gorm:"column:activity_id;not null;comment:关联livestream_activities.id" json:"activity_id"` // 关联livestream_activities.id
|
|
||||||
Name string `gorm:"column:name;not null;comment:奖品名称" json:"name"` // 奖品名称
|
|
||||||
Image string `gorm:"column:image;comment:奖品图片" json:"image"` // 奖品图片
|
|
||||||
Weight int32 `gorm:"column:weight;not null;default:1;comment:抽奖权重" json:"weight"` // 抽奖权重
|
|
||||||
Quantity int32 `gorm:"column:quantity;not null;default:-1;comment:库存数量(-1=无限)" json:"quantity"` // 库存数量(-1=无限)
|
|
||||||
Remaining int32 `gorm:"column:remaining;not null;default:-1;comment:剩余数量" json:"remaining"` // 剩余数量
|
|
||||||
Level int32 `gorm:"column:level;not null;default:1;comment:奖品等级" json:"level"` // 奖品等级
|
|
||||||
ProductID int64 `gorm:"column:product_id;comment:关联系统商品ID" json:"product_id"` // 关联系统商品ID
|
|
||||||
Sort int32 `gorm:"column:sort;not null;comment:排序" json:"sort"` // 排序
|
|
||||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
|
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3);comment:更新时间" json:"updated_at"` // 更新时间
|
|
||||||
CostPrice int64 `gorm:"column:cost_price;comment:成本价(分)" json:"cost_price"` // 成本价(分)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName LivestreamPrizes's table name
|
|
||||||
func (*LivestreamPrizes) TableName() string {
|
|
||||||
return TableNameLivestreamPrizes
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user