2026-07-12 17:38:48 +08:00

202 lines
7.3 KiB
Go

package pointwithdrawalconfig
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
type Service struct {
walletDB *sql.DB
userDB *sql.DB
}
func NewService(walletDB *sql.DB, userDB *sql.DB) *Service {
return &Service{walletDB: walletDB, userDB: userDB}
}
// List 按 app_code 读取同一套公共配置表,并补齐用户资料;孤儿配置仍返回,便于运营删除而不是静默隐藏。
func (s *Service) List(ctx context.Context, appCode string) ([]itemDTO, error) {
if err := s.ensureSchema(ctx); err != nil {
return nil, err
}
rows, err := s.walletDB.QueryContext(ctx, `
SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
service_country_codes, status, created_at_ms, updated_at_ms
FROM point_withdrawal_coin_seller_configs
WHERE app_code = ?
ORDER BY sort_order ASC, seller_user_id ASC`, normalizeAppCode(appCode))
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]itemDTO, 0)
for rows.Next() {
var item itemDTO
var sellerUserID int64
var countriesJSON []byte
if err := rows.Scan(&item.AppCode, &sellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount, &countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
return nil, err
}
item.SellerUserID = strconv.FormatInt(sellerUserID, 10)
if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil {
return nil, fmt.Errorf("parse service countries for seller %d: %w", sellerUserID, err)
}
item.ServiceCountryCodes = normalizeCountries(item.ServiceCountryCodes)
displayUserID, nickname, avatar, countryName, lookupErr := s.lookupSeller(ctx, item.AppCode, sellerUserID, false)
if lookupErr != nil && !errors.Is(lookupErr, sql.ErrNoRows) {
return nil, lookupErr
}
item.DisplayUserID, item.Nickname, item.Avatar, item.CountryName = displayUserID, nickname, avatar, countryName
items = append(items, item)
}
return items, rows.Err()
}
// Upsert 在写白名单前验证同 App 的 active 币商身份,防止普通用户被配置成账务收款方。
func (s *Service) Upsert(ctx context.Context, appCode string, req upsertRequest, adminID int64) (itemDTO, error) {
appCode = normalizeAppCode(appCode)
if req.SellerUserID <= 0 || req.PointAmount <= 0 || req.SellerCoinAmount <= 0 || req.SortOrder < 0 {
return itemDTO{}, errors.New("币商、排序和兑换比例必须是正数")
}
status := strings.ToLower(strings.TrimSpace(req.Status))
if status == "" {
status = "active"
}
if status != "active" && status != "disabled" {
return itemDTO{}, errors.New("状态只能是 active 或 disabled")
}
if _, _, _, _, err := s.lookupSeller(ctx, appCode, req.SellerUserID, true); err != nil {
return itemDTO{}, err
}
if err := s.ensureSchema(ctx); err != nil {
return itemDTO{}, err
}
countries := normalizeCountries(req.ServiceCountryCodes)
countriesJSON, err := json.Marshal(countries)
if err != nil {
return itemDTO{}, err
}
nowMS := time.Now().UTC().UnixMilli()
_, err = s.walletDB.ExecContext(ctx, `
INSERT INTO point_withdrawal_coin_seller_configs (
app_code, seller_user_id, sort_order, point_amount, seller_coin_amount, service_country_codes,
status, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
sort_order = VALUES(sort_order), point_amount = VALUES(point_amount),
seller_coin_amount = VALUES(seller_coin_amount), service_country_codes = VALUES(service_country_codes),
status = VALUES(status), updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`,
appCode, req.SellerUserID, req.SortOrder, req.PointAmount, req.SellerCoinAmount, countriesJSON,
status, adminID, adminID, nowMS, nowMS)
if err != nil {
return itemDTO{}, err
}
return s.Get(ctx, appCode, req.SellerUserID)
}
func (s *Service) Get(ctx context.Context, appCode string, sellerUserID int64) (itemDTO, error) {
items, err := s.List(ctx, appCode)
if err != nil {
return itemDTO{}, err
}
needle := strconv.FormatInt(sellerUserID, 10)
for _, item := range items {
if item.SellerUserID == needle {
return item, nil
}
}
return itemDTO{}, sql.ErrNoRows
}
func (s *Service) Delete(ctx context.Context, appCode string, sellerUserID int64) error {
if sellerUserID <= 0 {
return errors.New("币商 ID 不正确")
}
if err := s.ensureSchema(ctx); err != nil {
return err
}
result, err := s.walletDB.ExecContext(ctx, `DELETE FROM point_withdrawal_coin_seller_configs WHERE app_code = ? AND seller_user_id = ?`, normalizeAppCode(appCode), sellerUserID)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *Service) lookupSeller(ctx context.Context, appCode string, sellerUserID int64, requireActive bool) (displayUserID string, nickname string, avatar string, countryName string, err error) {
if s.userDB == nil {
return "", "", "", "", errors.New("user db is not configured")
}
query := `
SELECT COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''),
COALESCE(c.country_display_name, c.country_name, u.country, '')
FROM coin_seller_profiles csp
JOIN users u ON u.user_id = csp.user_id AND u.app_code = csp.app_code
LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
WHERE csp.app_code = ? AND csp.user_id = ?`
if requireActive {
query += " AND csp.status = 'active' AND u.status = 'active'"
}
query += " LIMIT 1"
err = s.userDB.QueryRowContext(ctx, query, appCode, sellerUserID).Scan(&displayUserID, &nickname, &avatar, &countryName)
if errors.Is(err, sql.ErrNoRows) && requireActive {
return "", "", "", "", errors.New("目标用户不是当前 App 的启用币商")
}
return
}
func (s *Service) ensureSchema(ctx context.Context) error {
if s == nil || s.walletDB == nil {
return errors.New("wallet db is not configured")
}
_, err := s.walletDB.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
app_code VARCHAR(32) NOT NULL, seller_user_id BIGINT NOT NULL, sort_order INT NOT NULL DEFAULT 0,
point_amount BIGINT NOT NULL, seller_coin_amount BIGINT NOT NULL, service_country_codes JSON NOT NULL,
status VARCHAR(24) NOT NULL DEFAULT 'active', created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, seller_user_id),
KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例'`)
return err
}
func normalizeCountries(values []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(values))
for _, value := range values {
value = strings.ToUpper(strings.TrimSpace(value))
if value == "" || len(value) > 16 {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
sort.Strings(out)
return out
}
func normalizeAppCode(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return "lalu"
}
return value
}