Files
hfb_sys/backend/internal/modules/distribution/service.go
T
yml2213 cbd5412315 test: 后端测试从 SQLite 全面切换到 MySQL,与生产环境对齐
测试库不再使用 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 的测试运行说明
2026-09-10 14:34:01 +08:00

307 lines
9.7 KiB
Go

package distribution
import (
"context"
"errors"
"hfb_sys/backend/internal/modules/auth"
)
var ErrDependencyUnavailable = errors.New("distribution dependency unavailable")
type Service struct {
repo *Repository
jwt *auth.JWTManager
}
func NewService(repo *Repository, jwt ...*auth.JWTManager) *Service {
s := &Service{repo: repo}
if len(jwt) > 0 {
s.jwt = jwt[0]
}
return s
}
func (s *Service) available() error {
if s == nil || s.repo == nil {
return ErrDependencyUnavailable
}
return nil
}
func (s *Service) ListSites(ctx context.Context) ([]SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListSites(ctx)
}
func (s *Service) ListSitesForAdmin(ctx context.Context, adminID uint64) ([]SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
items, err := s.repo.ListSites(ctx)
if err != nil {
return nil, err
}
global, err := s.repo.hasGlobalDistributionAccess(ctx, adminID)
if err != nil {
return nil, err
}
if global {
return items, nil
}
siteIDs, err := s.repo.listActiveMemberSiteIDs(ctx, adminID)
if err != nil {
return nil, err
}
if len(siteIDs) == 0 {
return []SiteDTO{}, nil
}
allowed := make(map[uint64]struct{}, len(siteIDs))
for _, siteID := range siteIDs {
allowed[siteID] = struct{}{}
}
filtered := make([]SiteDTO, 0, len(allowed))
for _, item := range items {
if _, ok := allowed[item.ID]; ok {
filtered = append(filtered, item)
}
}
return filtered, nil
}
func (s *Service) ScopeSiteID(ctx context.Context, adminID, requested uint64) (uint64, error) {
if err := s.available(); err != nil {
return 0, err
}
return s.repo.ScopeSiteID(ctx, adminID, requested)
}
func (s *Service) FindSite(ctx context.Context, adminID, id uint64) (*SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.FindSite(ctx, adminID, id)
}
func (s *Service) CreateSite(ctx context.Context, req CreateSiteRequest) (*SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.CreateSite(ctx, req)
}
func (s *Service) UpdateSite(ctx context.Context, adminID, id uint64, req UpdateSiteRequest) (*SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.UpdateSite(ctx, adminID, id, req)
}
func (s *Service) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListRules(ctx, adminID, siteID, mode)
}
func (s *Service) CreateRule(ctx context.Context, adminID uint64, req CreateRuleRequest) (*RuleDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.CreateRule(ctx, adminID, req)
}
func (s *Service) UpdateRuleStatus(ctx context.Context, adminID, id uint64, status string) (*RuleDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.UpdateRuleStatus(ctx, adminID, id, status)
}
func (s *Service) ListOrders(ctx context.Context, query DistributionOrderListQuery) (*DistributionOrderPage, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListOrders(ctx, query)
}
func (s *Service) FindOrder(ctx context.Context, adminID, id uint64) (*DistributionOrderDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.FindOrder(ctx, adminID, id)
}
func (s *Service) ListAccountDisplays(ctx context.Context, siteID uint64, status string) ([]AccountDisplayDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListAccountDisplays(ctx, siteID, status)
}
func (s *Service) UpdateAccountDisplay(ctx context.Context, adminID, id uint64, status string) (*AccountDisplayDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.UpdateAccountDisplay(ctx, adminID, id, status)
}
func (s *Service) ListPaymentChannels(ctx context.Context, siteID uint64) ([]PaymentChannelDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListPaymentChannels(ctx, siteID)
}
func (s *Service) BindPaymentChannel(ctx context.Context, siteID uint64, req BindPaymentChannelRequest) (*PaymentChannelDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.BindPaymentChannel(ctx, siteID, req)
}
func (s *Service) UnbindPaymentChannel(ctx context.Context, adminID, id uint64) error {
if err := s.available(); err != nil {
return err
}
return s.repo.UnbindPaymentChannel(ctx, adminID, id)
}
func (s *Service) ListSettlementBatches(ctx context.Context, siteID uint64) ([]SettlementBatchDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListSettlementBatches(ctx, siteID)
}
func (s *Service) CreateSettlementBatch(ctx context.Context, adminID uint64, req CreateSettlementBatchRequest) (*SettlementBatchDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.CreateSettlementBatch(ctx, adminID, req)
}
func (s *Service) ListSettlementItems(ctx context.Context, adminID, batchID uint64) ([]SettlementItemDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListSettlementItems(ctx, adminID, batchID)
}
func (s *Service) MarkSettlementBatch(ctx context.Context, adminID, id uint64) (*SettlementBatchDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.MarkSettlementBatch(ctx, adminID, id)
}
func (s *Service) CreateFinancialSnapshot(ctx context.Context, adminID, orderID uint64, req CreateFinancialSnapshotRequest) (*DistributionOrderDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.CreateFinancialSnapshot(ctx, adminID, orderID, req)
}
func (s *Service) Login(ctx context.Context, req DistributionLoginRequest, clientIP string) (*DistributionLoginResponse, error) {
if err := s.available(); err != nil {
return nil, err
}
if s.jwt == nil {
return nil, ErrDependencyUnavailable
}
admin, member, err := s.repo.LoginMember(ctx, req.Username, req.Password)
if err != nil {
return nil, err
}
pair, err := s.jwt.GenerateSubjectPairWithVersion(admin.ID, admin.Username, "admin", admin.TokenVersion)
if err != nil {
return nil, err
}
return &DistributionLoginResponse{
Token: pair.AccessToken,
RefreshToken: pair.RefreshToken,
Member: *member,
}, nil
}
func (s *Service) Me(ctx context.Context, adminID uint64) (*DistributionCurrentMemberDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.GetMemberProfile(ctx, adminID)
}
func (s *Service) OnboardSite(ctx context.Context, req OnboardSiteRequest) (*OnboardSiteResponse, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.OnboardSite(ctx, req)
}
func (s *Service) SyncSiteAccounts(ctx context.Context, adminID, siteID uint64, listingIDs []uint64) (*SyncSiteAccountsResult, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.SyncSiteAccounts(ctx, adminID, siteID, listingIDs)
}
func (s *Service) ListPublicSiteListings(ctx context.Context, siteCode string, page, pageSize int) (*PublicDistributionListingsResult, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListPublicSiteListings(ctx, siteCode, page, pageSize)
}
func (s *Service) GetPublicSiteInfo(ctx context.Context, code, domain string) (*PublicSiteInfoDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.GetPublicSiteInfo(ctx, code, domain)
}
func (s *Service) UpdateSiteBranding(ctx context.Context, adminID, siteID uint64, req UpdateSiteBrandingRequest) (*SiteDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.UpdateSiteBranding(ctx, adminID, siteID, req)
}
func (s *Service) UpdateAccountMarkup(ctx context.Context, adminID, id uint64, ratioDelta float64) (*AccountDisplayDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.UpdateAccountMarkup(ctx, adminID, id, ratioDelta)
}
func (s *Service) BatchUpdateAccountMarkup(ctx context.Context, adminID, siteID uint64, req BatchUpdateAccountMarkupRequest) error {
if err := s.available(); err != nil {
return err
}
return s.repo.BatchUpdateAccountMarkup(ctx, adminID, siteID, req)
}
func (s *Service) ListReceivedListings(ctx context.Context, adminID, siteID uint64, q ReceivedListingQuery) (*ReceivedListingPage, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListReceivedListings(ctx, adminID, siteID, q)
}
func (s *Service) UpdateReceivedListingStatus(ctx context.Context, adminID, listingID uint64, status string) error {
if err := s.available(); err != nil {
return err
}
return s.repo.UpdateReceivedListingStatus(ctx, adminID, listingID, status)
}
func (s *Service) GetSiteWallet(ctx context.Context, adminID, siteID uint64) (*SiteWalletDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.GetSiteWallet(ctx, adminID, siteID)
}
func (s *Service) ApplySiteWithdrawal(ctx context.Context, adminID, siteID uint64, req ApplySiteWithdrawalRequest) (*SiteWithdrawalDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ApplySiteWithdrawal(ctx, adminID, siteID, req)
}
func (s *Service) ListSiteWithdrawals(ctx context.Context, adminID, siteID uint64, page, size int) (*SiteWithdrawalPage, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.ListSiteWithdrawals(ctx, adminID, siteID, page, size)
}
func (s *Service) GetDashboardAnalytics(ctx context.Context, adminID, siteID uint64) (*DistributionAnalyticsDTO, error) {
if err := s.available(); err != nil {
return nil, err
}
return s.repo.GetDashboardAnalytics(ctx, adminID, siteID)
}