测试库不再使用 SQLite 内存库,改为每个测试包独立的 MySQL 库, 表结构由 backend/migrations 的真实迁移建立。 基础设施 - 新增 internal/database/testdb.go:NewTestDB() 保持原签名(28 处调用零改动), 内部连接 MySQL 测试库,首次调用重建库并执行全部真实迁移,之后每次调用清空全表 - 库名 = 测试库基础名 + 包目录名,同包共享、跨包隔离,使 go test ./... 可并发执行 (共用一库时各包会互相 DROP/CREATE 导致 database exists) - 新增 NewDryRunDB():MySQL 方言 + DryRun,供断言 SQL 形态的 5 处测试使用 - 新增 AutoMigrateIfMissing():只对不存在的表 AutoMigrate, 避免改动迁移定义的索引与约束 - 新增 scripts/prepare-test-db.sh:建库与通配授权,可重复执行 - 33 处 sqlite.Open 全部替换,26 个测试文件改用 MySQL - 删除 46 处手写 SQLite 方言 DDL(AUTOINCREMENT / INTEGER PRIMARY KEY / TEXT), 表统一由真实迁移建立 - go.mod 移除 gorm.io/driver/sqlite 与 mattn/go-sqlite3 - 删除生产代码里 distribution/repository.go 的 sqlite 方言分支(strftime) 对齐环境后暴露并修复的真实问题 - DATE(col) 在 MySQL 返回 DATE 类型,使 Date string 字段变成 RFC3339 时间串, 大盘日期会带出时间部分;28 处改为 DATE_FORMAT(col, '%Y-%m-%d') - 系统自动创建分销结算批次时 created_by 写入 0,违反指向 admin_users 的外键, 生产会直接插入失败;改为写 NULL(新增 adminRef helper) - MySQL DATETIME 仅到秒,静默截断 Go 侧写入的时间, 导致租用时长计算与结算时点回读比对偏差; rental_orders 的 9 个业务时间列提到 DATETIME(6) - 测试数据补齐严格模式要求的 NOT NULL 列、修正超出列长的取值、补齐外键父记录 迁移 - 000065 的 ALTER TABLE rental_orders 一并完成业务时间列精度调整, feature 侧不再单开迁移文件 脚本与文档 - check.sh / check-backend-coverage.sh 去掉 -p 1 串行与 SQLite 相关注释 - 更新 docs/Repository层测试补充总结.md 的测试运行说明
193 lines
6.5 KiB
Go
193 lines
6.5 KiB
Go
package dispute
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
|
conditions := r.userVisibleQuery(ctx, userID)
|
|
var total int64
|
|
if err := conditions.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
var rows []disputeRow
|
|
err := applyUserVisibleFilter(r.baseQuery(ctx), userID).
|
|
Order("d.id DESC").
|
|
Offset(offset).Limit(pageSize).
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
|
}
|
|
|
|
func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64) (*DisputeDTO, error) {
|
|
var row disputeRow
|
|
if err := applyUserVisibleFilter(r.baseQuery(ctx).Where("d.id = ?", id), userID).
|
|
First(&row).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
dto := row.toDTO()
|
|
return &dto, nil
|
|
}
|
|
|
|
func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*PaginatedResult, error) {
|
|
page := query.Page
|
|
pageSize := query.PageSize
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
|
|
var rows []disputeRow
|
|
err := applyAdminListFilters(r.baseQuery(ctx), query).
|
|
Order("d.id DESC").
|
|
Offset(offset).
|
|
Limit(pageSize).
|
|
Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var total int64
|
|
// 首页查询且返回条数小于 pageSize 时,总数等于当前返回条数,彻底免除第二次 COUNT(*) 扫描
|
|
if page == 1 && len(rows) < pageSize {
|
|
total = int64(len(rows))
|
|
} else {
|
|
countDB := applyAdminListFilters(r.adminDisputeCountQuery(ctx, query), query)
|
|
if err := countDB.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
|
}
|
|
|
|
func (r *Repository) FindAdminByID(ctx context.Context, id uint64) (*DisputeDTO, error) {
|
|
var row disputeRow
|
|
if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
dto := row.toDTO()
|
|
return &dto, nil
|
|
}
|
|
|
|
func (r *Repository) userVisibleQuery(ctx context.Context, userID uint64) *gorm.DB {
|
|
return applyUserVisibleFilter(r.adminFilterQuery(ctx), userID)
|
|
}
|
|
|
|
func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB {
|
|
return db.Where(
|
|
`((d.initiator_type = ? AND d.initiator_id = ?) OR d.target_user_id = ? OR (d.initiator_type = ? AND (o.owner_id = ? OR o.renter_id = ?)))`,
|
|
disputeInitiatorUser,
|
|
userID,
|
|
userID,
|
|
disputeInitiatorAdmin,
|
|
userID,
|
|
userID,
|
|
)
|
|
}
|
|
|
|
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
|
return r.adminFilterQuery(ctx).
|
|
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
|
o.owner_id, o.renter_id, o.extra_item_original_amount_cent,
|
|
owner.phone AS owner_phone, renter.phone AS renter_phone,
|
|
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m,
|
|
c.consumable_amount_cent AS checkout_consumable_amount_cent,
|
|
CASE WHEN c.deposit_deduct_amount_cent > 0 THEN c.deposit_deduct_amount_cent ELSE c.other_amount_cent END AS checkout_deposit_deduct_amount_cent`)
|
|
}
|
|
|
|
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
|
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
|
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
|
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
|
|
Joins("JOIN users AS owner ON owner.id = o.owner_id").
|
|
Joins("JOIN users AS renter ON renter.id = o.renter_id").
|
|
Joins("LEFT JOIN order_checkouts AS c ON c.id = d.checkout_id")
|
|
}
|
|
|
|
func (r *Repository) adminDisputeCountQuery(ctx context.Context, query AdminListQuery) *gorm.DB {
|
|
kw := strings.TrimSpace(query.Keyword)
|
|
if kw == "" {
|
|
return r.db.WithContext(ctx).Table("disputes AS d")
|
|
}
|
|
upper := strings.ToUpper(kw)
|
|
if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && id < 100000000 {
|
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id")
|
|
}
|
|
if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
|
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id")
|
|
}
|
|
if strings.HasPrefix(upper, "L") || (len(kw) >= 12 && isAllDigits(kw)) {
|
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
|
Joins("JOIN rental_listings AS l ON l.id = o.listing_id")
|
|
}
|
|
if len(kw) == 11 && kw[0] == '1' && isAllDigits(kw) {
|
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
|
Joins("JOIN users AS owner ON owner.id = o.owner_id").
|
|
Joins("JOIN users AS renter ON renter.id = o.renter_id")
|
|
}
|
|
return r.adminFilterQuery(ctx)
|
|
}
|
|
|
|
func applyAdminListFilters(db *gorm.DB, query AdminListQuery) *gorm.DB {
|
|
if query.Status != "" {
|
|
db = db.Where("d.status = ?", query.Status)
|
|
}
|
|
if query.Type != "" {
|
|
db = db.Where("d.type = ?", query.Type)
|
|
}
|
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
|
upper := strings.ToUpper(keyword)
|
|
if len(keyword) == 11 && keyword[0] == '1' && isAllDigits(keyword) {
|
|
// 11 位手机号:命中 users(phone) 唯一索引
|
|
db = db.Where("(owner.phone = ? OR renter.phone = ?)", keyword, keyword)
|
|
} else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
|
|
// 订单号:命中 rental_orders(order_no) 唯一索引
|
|
db = db.Where("o.order_no = ?", keyword)
|
|
} else if strings.HasPrefix(upper, "L") || (len(keyword) >= 12 && isAllDigits(keyword)) {
|
|
// 商品编号(如 L前缀或时间计算生成的 12位纯数字如 202609040184):命中 rental_listings(listing_no) 唯一索引
|
|
db = db.Where("l.listing_no = ?", keyword)
|
|
} else if id, err := strconv.ParseUint(keyword, 10, 64); err == nil && id > 0 && keyword[0] != '0' && id < 100000000 {
|
|
// 短纯数字 ID(非零开头):命中纠纷 ID、订单 ID 或关联 ID
|
|
db = db.Where("(d.id = ? OR d.order_id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)", id, id, id, id, id)
|
|
} else {
|
|
like := "%" + keyword + "%"
|
|
db = db.Where(
|
|
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ?)`,
|
|
like,
|
|
like,
|
|
like,
|
|
like,
|
|
like,
|
|
)
|
|
}
|
|
}
|
|
return db
|
|
}
|
|
|
|
func isAllDigits(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|