291 lines
7.7 KiB
Go
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
|
|
}
|