2026-05-29 19:56:14 +08:00

291 lines
7.7 KiB
Go

package hostsalarysettlement
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
)
const maxPageSize = 100
type Service struct {
walletDB *sql.DB
userDB *sql.DB
}
type userProfile struct {
UserID int64
DisplayUserID string
Username string
Avatar string
}
func NewService(walletDB *sql.DB, userDB *sql.DB) *Service {
return &Service{walletDB: walletDB, userDB: userDB}
}
func (s *Service) List(ctx context.Context, appCode string, req query) ([]settlementDTO, int64, error) {
if s == nil || s.walletDB == nil {
return nil, 0, fmt.Errorf("wallet mysql is not configured")
}
req = normalizeQuery(req)
whereSQL, args := settlementWhere(appCode, req)
var total int64
if err := s.walletDB.QueryRowContext(ctx, `SELECT COUNT(*) FROM host_salary_settlement_records `+whereSQL, args...).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := s.walletDB.QueryContext(ctx, `
SELECT settlement_id, command_id, transaction_id, settlement_type, user_id, agency_owner_user_id,
cycle_key, policy_id, level_no, total_diamonds, host_salary_usd_minor_delta,
host_coin_reward_delta, agency_salary_usd_minor_delta, residual_usd_minor_delta,
status, reason, created_at_ms
FROM host_salary_settlement_records
`+whereSQL+`
ORDER BY created_at_ms DESC, settlement_id DESC
LIMIT ? OFFSET ?`,
append(args, req.PageSize, offset(req.Page, req.PageSize))...,
)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]settlementDTO, 0, req.PageSize)
userIDs := make([]int64, 0, req.PageSize*2)
for rows.Next() {
var item settlementDTO
var userID int64
var agencyOwnerUserID int64
if err := rows.Scan(
&item.SettlementID,
&item.CommandID,
&item.TransactionID,
&item.SettlementType,
&userID,
&agencyOwnerUserID,
&item.CycleKey,
&item.PolicyID,
&item.LevelNo,
&item.TotalDiamonds,
&item.HostSalaryUSDMinorDelta,
&item.HostCoinRewardDelta,
&item.AgencySalaryUSDMinorDelta,
&item.ResidualUSDMinorDelta,
&item.Status,
&item.Reason,
&item.CreatedAtMS,
); err != nil {
return nil, 0, err
}
item.UserID = strconv.FormatInt(userID, 10)
item.AgencyOwnerUserID = formatOptionalUserID(agencyOwnerUserID)
item.User = userDTO{UserID: item.UserID}
item.AgencyOwner = userDTO{UserID: item.AgencyOwnerUserID}
item.HostSalaryUSDDelta = formatUSDMinor(item.HostSalaryUSDMinorDelta)
item.AgencySalaryUSDDelta = formatUSDMinor(item.AgencySalaryUSDMinorDelta)
item.ResidualUSDDelta = formatUSDMinor(item.ResidualUSDMinorDelta)
items = append(items, item)
if userID > 0 {
userIDs = append(userIDs, userID)
}
if agencyOwnerUserID > 0 {
userIDs = append(userIDs, agencyOwnerUserID)
}
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
profiles, err := s.userProfiles(ctx, appCode, userIDs)
if err != nil {
return nil, 0, err
}
for i := range items {
if userID, err := strconv.ParseInt(items[i].UserID, 10, 64); err == nil {
items[i].User = userDTOFromProfile(items[i].UserID, profiles[userID])
}
if agencyID, err := strconv.ParseInt(items[i].AgencyOwnerUserID, 10, 64); err == nil && agencyID > 0 {
items[i].AgencyOwner = userDTOFromProfile(items[i].AgencyOwnerUserID, profiles[agencyID])
}
}
return items, total, nil
}
func settlementWhere(appCode string, req query) (string, []any) {
conditions := []string{"app_code = ?"}
args := []any{strings.TrimSpace(appCode)}
if req.CycleKey != "" {
conditions = append(conditions, "cycle_key = ?")
args = append(args, req.CycleKey)
}
if req.SettlementType != "" {
conditions = append(conditions, "settlement_type = ?")
args = append(args, req.SettlementType)
}
if req.Status != "" {
conditions = append(conditions, "status = ?")
args = append(args, req.Status)
}
if req.UserID > 0 {
conditions = append(conditions, "user_id = ?")
args = append(args, req.UserID)
}
if req.AgencyOwnerUserID > 0 {
conditions = append(conditions, "agency_owner_user_id = ?")
args = append(args, req.AgencyOwnerUserID)
}
if req.PolicyID > 0 {
conditions = append(conditions, "policy_id = ?")
args = append(args, req.PolicyID)
}
if req.StartAtMS > 0 {
conditions = append(conditions, "created_at_ms >= ?")
args = append(args, req.StartAtMS)
}
if req.EndAtMS > 0 {
conditions = append(conditions, "created_at_ms < ?")
args = append(args, req.EndAtMS)
}
// 查询只读 wallet-service 结算事实表,不回推钱包分录;账务对账需要单独走 entries/transactions 明细。
return "WHERE " + strings.Join(conditions, " AND "), args
}
func (s *Service) userProfiles(ctx context.Context, appCode string, userIDs []int64) (map[int64]userProfile, error) {
if s == nil || s.userDB == nil || len(userIDs) == 0 {
return map[int64]userProfile{}, nil
}
uniqueIDs := uniquePositiveUserIDs(userIDs)
if len(uniqueIDs) == 0 {
return map[int64]userProfile{}, nil
}
placeholders := strings.TrimRight(strings.Repeat("?,", len(uniqueIDs)), ",")
args := make([]any, 0, len(uniqueIDs)+1)
args = append(args, strings.TrimSpace(appCode))
for _, id := range uniqueIDs {
args = append(args, id)
}
rows, err := s.userDB.QueryContext(ctx, `
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
FROM users
WHERE app_code = ? AND user_id IN (`+placeholders+`)`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
profiles := make(map[int64]userProfile, len(uniqueIDs))
for rows.Next() {
var item userProfile
if err := rows.Scan(&item.UserID, &item.DisplayUserID, &item.Username, &item.Avatar); err != nil {
return nil, err
}
profiles[item.UserID] = item
}
return profiles, rows.Err()
}
func normalizeQuery(req query) query {
req.CycleKey = strings.TrimSpace(req.CycleKey)
req.SettlementType = normalizeSettlementType(req.SettlementType)
req.Status = normalizeStatus(req.Status)
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 50
}
if req.PageSize > maxPageSize {
req.PageSize = maxPageSize
}
return req
}
func normalizeSettlementType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all":
return ""
case "daily", "day":
return "daily"
case "half_month", "half-month", "halfmonth":
return "half_month"
case "month_end", "month-end", "monthend":
return "month_end"
default:
return ""
}
}
func normalizeStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all":
return ""
case "succeeded", "skipped", "failed":
return strings.ToLower(strings.TrimSpace(value))
default:
return ""
}
}
func uniquePositiveUserIDs(ids []int64) []int64 {
seen := map[int64]struct{}{}
out := make([]int64, 0, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
func userDTOFromProfile(fallbackID string, profile userProfile) userDTO {
if profile.UserID <= 0 {
return userDTO{UserID: fallbackID}
}
return userDTO{
UserID: strconv.FormatInt(profile.UserID, 10),
DisplayUserID: profile.DisplayUserID,
Username: profile.Username,
Avatar: profile.Avatar,
}
}
func formatOptionalUserID(value int64) string {
if value <= 0 {
return ""
}
return strconv.FormatInt(value, 10)
}
func formatUSDMinor(value int64) string {
sign := ""
if value < 0 {
sign = "-"
value = -value
}
whole := value / 100
fraction := value % 100
if fraction == 0 {
return sign + strconv.FormatInt(whole, 10)
}
if fraction < 10 {
return sign + strconv.FormatInt(whole, 10) + ".0" + strconv.FormatInt(fraction, 10)
}
return sign + strconv.FormatInt(whole, 10) + "." + strconv.FormatInt(fraction, 10)
}
func offset(page int, pageSize int) int {
if page <= 1 {
return 0
}
return (page - 1) * pageSize
}