积分与余额账务系统硬切重构

This commit is contained in:
2026-08-24 15:59:11 +08:00
parent ca12226b39
commit 5cae5b5c93
159 changed files with 7275 additions and 4718 deletions
+139 -33
View File
@@ -108,39 +108,60 @@ class UserController extends ControllerBase {
'permission_callback' => [$this, 'check_permission'],
]);
// Credits settings (admin)
register_rest_route(self::API_NAMESPACE, '/credits/settings', [
register_rest_route(self::API_NAMESPACE, '/assets/settings', [
'methods' => ['GET', 'POST'],
'callback' => [$this, 'rest_handle_credit_settings'],
'permission_callback' => [$this, 'check_permission'],
]);
// Credits — current user balance (frontend)
register_rest_route(self::API_NAMESPACE, '/credits/me', [
register_rest_route(self::API_NAMESPACE, '/assets/me', [
'methods' => 'GET',
'callback' => [$this, 'rest_get_credits_me'],
'permission_callback' => function () { return is_user_logged_in(); },
'permission_callback' => [$this, 'check_asset_self_permission'],
]);
// Credits — user list (admin)
register_rest_route(self::API_NAMESPACE, '/credits/users', [
register_rest_route(self::API_NAMESPACE, '/assets/accounts', [
'methods' => 'GET',
'callback' => [$this, 'rest_list_credit_users'],
'permission_callback' => [$this, 'check_permission'],
]);
// Credits — adjust user balance (admin)
register_rest_route(self::API_NAMESPACE, '/credits/users/(?P<id>\d+)/adjust', [
register_rest_route(self::API_NAMESPACE, '/assets/accounts/(?P<id>\d+)/adjust', [
'methods' => 'POST',
'callback' => [$this, 'rest_adjust_user_credits'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'id' => ['type' => 'integer', 'minimum' => 1, 'required' => true],
'asset' => ['type' => 'string', 'enum' => ['points', 'balance'], 'required' => true],
'delta' => [
'required' => true,
'oneOf' => [
['type' => 'integer'],
['type' => 'string', 'pattern' => '^-?(?:0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
],
],
'remark' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
// Credits — transaction log (admin)
register_rest_route(self::API_NAMESPACE, '/credits/log', [
register_rest_route(self::API_NAMESPACE, '/assets/transactions', [
'methods' => 'GET',
'callback' => [$this, 'rest_get_credits_log'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'transaction_id' => ['type' => 'integer', 'minimum' => 1],
'operation_key' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
'user_id' => ['type' => 'integer', 'minimum' => 1],
'actor_user_id' => ['type' => 'integer', 'minimum' => 1],
'asset' => ['type' => 'string', 'enum' => ['points', 'balance']],
'reason' => ['type' => 'string', 'maxLength' => 64, 'sanitize_callback' => 'sanitize_key'],
'ref_type' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_key'],
'ref_key' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
'date_from' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'date_to' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
// Report ticket list (admin)
@@ -639,60 +660,145 @@ class UserController extends ControllerBase {
$user_id = get_current_user_id();
return $this->success(['data' => [
'user_id' => $user_id,
'balance' => class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($user_id) : 0,
'points' => \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
]]);
}
public function check_asset_self_permission(\WP_REST_Request $request) {
if (!is_user_logged_in()) {
return new \WP_Error('aksrt_auth_required', __('请先登录', 'aksrt'), ['status' => 401]);
}
$nonce = (string) $request->get_header('X-WP-Nonce');
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
}
return true;
}
public function rest_list_credit_users(\WP_REST_Request $request): \WP_REST_Response {
$args = [
'search' => (string) $request->get_param('search'),
'page' => (int) $request->get_param('page'),
'per_page' => (int) $request->get_param('per_page'),
];
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::list_users($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
$asset = sanitize_key((string) $request->get_param('asset'));
$asset = in_array($asset, ['points', 'balance'], true) ? $asset : 'points';
$data = \AKSRT\Ledger\LedgerQuery::get_instance()->list_accounts($asset, $args);
foreach ($data['items'] as &$item) {
$item['asset'] = $asset;
$item['balance'] = $asset === 'balance'
? \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, (int) $item['balance_minor'])->decimal()
: (int) $item['balance_minor'];
unset($item['balance_minor']);
}
unset($item);
return $this->success(['data' => $data]);
}
public function rest_adjust_user_credits(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Credits\Credits')) {
return $this->error(__('积分系统不可用', 'aksrt'), 500);
}
$user_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$delta = (int) ($params['delta'] ?? 0);
$reason = sanitize_text_field((string) ($params['reason'] ?? 'admin'));
if ($user_id <= 0 || $delta === 0) {
return $this->error(__('用户或积分变动值无效', 'aksrt'), 422);
}
if ($delta > 0) {
$result = \AKSRT\Credits\Credits::add($user_id, $delta, $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
} else {
$result = \AKSRT\Credits\Credits::deduct($user_id, abs($delta), $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
}
$asset = sanitize_key((string) ($params['asset'] ?? ''));
$raw_delta = $params['delta'] ?? null;
$remark = sanitize_text_field((string) ($params['remark'] ?? ''));
if ($user_id <= 0 || !in_array($asset, ['points', 'balance'], true) || !get_userdata($user_id)) {
return $this->error(__('用户或资产类型无效', 'aksrt'), 400);
}
if (!current_user_can('edit_user', $user_id)) {
return $this->error(__('无权调整该用户资产', 'aksrt'), 403);
}
$negative = is_string($raw_delta) && str_starts_with($raw_delta, '-');
$unsigned = $negative ? substr($raw_delta, 1) : $raw_delta;
try {
if ($asset === 'points') {
if (!is_int($raw_delta) || $raw_delta === PHP_INT_MIN) throw new \InvalidArgumentException();
$minor = abs($raw_delta);
$negative = $raw_delta < 0;
} else {
if (!is_string($unsigned)) throw new \InvalidArgumentException();
$minor = \AKSRT\Ledger\AssetAmount::balance($unsigned)->minor();
}
} catch (\InvalidArgumentException $e) {
return $this->error(__('资产变动金额格式无效', 'aksrt'), 400);
}
if ($minor <= 0) return $this->error(__('资产变动金额必须非零', 'aksrt'), 400);
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'admin-adjustment', get_current_user_id());
if (is_wp_error($operation_key)) return new \WP_REST_Response(['success' => false, 'message' => $operation_key->get_error_message()], 400);
$service = \AKSRT\Ledger\LedgerService::get_instance();
$result = $negative
? $service->debit($user_id, $asset, $minor, $operation_key, 'admin_adjustment', ['type' => 'user', 'id' => $user_id], $remark, get_current_user_id())
: $service->credit($user_id, $asset, $minor, $operation_key, 'admin_adjustment', ['type' => 'user', 'id' => $user_id], $remark, get_current_user_id());
if (is_wp_error($result)) {
return $this->error($result->get_error_message(), 422);
$status = (int) (($result->get_error_data()['status'] ?? 400));
return $this->error($result->get_error_message(), $status);
}
return $this->success(['data' => [
'user_id' => $user_id,
'balance' => (int) $result,
], 'message' => __('积分调整已保存', 'aksrt')]);
'asset' => $asset,
'balance' => $asset === 'balance'
? \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, $result->balance($user_id, $asset))->decimal()
: $result->balance($user_id, $asset),
'transaction_id' => $result->transaction_id,
'replayed' => $result->replayed,
], 'message' => __('资产调整已保存', 'aksrt')]);
}
public function rest_get_credits_log(\WP_REST_Request $request): \WP_REST_Response {
$args = [
'transaction_id' => (int) $request->get_param('transaction_id'),
'operation_key' => (string) $request->get_param('operation_key'),
'user_id' => (int) $request->get_param('user_id'),
'actor_user_id' => (int) $request->get_param('actor_user_id'),
'reason' => (string) $request->get_param('reason'),
'ref_type' => (string) $request->get_param('ref_type'),
'ref_key' => (string) $request->get_param('ref_key'),
'page' => (int) $request->get_param('page'),
'per_page' => (int) $request->get_param('per_page'),
'date_from' => (string) $request->get_param('date_from'),
'date_to' => (string) $request->get_param('date_to'),
];
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::get_log($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
$args['asset'] = sanitize_key((string) $request->get_param('asset'));
$data = \AKSRT\Ledger\LedgerQuery::get_instance()->history($args);
foreach ($data['items'] as &$item) {
$assets = array_values(array_unique(array_map('strval', (array) ($item['assets'] ?? []))));
$single_asset = count($assets) === 1 ? $assets[0] : '';
if ($single_asset !== '') {
$item['delta'] = $this->asset_minor_for_response($single_asset, (int) ($item['delta_minor'] ?? 0), true);
$item['fee'] = $this->asset_minor_for_response($single_asset, (int) ($item['fee_minor'] ?? 0));
$item['balance_after'] = $this->asset_minor_for_response($single_asset, (int) ($item['balance_after_minor'] ?? 0));
} else {
$item['delta'] = null;
$item['fee'] = null;
$item['balance_after'] = null;
}
unset($item['delta_minor'], $item['fee_minor'], $item['balance_after_minor']);
foreach ($item['entries'] as &$entry) {
$entry_asset = (string) ($entry['asset'] ?? '');
$entry['delta'] = $this->asset_minor_for_response($entry_asset, (int) ($entry['delta_minor'] ?? 0), true);
$entry['balance_after'] = $this->asset_minor_for_response($entry_asset, (int) ($entry['balance_after_minor'] ?? 0));
unset($entry['delta_minor'], $entry['balance_after_minor']);
}
unset($entry);
}
unset($item);
return $this->success(['data' => $data]);
}
private function asset_minor_for_response(string $asset, int $minor, bool $signed = false) {
if ($asset === \AKSRT\Ledger\AssetType::POINTS->value) {
return $minor;
}
if ($asset !== \AKSRT\Ledger\AssetType::BALANCE->value) {
return null;
}
$negative = $minor < 0;
$value = \AKSRT\Ledger\AssetAmount::decimal_from_minor(abs($minor));
if ($negative) {
return '-' . $value;
}
return $signed && $minor > 0 ? '+' . $value : $value;
}
}