diff --git a/.env-example b/.env-example index 83f2685..935c9f9 100644 --- a/.env-example +++ b/.env-example @@ -18,6 +18,7 @@ DATABASE_CHARSET = utf8mb4 DATABASE_PREFIX = # PlayX 配置 +# 以下三项比例以数据库表 mall_config 为准(后台 积分商城 → 商城参数配置);未迁移或无记录时回退为下列 env 默认值 # 提现折算:积分 -> 现金(如 10 分 = 1 元,则 points_to_cash_ratio=0.1) PLAYX_POINTS_TO_CASH_RATIO=0.1 # 返还比例:新增保障金 = ABS(yesterday_win_loss_net) * return_ratio(仅亏损时) diff --git a/app/admin/controller/mall/PlayxConfig.php b/app/admin/controller/mall/PlayxConfig.php new file mode 100644 index 0000000..6e09fea --- /dev/null +++ b/app/admin/controller/mall/PlayxConfig.php @@ -0,0 +1,91 @@ +initializeBackend($request); + if ($response !== null) { + return $response; + } + + $row = MallConfig::order('id', 'asc')->find(); + if (!$row) { + $now = time(); + MallConfig::create([ + 'return_ratio' => floatval(env('PLAYX_RETURN_RATIO', '0.1')), + 'unlock_ratio' => floatval(env('PLAYX_UNLOCK_RATIO', '0.1')), + 'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')), + 'create_time' => $now, + 'update_time' => $now, + ]); + $row = MallConfig::order('id', 'asc')->find(); + } + + return $this->success('', [ + 'row' => $row ? $row->toArray() : [], + 'remark' => get_route_remark(), + ]); + } + + public function save(Request $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + + $data = $request->post(); + if (empty($data) || !is_array($data)) { + return $this->error(__('Parameter %s can not be empty', [''])); + } + + $return = $data['return_ratio'] ?? null; + $unlock = $data['unlock_ratio'] ?? null; + $cash = $data['points_to_cash_ratio'] ?? null; + if (!is_numeric($return) || !is_numeric($unlock) || !is_numeric($cash)) { + return $this->error(__('Parameter error')); + } + + $returnF = floatval($return); + $unlockF = floatval($unlock); + $cashF = floatval($cash); + if ($returnF < 0 || $unlockF < 0 || $cashF < 0) { + return $this->error(__('Parameter error')); + } + + $row = MallConfig::order('id', 'asc')->find(); + if (!$row) { + $now = time(); + MallConfig::create([ + 'return_ratio' => $returnF, + 'unlock_ratio' => $unlockF, + 'points_to_cash_ratio' => $cashF, + 'create_time' => $now, + 'update_time' => $now, + ]); + } else { + $row->return_ratio = $returnF; + $row->unlock_ratio = $unlockF; + $row->points_to_cash_ratio = $cashF; + $row->save(); + } + + MallPlayxRatios::forget(); + + return $this->success(__('The current page configuration item was updated successfully')); + } +} diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index 047c5f5..61f0a7f 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -14,6 +14,7 @@ use app\common\model\MallDailyPush; use app\common\model\MallSession; use app\common\model\MallOrder; use app\common\model\MallUserAsset; +use app\common\library\MallPlayxRatios; use app\common\model\MallAddress; use support\think\Db; use Webman\Http\Request; @@ -231,8 +232,9 @@ class Playx extends Api $requestId = 'report_' . $date; } - $returnRatio = config('playx.return_ratio', 0.1); - $unlockRatio = config('playx.unlock_ratio', 0.1); + $ratios = MallPlayxRatios::get(); + $returnRatio = $ratios['return_ratio']; + $unlockRatio = $ratios['unlock_ratio']; $results = []; $allDeduped = true; @@ -332,8 +334,9 @@ class Playx extends Api $exists = MallDailyPush::where('user_id', $playxUserId)->where('date', $date)->find(); if ($exists) { $newLocked = 0; - $returnRatio = config('playx.return_ratio', 0.1); - $unlockRatio = config('playx.unlock_ratio', 0.1); + $ratios = MallPlayxRatios::get(); + $returnRatio = $ratios['return_ratio']; + $unlockRatio = $ratios['unlock_ratio']; if ($yesterdayWinLossNet < 0) { $newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio)); } @@ -365,8 +368,9 @@ class Playx extends Api ]); $newLocked = 0; - $returnRatio = config('playx.return_ratio', 0.1); - $unlockRatio = config('playx.unlock_ratio', 0.1); + $ratios = MallPlayxRatios::get(); + $returnRatio = $ratios['return_ratio']; + $unlockRatio = $ratios['unlock_ratio']; if ($yesterdayWinLossNet < 0) { $newLocked = intval(round(abs(floatval($yesterdayWinLossNet)) * $returnRatio)); } @@ -668,7 +672,7 @@ class Playx extends Api ]); } - $ratio = config('playx.points_to_cash_ratio', 0.1); + $ratio = MallPlayxRatios::get()['points_to_cash_ratio']; $withdrawableCash = round($asset->available_points * $ratio, 2); return $this->success('', [ @@ -1257,7 +1261,7 @@ SQL; 'withdrawable_cash' => 0, ]; } - $ratio = config('playx.points_to_cash_ratio', 0.1); + $ratio = MallPlayxRatios::get()['points_to_cash_ratio']; return [ 'locked_points' => $asset->locked_points, 'available_points' => $asset->available_points, diff --git a/app/common/library/MallPlayxRatios.php b/app/common/library/MallPlayxRatios.php new file mode 100644 index 0000000..a70787b --- /dev/null +++ b/app/common/library/MallPlayxRatios.php @@ -0,0 +1,58 @@ +find(); + if ($row) { + self::$cache = [ + 'return_ratio' => self::toFloat($row->return_ratio), + 'unlock_ratio' => self::toFloat($row->unlock_ratio), + 'points_to_cash_ratio' => self::toFloat($row->points_to_cash_ratio), + ]; + + return self::$cache; + } + + self::$cache = [ + 'return_ratio' => floatval(env('PLAYX_RETURN_RATIO', '0.1')), + 'unlock_ratio' => floatval(env('PLAYX_UNLOCK_RATIO', '0.1')), + 'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')), + ]; + + return self::$cache; + } + + public static function forget(): void + { + self::$cache = null; + } + + private static function toFloat(mixed $v): float + { + if (is_numeric($v)) { + return floatval($v); + } + + return 0.0; + } +} diff --git a/app/common/model/MallConfig.php b/app/common/model/MallConfig.php new file mode 100644 index 0000000..9a2a8f4 --- /dev/null +++ b/app/common/model/MallConfig.php @@ -0,0 +1,25 @@ + 'integer', + 'update_time' => 'integer', + 'return_ratio' => 'float', + 'unlock_ratio' => 'float', + 'points_to_cash_ratio' => 'float', + ]; +} diff --git a/config/playx.php b/config/playx.php index b75ba26..b0d5d9e 100644 --- a/config/playx.php +++ b/config/playx.php @@ -4,11 +4,12 @@ * PlayX 积分商城对接配置 */ return [ - // 返还比例:新增保障金 = ABS(yesterday_win_loss_net) * 返还比例(仅亏损时) + /** + * 以下三项比例以数据表 mall_config 为准(后台「商城参数配置」)。 + * 此处保留 env 仅作兼容/文档默认值;业务代码请使用 MallPlayxRatios::get()。 + */ 'return_ratio' => floatval(env('PLAYX_RETURN_RATIO', '0.1')), - // 解锁比例:今日可领取上限 = yesterday_total_deposit * 解锁比例 'unlock_ratio' => floatval(env('PLAYX_UNLOCK_RATIO', '0.1')), - // 提现折算:积分 → 现金(如 10 分 = 1 元) 'points_to_cash_ratio' => floatval(env('PLAYX_POINTS_TO_CASH_RATIO', '0.1')), // Daily Push 签名校验(PlayX 调用商城时使用) 'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')), diff --git a/config/route.php b/config/route.php index 613e54c..beb5002 100644 --- a/config/route.php +++ b/config/route.php @@ -217,6 +217,10 @@ Route::post('/admin/user/moneyLog/add', [\app\admin\controller\user\MoneyLog::cl // admin/mall/dailyPush Route::post('/admin/mall/dailyPush/backfillUsers', [\app\admin\controller\mall\DailyPush::class, 'backfillUsers']); +// admin/mall/playxConfig(积分商城参数) +Route::get('/admin/mall.PlayxConfig/index', [\app\admin\controller\mall\PlayxConfig::class, 'index']); +Route::post('/admin/mall.PlayxConfig/save', [\app\admin\controller\mall\PlayxConfig::class, 'save']); + // admin/routine/config Route::get('/admin/routine/config/index', [\app\admin\controller\routine\Config::class, 'index']); Route::post('/admin/routine/config/edit', [\app\admin\controller\routine\Config::class, 'edit']); diff --git a/web/src/api/backend/mall/playxConfig.ts b/web/src/api/backend/mall/playxConfig.ts new file mode 100644 index 0000000..6b18ae9 --- /dev/null +++ b/web/src/api/backend/mall/playxConfig.ts @@ -0,0 +1,28 @@ +import createAxios from '/@/utils/axios' + +export const url = '/admin/mall.PlayxConfig/' + +export const actionUrl = new Map([ + ['index', url + 'index'], + ['save', url + 'save'], +]) + +export function index() { + return createAxios({ + url: actionUrl.get('index'), + method: 'get', + }) +} + +export function save(data: anyObj) { + return createAxios( + { + url: actionUrl.get('save'), + method: 'post', + data, + }, + { + showSuccessMessage: true, + } + ) +} diff --git a/web/src/components/mall/MallPageIntro.vue b/web/src/components/mall/MallPageIntro.vue new file mode 100644 index 0000000..f416fff --- /dev/null +++ b/web/src/components/mall/MallPageIntro.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/web/src/lang/backend/en/mall/dailyPush.ts b/web/src/lang/backend/en/mall/dailyPush.ts index f7e7fc6..297b4c5 100644 --- a/web/src/lang/backend/en/mall/dailyPush.ts +++ b/web/src/lang/backend/en/mall/dailyPush.ts @@ -1,13 +1,12 @@ export default { - id: 'id', - user_id: 'user_id', - date: 'date', - username: 'username', - yesterday_win_loss_net: 'yesterday_win_loss_net', - yesterday_total_deposit: 'yesterday_total_deposit', - lifetime_total_deposit: 'lifetime_total_deposit', - lifetime_total_withdraw: 'lifetime_total_withdraw', - create_time: 'create_time', - 'quick Search Fields': 'id', + id: 'ID', + user_id: 'PlayX user ID', + date: 'Business date', + username: 'Username', + yesterday_win_loss_net: 'Yesterday net win/loss', + yesterday_total_deposit: 'Yesterday total deposit', + lifetime_total_deposit: 'Lifetime total deposit', + lifetime_total_withdraw: 'Lifetime total withdraw', + create_time: 'Created at', + 'quick Search Fields': 'ID', } - diff --git a/web/src/lang/backend/en/mall/pageIntro.ts b/web/src/lang/backend/en/mall/pageIntro.ts new file mode 100644 index 0000000..965953e --- /dev/null +++ b/web/src/lang/backend/en/mall/pageIntro.ts @@ -0,0 +1,58 @@ +export default { + userAsset: { + title: 'About this page', + desc: 'Manage mall user asset records, including PlayX binding and points fields used for claims and redemptions.', + }, + address: { + title: 'About this page', + desc: 'View and manage user shipping addresses for physical orders.', + }, + order: { + title: 'About this page', + desc: 'Central list of mall orders (approval, shipping, points changes) with search and admin actions.', + }, + dailyPush: { + title: 'About this page', + desc: 'Daily T+1 snapshots pushed by the partner; rows are stored here and drive user-asset related updates.', + }, + claimLog: { + title: 'About this page', + desc: 'History of users moving points from locked/pending to available, filterable by time and user.', + }, + item: { + title: 'About this page', + desc: 'Manage catalog items, stock, and multilingual display text for the points mall.', + }, + pintsOrder: { + title: 'About this page', + desc: 'Orders paid with points—use for redemption and fulfillment checks.', + }, + redemptionOrder: { + title: 'About this page', + desc: 'Redemption-style orders (e.g. bonus payouts) for issuance and reconciliation.', + }, + playxCenter: { + title: 'About this page', + desc: 'PlayX hub: orders, daily push, claim logs, and user assets in one place for integration and troubleshooting.', + }, + playxOrder: { + title: 'About this page', + desc: 'PlayX-facing order list for cross-checking with the mall.', + }, + playxDailyPush: { + title: 'About this page', + desc: 'PlayX daily push rows in admin (compare with the standalone Daily push menu when sources align).', + }, + playxClaimLog: { + title: 'About this page', + desc: 'PlayX users’ claim history on the mall side for amount and frequency checks.', + }, + playxUserAsset: { + title: 'About this page', + desc: 'PlayX user assets in list form—cross-check with the main User assets screen when needed.', + }, + playxConfig: { + title: 'About this page', + desc: 'Edit return/unlock/points-to-cash ratios used by daily push and asset APIs; values are stored in mall_config and apply immediately after save.', + }, +} diff --git a/web/src/lang/backend/en/mall/playxConfig.ts b/web/src/lang/backend/en/mall/playxConfig.ts new file mode 100644 index 0000000..6307a6f --- /dev/null +++ b/web/src/lang/backend/en/mall/playxConfig.ts @@ -0,0 +1,8 @@ +export default { + return_ratio: 'Return ratio (return_ratio)', + return_ratio_tip: 'In daily push, when yesterday net win/loss is negative: locked increment = |net| × this ratio.', + unlock_ratio: 'Unlock ratio (unlock_ratio)', + unlock_ratio_tip: 'In daily push: daily claim cap = yesterday total deposit × this ratio.', + points_to_cash_ratio: 'Points to cash ratio', + points_to_cash_ratio_tip: 'For withdrawable cash display: cash ≈ available points × this ratio.', +} diff --git a/web/src/lang/backend/en/menu.ts b/web/src/lang/backend/en/menu.ts index dd80ba3..2e3445a 100644 --- a/web/src/lang/backend/en/menu.ts +++ b/web/src/lang/backend/en/menu.ts @@ -115,5 +115,8 @@ export default { mall_playxUserAsset: 'playX user assets', mall_pintsOrder: 'Points orders', mall_redemptionOrder: 'Redemption orders', + mall_playxConfig: 'Mall parameters', + mall_playxConfig_index: 'View', + mall_playxConfig_save: 'Save', }, } diff --git a/web/src/lang/backend/zh-cn/mall/pageIntro.ts b/web/src/lang/backend/zh-cn/mall/pageIntro.ts new file mode 100644 index 0000000..50a8985 --- /dev/null +++ b/web/src/lang/backend/zh-cn/mall/pageIntro.ts @@ -0,0 +1,58 @@ +export default { + userAsset: { + title: '页面说明', + desc: '维护积分商城用户资产主数据,含 PlayX 用户绑定、积分字段等,用于前台领取与兑换鉴权。', + }, + address: { + title: '页面说明', + desc: '查看与管理用户收货地址,供实物类订单发货使用。', + }, + order: { + title: '页面说明', + desc: '集中查看商城订单(含审核、发货、积分变动等),支持按条件检索与后台操作。', + }, + dailyPush: { + title: '页面说明', + desc: '展示合作方 T+1 每日推送的经营数据快照;接口写入后在此留痕,并用于同步用户资产相关字段。', + }, + claimLog: { + title: '页面说明', + desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。', + }, + item: { + title: '页面说明', + desc: '维护积分商城上架商品、库存与多语言展示信息。', + }, + pintsOrder: { + title: '页面说明', + desc: '以积分支付的订单列表,用于核对兑换与发货状态。', + }, + redemptionOrder: { + title: '页面说明', + desc: '兑换类业务订单(如礼金等)列表,用于核对发放与对账。', + }, + playxCenter: { + title: '页面说明', + desc: 'PlayX 相关能力的聚合入口:订单、每日推送、领取记录与用户资产分栏查看,便于联调与排障。', + }, + playxOrder: { + title: '页面说明', + desc: '从 PlayX 视角查看与商城关联的订单数据,用于对接核对。', + }, + playxDailyPush: { + title: '页面说明', + desc: 'PlayX 每日推送记录在后台的列表视图(与独立「每日推送」菜单数据源一致时可对照查看)。', + }, + playxClaimLog: { + title: '页面说明', + desc: 'PlayX 用户在商城侧的领取流水,用于核对领取额度与频次。', + }, + playxUserAsset: { + title: '页面说明', + desc: 'PlayX 用户资产在后台的列表视图,可与「用户资产」主菜单交叉核对。', + }, + playxConfig: { + title: '页面说明', + desc: '配置每日推送用到的返还比例、解锁比例及积分折算现金比例;保存后立即作用于接口逻辑(mall_config)。', + }, +} diff --git a/web/src/lang/backend/zh-cn/mall/playxConfig.ts b/web/src/lang/backend/zh-cn/mall/playxConfig.ts new file mode 100644 index 0000000..6233f26 --- /dev/null +++ b/web/src/lang/backend/zh-cn/mall/playxConfig.ts @@ -0,0 +1,8 @@ +export default { + return_ratio: '返还比例(return_ratio)', + return_ratio_tip: '每日推送中,仅当昨日净输赢为负时:待领取增量 = |净输赢| × 本比例。', + unlock_ratio: '解锁比例(unlock_ratio)', + unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例。', + points_to_cash_ratio: '积分折算现金比例', + points_to_cash_ratio_tip: '用于资产接口中「可提现现金」等展示:现金 ≈ 可用积分 × 本比例。', +} diff --git a/web/src/lang/backend/zh-cn/menu.ts b/web/src/lang/backend/zh-cn/menu.ts index 78ee4dc..bd20ca5 100644 --- a/web/src/lang/backend/zh-cn/menu.ts +++ b/web/src/lang/backend/zh-cn/menu.ts @@ -116,5 +116,8 @@ export default { mall_playxUserAsset: 'playX用户资产', mall_pintsOrder: '积分订单', mall_redemptionOrder: '兑换订单', + mall_playxConfig: '商城参数配置', + mall_playxConfig_index: '查看', + mall_playxConfig_save: '保存', }, } diff --git a/web/src/lang/index.ts b/web/src/lang/index.ts index 114bb44..5625542 100644 --- a/web/src/lang/index.ts +++ b/web/src/lang/index.ts @@ -53,8 +53,11 @@ export async function loadLang(app: App) { */ if (locale == 'zh-cn') { assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/zh-cn/**/*.ts', { eager: true }), locale)) + // 积分商城后台页表格列在 setup 阶段即 t(),若仅依赖路由懒加载会出现列头显示为 key + assignLocale[locale].push(getLangFileMessage(import.meta.glob('./backend/zh-cn/mall/**/*.ts', { eager: true }), locale)) } else if (locale == 'en') { assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/en/**/*.ts', { eager: true }), locale)) + assignLocale[locale].push(getLangFileMessage(import.meta.glob('./backend/en/mall/**/*.ts', { eager: true }), locale)) } const messages = { diff --git a/web/src/views/backend/mall/address/index.vue b/web/src/views/backend/mall/address/index.vue index 6cdddfd..5202bbb 100644 --- a/web/src/views/backend/mall/address/index.vue +++ b/web/src/views/backend/mall/address/index.vue @@ -1,5 +1,6 @@