Commit Graph
278 Commits
Author SHA1 Message Date
kate522 a1eb545bef fix: mute records get an unban entry on the restrictions tab
The record list rendered an action only for global ban/suspend rows, so
mute (禁言) rows showed an empty 操作 column with no way to lift them.
- record list: active mute rows now render a 解除禁言 button
- unban view: falls back to the active mute record when no ban/suspend
  exists, with 解除禁言/禁言中 titles (ban > suspend > mute precedence
  preserved); lifting still goes through lift_user_restriction (mute
  included by default)
- contract tests: cover the mute action link and the unban-view fallback
2026-09-20 16:22:47 +08:00
kate522 0df6e760e3 feat: full parity pass - suspend enforcement, report modal polish, wechat notify
- Restriction: add is_suspended/get_active_suspend; comment block now covers
  suspend (小黑屋) in addition to mute, with reason in the deny message
- FrontSubmitSave::persist: block post/draft/edit submission for suspended
  users (reference-theme semantics: 小黑屋 loses all publishing rights)
- report.js: close icon switched from fa-times to the SVG sprite ic-close
  (#icon-close), same as reference theme; textareas gain autoHeight growth
  (hidden 'other reason' box skips the first measurement)
- WeChatNotify: NOTIFY_TYPES now includes report/system so report results
  and account restriction notices fan out to WeChat template messages;
  generic title/description fallback for template values
- contract tests: +3 tests (suspend enforcement, modal polish, wechat types)
2026-09-20 16:09:12 +08:00
kate522 c38616b290 feat(admin): rebuild the restrictions tab on the reference ban modal
Replace the self-invented CRUD (warn/mute/suspend/ban + scope selector)
with the reference-theme ban model: ban method (blackroom suspend /
login ban), duration radios (1/7/14/30/100 days or permanent, default 7),
preset reason chips with an other-reason input, ban note, delete-all-
content switch (posts/comments to trash), and a no-appeal switch with a
required notice that also gates the frontend appeal entry. Banned users
render the status info view (status/start/end/reason) with an unban
leave-a-message form; the record list uses the reference columns and
per-user unban links. Bans notify the user in-site like the reference
zib_updata_user_ban_msg.
2026-09-20 15:48:48 +08:00
kate522 970b377b9a perf(admin): toggle the ticket process form instead of reloading the page
Expand and cancel were full admin navigations (action/id query round
trip), which is why they felt slow. Pre-render each pending ticket's
process form as a hidden colspan=6 row and swap list row <-> form via a
small delegated jQuery handler: expand hides the row (info stays
single-sourced), collapse brings it back, only one form is open at a
time, and the no-JS action=process links keep working as a fallback.
2026-09-20 15:32:27 +08:00
kate522 03c2f1f84a test(admin): fix the cross-line cancel-button assertion 2026-09-20 15:27:28 +08:00
kate522 fe6b4da455 feat(admin): add a cancel button next to the ticket process submit
Collapses the inline process form by linking back to the current list
URL without the action/id params (filters preserved).
2026-09-20 15:26:55 +08:00
kate522 c4531b9757 fix(admin): drop the view-all button from the ticket process view
The reference theme has no such element above the inline process form;
filter reset is already handled by the type buttons and status subsubsub.
Contract label list updated accordingly.
2026-09-20 15:25:01 +08:00
kate522 0a958644b8 fix(admin): expand the ticket process form inline without duplicating the row
Restore the reference-theme direction: processing happens inline in the
filtered list, not on a separate page. The actual defect was the same
ticket being rendered twice (list row + form) while the bare <form><table>
inside <tbody> got foster-parented by the browser, displacing the extra
row info below the submit button. Now the processing ticket renders only
as the form, wrapped in a valid <tr><td colspan=6>, and every other row
renders normally.
2026-09-20 15:23:08 +08:00
kate522 09cff17d72 fix(admin): render the ticket process form as a dedicated view
action=process used to append the process form into the tickets table as
a bare <form><table> inside <tbody> — invalid HTML that browsers reflow,
leaving the ticket list stacked below the submit button. Render the
process form as its own view (early return, list and pagenavi skipped,
with a lookup fallback beyond the current page slice) and keep the list
view free of form injection.
2026-09-20 15:16:44 +08:00
kate522 c7384b10dd feat(report): route report notices into the in-site message system
Ban appeals already sent site messages to admins and to the appellant,
but reports only produced emails: the submit path never notified admins
in-site and the status change path never notified the reporter. Send a
'report' site message to administrators on submit (deep link to the
ticket list) and to the reporter on status change (status label plus
the admin note), matching the appeal flow's direct-send pattern.
Contract test extended accordingly.
2026-09-20 15:07:45 +08:00
kate522 c7a552dfd5 fix(report): carry the admin note into the reporter result email
update_status() fired aksrt_report_status_changed without the note and
the mail listener hard-coded note => '', so the reply-to-reporter
comment written in the process form never reached the email. Pass the
sanitized note as a 4th action arg, render it via the existing {note}
placeholder, and map the raw status key to its Chinese label in the
email body. Contract test extended accordingly.
2026-09-20 15:03:09 +08:00
kate522 044830b58a test(report): match the dispatched action-field contract in the process form 2026-09-20 14:56:48 +08:00
kate522 9cc9ff6d52 fix(admin): stop the ticket process form from white-screening on submit
The process form posted to admin-post.php without a hidden action field,
so no admin_post_{action} hook fired and WordPress returned a blank
page. Dispatch to aksrt_users_update_report / aksrt_users_process_appeal
per ticket kind, matching every other native form, and lock the contract
in the report test.
2026-09-20 14:56:08 +08:00
kate522 3d4179a756 fix(report): give preset reasons a stable non-empty slug for CJK labels
ban_settings() derived each reason slug via sanitize_key(label), which
strips CJK text to an empty string; the frontend filter then discarded
every reason (including the schema defaults) and fell back to the
hard-coded list. Derive the slug from an ASCII key when possible and
from a stable md5 short hash of the label otherwise, and relax the
frontend filter to the label only. Adds a WP-stub harness that verifies
the defaults path end to end (tests/dev/verify-ban-settings-defaults.php).
2026-09-20 14:48:28 +08:00
kate522 67547fdf23 fix(report): fall back to schema defaults when the ban/report section was never saved
ban_settings()/report_settings() read the raw aksrt_settings option, so on
sites that never saved the ban/report admin section the keys are absent and
the preset reasons resolved to an empty list (frontend hard-coded fallback).
Read the section through SchemaProvider instead — read_section_settings
merges schema defaults, matching the reference theme _pz(key, default)
semantics. Also drop the dead user_report_categories parse chain that fed
a key nobody stores or consumes.
2026-09-20 14:35:17 +08:00
kate522 2882b83c21 fix(report): restore the centered plus icon in the image upload box
main.css already ships the reference plus icon (.preview .add:before/:after,
25px rounded bars centered via inset:0 + margin:auto). The duplicate
left/top:50% + translate version in modal-runtime.css over-constrained
the same pseudo elements (right/bottom:0 + margin:auto from main.css
still applied), pushing the bars to 75%/75% so only an L-shaped corner
was visible. Drop the duplicate and lock the single-source contract in
the report test.
2026-09-20 14:28:05 +08:00
kate522 7a80297a56 fix(report): align the report modal with the reference theme 1:1
- read preset reasons from the real source (ban_preset_reason via
  UserRuntime::ban_settings); the previous injection read a
  nonexistent report.categories key, so backend reasons never showed
- rebuild the modal on the reference markup contract: edit-header
  warning header, reason chips toggling the reference .active class
  (focus color), other-reason textarea, violation link, description,
  but-average modal-buts footer
- add image evidence upload gated by user_report_img_s: new
  /report/upload-image route reusing the comment upload handler with
  its own permission, preview/remove UI, up to 3 images appended to
  the report content
- render the backend report notice (user_report_desc, sanitized HTML)
  inside the modal
- widen the dialog to the reference 600px desktop geometry and share
  the form-upload preview styles with the order review modal
- extend the report contract test accordingly
2026-09-20 14:20:09 +08:00
kate522 7c730b83d6 fix(report): unify the frontend report entry and modal
- enqueue report.js on singular pages too: the single meta box and
  comment report buttons rendered without their script handler, so
  clicking the report item did nothing
- align the report runtime default with the admin schema and the
  reference theme (user_report_s default true) so the public user
  home cover menu shows the report entry
- rebuild report.js on top of AksrtModalRuntime following the
  reference theme modal format: red warning header, preset reason
  badges with an other-reason textarea, violation link field, detail
  description and a cancel/submit footer; submit goes through the
  unified /report/submit endpoint
- keep report inside the comment three-dot dropdown and enrich the
  payload (target name/avatar) for the modal; inject configured
  report categories via aksrtData.reportCategories
- update the report contract test accordingly
2026-09-20 13:40:05 +08:00
kate522 381256ed59 feat(admin): host the ban/report settings page in the SPA schema
The users-interaction sidebar entry 禁封/举报 was a redirect stub; it is
now a schema-driven settings page replicating the reference theme
field-for-field:

- user_ban_s / user_report_s switches, ban_preset_reason and
  ban_appeal_keys as repeatable group rows ({t}), ban_appeal_desc and
  user_report_desc HTML textareas with the reference default copy, and
  user_report_img_s — keys, labels, help texts, defaults and ordering
  all mirrored in SchemaUsers.php and the SPA report.js schema
- UserRuntime::ban_settings()/report_settings() and
  BanAppeal::settings() read the new group-array values with fallback
  to the previous line/comma formats
- delete the self-built SPA queue pages (UsersBan, UsersReport,
  ReportDetailDrawer, UsersMessages) that the reference theme does not
  have; legacy /users/report bookmarks still redirect to the native page
- contract tests: lock the schema keys/labels, the schemaPage mount and
  the deleted-file state; drop reads of removed components
2026-09-19 22:24:55 +08:00
kate522 c0e0912eee feat(admin): fully align the ban page with the reference theme
- submenu page title becomes 处理举报及禁封申诉 (menu title 举报&禁封申诉)
- replicate the reference pagenavi: first/prev/numbered/next/last links
  with displaying-num (每页 20 共N) and paging-input (第X页共Y页)
- report process form is now status-only like the reference theme:
  被举报用户/举报信息/被举报用户禁封信息/处理说明 blocks, with a 点击此处
  link to the restrictions tab (prefilled user_id) instead of an
  enforcement select
- replicate reference row details (举报用户/举报内容, 正在处理 state,
  此用户已解封 auto-close for stale appeals, 当前用户已解封 notice) and
  the table-box/preview-item style block
- drop now-unused ReportCenter::get_report_row
2026-09-19 22:09:24 +08:00
kate522 902e71ee62 feat(admin): rebuild ban page to mirror the reference theme
- rename the users.php submenu to 举报&禁封申诉 and the page heading to
  用户禁封申诉及用户举报管理
- merge report tickets and ban appeals into one Zibll-style queue:
  type filter buttons (全部/禁封申诉/用户举报), status subsubsub
  (全部/待处理/已批准/已拒绝), search box, sortable widefat table with
  the same column set (状态/类型/提交用户/详情/提交时间/更新时间),
  inline process forms and 20-per-page pagination
- report process form shows the reported target, restriction status and
  an enforcement action select; appeal form offers approve/reject radios
  with a message to the user
- move restriction enforcement into ReportCenter::apply_action so REST
  and the native page share one implementation; add get_report_row()
- drop the old three-tab rendering (reports/appeals tabs) and fix all
  notice/mail/SPA links to the merged page
2026-09-19 22:00:17 +08:00
kate522 835e0f983d feat(users): enforce real bans from report processing and fix appeal lifting
- apply_report_action now writes aksrt_user_restriction via Restriction::restrict
  and lifts via new lift_user_restriction(), replacing the display-only
  aksrt_user_ban_state meta dual-track
- ban type restrictions destroy the target user's sessions on apply
- appeal approval lifts active ban/suspend/mute restrictions instead of
  updating a nonexistent status column
- UsersBan.vue posts to /users/{id}/restrict with valid types (warn/mute/
  suspend/ban), scope and expires_at
- add comment report entry to the comment actions dropdown
- add duplicate pending report guard on submit
- remove dead ReportCenter ajax handlers, option migration, REST alias
  routes and write-only restriction meta
- fix report admin mail link to the real aksrt_user_ban page
- add report-ban contract tests
2026-09-19 21:38:56 +08:00
kate522 ad714bb59c Revert "fix(frontend): drop 12px font pin on header icon cards to match reference"
This reverts commit 6fcd63b923.
2026-09-19 17:06:19 +08:00
kate522 6fcd63b923 fix(frontend): drop 12px font pin on header icon cards to match reference
The reference theme renders header slider icon cards on the bootstrap
14px body baseline (title .em12 -> 16.8px, desc 14px). A local
.header-slider-card .icon-cover-card { font-size: 12px } override shrank
text to 14.4/12px, making AKSRT cards visibly smaller than the reference.
Remove the pin from base.css and the public bundle; container gutter
restoration stays.
2026-09-19 16:51:28 +08:00
kate522 cd7082454a fix(widgets): preserve badge HTML in icon card title/desc like reference theme
wp_kses_post() strips the custom <badge> element, so icon card title/desc
lost admin-authored markup (e.g. <badge class="ml6 jb-blue">GO</badge>)
both at save time and render time, diverging from the reference theme
which stores and echoes these fields raw.

- add shared inline whitelist (aksrt_icon_card_kses_tags/aksrt_icon_card_kses)
  and route both card renderers plus the media-brand fallback through it
- drop the dead unused $allow array in aksrt_icon_cover_card
- add 'rich_html' sanitize type (FieldSchema + SchemaProvider) and mark
  icon card title/desc fields in widget schemas, page display schema and
  the page config meta box save path
- align header slider card module with reference show_widget_bg branches
  (aksrt-panel/flex1 vs padding-10 wrapper/relative-h)
- update widget contract tests to pin the whitelist behavior
2026-09-19 16:39:59 +08:00
kate522 4653128a18 feat(checkin): open reference-parity details modal from signed buttons
- Rebuild checkin details modal HTML 1:1 with reference theme:
  colorful header c-blue, user card (points/level), daily reward
  card and 7-day continuous mini cards
- Signed entry buttons (header dropdown, user-card badge, user
  center) become clickable checkin-details-link modal triggers
  instead of disabled buttons
- Swap data-signin-btn to details link in place after successful
  signin (reference .initiate-checkin replacement behavior)
- Add aksrt-checkin-details-modal runtime shell (modal-mini 400px)
- Remove dead code: legacy lite modal, admin-ajax details handler,
  /signin/history route, signin calendar; rebuild public bundles
- Update contract tests to the new checkin modal contract
2026-09-19 14:49:58 +08:00
kate522 ae626ba1c8 refactor(referral): prune dead code and formalize ops list header
- remove unused AuthReader::invitation_mode and ReferralAttribution
  helpers (get_or_create_code / validate_ref); contract test now
  asserts the canonical invit_code_s reader in CreditsRuntime
- drop legacy aksrt_invite table from uninstall cleanup list
- track referral code creation time (aksrt_referral_code_created)
  and surface it in the referral ops REST list
- formalize ops list headers (归属用户/推广码/启用状态/推广用户数/
  有效付费用户数/创建时间) and right-align count columns
2026-09-19 12:54:22 +08:00
kate522 b375fcc55c chore(cleanup): 移除用户中心孤儿签到日历渲染器
credits-signin.php 已无任何引用(用户中心视图映射无 signin 项,profile-credits.php 亦不再挂载),整包删除,不保留兼容分支。

其专属 .uc-signin-calendar__title 样式与 public.css / public.min.css 产物同步移除;签到日历仍由 src/Credits/Signin.php 在签到弹窗内输出,enhancements2.js 的日历渲染逻辑保持不变。
2026-09-19 11:46:07 +08:00
kate522 32e76b440f fix(frontend): 用户中心余额与积分记录不再拼接括号内容
记录行只保留中文变动原因、时间与变动额,移除名称后的(引用键/备注/提现单号)拼接。

同步清理该改动产生的死代码:$record_note 计算块、.uc-transactions__remark 样式、LedgerQuery 中已无消费者的 financial_flow LEFT JOIN 与 withdrawal_number 字段,以及 credits-wallet.php 里恒为 true 的 $skip_wallet_ledger 双路分支。
2026-09-19 11:46:01 +08:00
kate522 0d617d88c9 fix(pay): 余额充值预设档位不再被 custom_price 参数校验拦截
选中任一预设充值档位点「立即支付」,必然弹「无效参数:custom_price」。这是 WP
REST 的参数校验层拦截,请求根本没进业务回调。

原因是 WP REST 的 default 只在「键完全缺失」时生效,键存在但值为空串时 default
不参与,空串会直接送进 pattern 校验:

  'custom_price' => ['type' => 'string', 'default' => '0', 'pattern' => '^(0|[1-9]\d*)(?:\.\d{1,2})?$']

而前端提交时无条件带上该字段(custom_price: custom),选预设档位时 custom 恒为
''。更隐蔽的变体:后台关掉「自定义充值金额」后模板不再渲染该输入框,customEl 为
null、custom 仍是 '',也就是只放开预设档位的配置下 100% 复现。

该字段只在 package=custom 时被消费,而金额格式与区间的业务裁决早已完整存在于
create_charge_order()(AssetAmount::balance 严格正则 + 溢出检查,再按 custom_limit
校验区间并返回本地化错误)。参数声明上的 pattern 是同一规则的第二份实现,两套口径
不同步即是本次缺陷来源。故本次按「单一口径」收口,不再采用「放宽 pattern + 回调归一化
空串 + 前端改送 '0'」的三处并行写法:

- BalanceServiceRest:custom_price 参数去掉 pattern、default 改为 ''、显式声明
  sanitize_callback;rest_charge_create 改为经声明式 args 用 $request->get_param()
  取值,不再手工重读 JSON 体
- credits.js:selectProduct 选中预设档位时清空自定义金额输入(否则框内残留数字会在
  提交时把 package 顶回 custom,静默覆盖用户刚点的档位);customInput 声明上移,去掉
  「先调用后赋值」的时序依赖
- balance-charge-modal.test.cjs:新增 2 项契约测试 / 6 条断言

清理的死代码:
- rest_charge_create 中手工 get_json_params() 并复刻 sanitize_text_field/sanitize_key
  的一段:与声明的 sanitize_callback 重复,使那些声明形同虚设(实为第二套取值路径)
- credits.js 提交处理器内重复读取 payment_method 的死分支(处理器开头已读取,中间无
  任何代码改动它)

放宽仅限「空串 / 键缺失」这一种情形:package=custom 而金额为空、非法或越界仍一律
400,007、abc、1e3、1.234、负数等仍被 AssetAmount::balance() 拒绝,失败方向仍是收紧。

行为变化(需知悉):
- 金额裁决权归 AssetAmount::balance(),其内部 trim() 使带首尾空格的金额被修剪后接受,
  此前被参数层 pattern 直接拒绝。这是「单一裁决者」的必然结果。
- 点选预设档位会清空自定义金额输入框,是修复该档位被静默覆盖的必要行为。

验证:node --test 全量 372 项 / 372 通过(原 370 项,新增 2 项);PHPUnit 44 项 /
106 断言通过;改动 PHP 文件 php -l 通过。变异验证:把 6 条关键断言跑在修复前的文件
内容上,6/6 转红,证明回归测试拦得住此缺陷。未做视觉验收,弹窗标记与 CSS 未改动。
2026-09-19 11:34:12 +08:00
kate522 ce21cc2b9b revert fa476af70e
revert refactor(pay): 充值商品以充值金额为唯一真值,移除销售金额与折扣

充值商品行此前是双值模型:price 充值金额决定到账、pay_price 销售金额决定支付,
pay_price 为 0 时回落到 price,否则即「付更少拿更多」的折扣。该模型把三处耦合在
一起——后台 schema 暴露销售金额输入、线格式首段携带支付额、下单时计价与到账分离。
只改其中一处就会出现「后台不再可配但前端仍在展示折扣」或「展示已合并但下单仍按
旧价扣款」的静默不一致,故本次三层一并收口,充值金额即实际支付金额。

后台 schema 有两份且必须同步:SPA 侧 recharge.js 负责渲染表单,PHP 侧 SchemaPayment
负责默认值与清洗。只改一份会让界面不再可配而默认值仍在写 pay_price。

线格式三段 "支付|到账|标题" 保持不动,仅为兼容历史已存的字符串配置:支付段不再读取
任何配置来源,恒等于充值金额,故历史配置里的折扣价会被忽略。旧数据无需迁移,pay_price
键留在库中只是不再被读取,从重建后的后台保存一次即自然消失。

行为变化(需知悉):
- 已配置折扣的站点,其用户将按充值金额全额支付,即对这些用户是涨价。这是「不做优惠
  和促销」的必然结果。
- parse_packages 的校验口径随之变松:有效性只由到账段决定,故 "abc|10|x" 这类首段
  非法、到账段合法的行会被解析为 10 元档,而旧实现两段都作金额校验、会丢弃。理由是
  支付段已不表达任何含义,因一个无用字段是垃圾就丢弃一条本来有效的档位只会让运营困惑。
  既有数据不受影响——旧格式化器写出的首段恒为数字,只有手改出的脏数据才可能命中。
  该取舍已写入方法文档。

- recharge.js / SchemaPayment:删除 pay_price 字段与三档默认值中的该键,help 去掉
  「同时支持设置折扣」
- PaymentRuntime::format_product_lines:不再读取 pay_price,支付段由充值金额充当
- BalanceServiceCharge::parse_packages:不再产出 pay_price 键,金额唯一来源为到账段
- BalanceServiceCharge::create_charge_order:支付额与到账额同源于 price
- credits-wallet.php:移除销售金额行、「省X」折扣徽标及仅服务折扣的差额计算
- BalanceServiceRest:仅注释改写,说明积分路径的 pay_price 是售价而非折扣
- 新增 tests/widget-system/recharge-product-price-contract.test.cjs:5 条跨层契约

范围边界:积分商品 pay_points_product 的 pay_price 是「积分|支付|标签」里的实际售价,
本身不含折扣语义,保留不动。契约测试以正向断言固定该边界,避免日后被误当作同类问题
一并删掉。

验证:新增契约测试 5 项通过;node --test 全量 375 项 / 375 通过(原 370 项);PHPUnit
44 项 / 106 断言通过;改动的 PHP 文件 php -l 全部通过。变异验证:把 create_charge_order
改回按 pay_price 计价、把 pay_price 重新注入模板,对应断言分别转红,证明回归测试拦得
住回退。另以反射调用真实代码做端到端执行验证 11 项全过,覆盖结构化数组 -> 线格式 ->
商品数组全链路、数组内残存 pay_price 被忽略、历史折扣字符串(98|100|旧折扣档 取 100)、
到账段为 0 或非法时跳过等边界。

部署提示:assets/admin/build 为部署时构建、未入库,后台表单的改动需在目标机执行
cd assets/admin && npm ci && npm run build 后才会生效。前端 credits.js 走 filemtime
版本号,无需手工清缓存。
2026-09-19 03:25:12 +00:00
kate522 633ab7abe3 revert c1a0eade38
revert fix(pay): 余额充值选中预设档位不再因空 custom_price 被 REST 拒绝

选中任一所预设充值档位点「立即支付」,必然弹「无效参数:custom_price」。这是
WP REST 的参数校验层拦截,请求根本没进业务回调。

原因是 WP REST 的 default 只在「键完全缺失」时生效,键存在但值为空串时 default
不参与,空串会直接送进 pattern 校验:

  'custom_price' => ['default' => '0', 'pattern' => '^(0|[1-9]\d*)(?:\.\d{1,2})?$']

而前端提交时无条件带上该字段(custom_price: custom),选预设档位时 custom 恒为
'',于是必然校验失败。另一个更隐蔽的变体:后台若关掉「自定义充值金额」,模板里
根本不渲染该输入框,customEl 为 null,custom 仍是 '',同样报错——也就是说这个
缺陷在只放开预设档位的配置下 100% 复现。

同文件的积分兑换路径(/pay/balance/points-exchange)早已写成
custom: validCustom ? custom : '0',余额充值这条只是漏了同样的处理。

- BalanceServiceRest:custom_price 的 pattern 改为整体可选分组
  ^(?:(?:0|[1-9]\d*)(?:\.\d{1,2})?)?$,显式放行空串;rest_charge_create 把空串
  归一化为 '0',与参数声明的 default 语义对齐,避免 AssetAmount::balance() 拿到
  空串后抛 balance_decimal_invalid
- credits.js:非自定义金额时显式送 '0',与积分兑换路径写法一致
- 契约测试补 4 条断言:模式可选、允许空串、回调归一化、前端送 '0'

两层都改是有意的:JS 会被浏览器与 CDN 缓存,旧缓存仍会发空串,后端放行兜住;
后端放行则保证任何客户端都不再被这个字段卡住。放宽仅针对空串,金额格式校验
不放松——007、abc、-1、1e3、1.234 与带首尾空格的值仍一律拒绝。

验证:新 pattern 逐例实测,''、0、1、9.99、9999.99 放行,007、abc、-1、1e3、
1.234、带空格全部拒绝;变异验证把 pattern 改回旧写法后该用例立刻转红,证明回归
测试确实拦得住此缺陷;node --test 全量 370 项 / 370 通过;PHPUnit 44 项 / 106
断言通过。断言未直接比对含反斜杠的整串——JS 字面量与 PHP 单引号串的反斜杠层数
不同会静默比对失败,故拆为不含反斜杠的片段。
2026-09-18 17:44:25 +00:00
kate522 fa476af70e refactor(pay): 充值商品以充值金额为唯一真值,移除销售金额与折扣
充值商品行此前是双值模型:price 充值金额决定到账、pay_price 销售金额决定支付,
pay_price 为 0 时回落到 price,否则即「付更少拿更多」的折扣。该模型把三处耦合在
一起——后台 schema 暴露销售金额输入、线格式首段携带支付额、下单时计价与到账分离。
只改其中一处就会出现「后台不再可配但前端仍在展示折扣」或「展示已合并但下单仍按
旧价扣款」的静默不一致,故本次三层一并收口,充值金额即实际支付金额。

后台 schema 有两份且必须同步:SPA 侧 recharge.js 负责渲染表单,PHP 侧 SchemaPayment
负责默认值与清洗。只改一份会让界面不再可配而默认值仍在写 pay_price。

线格式三段 "支付|到账|标题" 保持不动,仅为兼容历史已存的字符串配置:支付段不再读取
任何配置来源,恒等于充值金额,故历史配置里的折扣价会被忽略。旧数据无需迁移,pay_price
键留在库中只是不再被读取,从重建后的后台保存一次即自然消失。

行为变化(需知悉):
- 已配置折扣的站点,其用户将按充值金额全额支付,即对这些用户是涨价。这是「不做优惠
  和促销」的必然结果。
- parse_packages 的校验口径随之变松:有效性只由到账段决定,故 "abc|10|x" 这类首段
  非法、到账段合法的行会被解析为 10 元档,而旧实现两段都作金额校验、会丢弃。理由是
  支付段已不表达任何含义,因一个无用字段是垃圾就丢弃一条本来有效的档位只会让运营困惑。
  既有数据不受影响——旧格式化器写出的首段恒为数字,只有手改出的脏数据才可能命中。
  该取舍已写入方法文档。

- recharge.js / SchemaPayment:删除 pay_price 字段与三档默认值中的该键,help 去掉
  「同时支持设置折扣」
- PaymentRuntime::format_product_lines:不再读取 pay_price,支付段由充值金额充当
- BalanceServiceCharge::parse_packages:不再产出 pay_price 键,金额唯一来源为到账段
- BalanceServiceCharge::create_charge_order:支付额与到账额同源于 price
- credits-wallet.php:移除销售金额行、「省X」折扣徽标及仅服务折扣的差额计算
- BalanceServiceRest:仅注释改写,说明积分路径的 pay_price 是售价而非折扣
- 新增 tests/widget-system/recharge-product-price-contract.test.cjs:5 条跨层契约

范围边界:积分商品 pay_points_product 的 pay_price 是「积分|支付|标签」里的实际售价,
本身不含折扣语义,保留不动。契约测试以正向断言固定该边界,避免日后被误当作同类问题
一并删掉。

验证:新增契约测试 5 项通过;node --test 全量 375 项 / 375 通过(原 370 项);PHPUnit
44 项 / 106 断言通过;改动的 PHP 文件 php -l 全部通过。变异验证:把 create_charge_order
改回按 pay_price 计价、把 pay_price 重新注入模板,对应断言分别转红,证明回归测试拦得
住回退。另以反射调用真实代码做端到端执行验证 11 项全过,覆盖结构化数组 -> 线格式 ->
商品数组全链路、数组内残存 pay_price 被忽略、历史折扣字符串(98|100|旧折扣档 取 100)、
到账段为 0 或非法时跳过等边界。

部署提示:assets/admin/build 为部署时构建、未入库,后台表单的改动需在目标机执行
cd assets/admin && npm ci && npm run build 后才会生效。前端 credits.js 走 filemtime
版本号,无需手工清缓存。
2026-09-18 23:56:31 +08:00
kate522 c1a0eade38 fix(pay): 余额充值选中预设档位不再因空 custom_price 被 REST 拒绝
选中任一所预设充值档位点「立即支付」,必然弹「无效参数:custom_price」。这是
WP REST 的参数校验层拦截,请求根本没进业务回调。

原因是 WP REST 的 default 只在「键完全缺失」时生效,键存在但值为空串时 default
不参与,空串会直接送进 pattern 校验:

  'custom_price' => ['default' => '0', 'pattern' => '^(0|[1-9]\d*)(?:\.\d{1,2})?$']

而前端提交时无条件带上该字段(custom_price: custom),选预设档位时 custom 恒为
'',于是必然校验失败。另一个更隐蔽的变体:后台若关掉「自定义充值金额」,模板里
根本不渲染该输入框,customEl 为 null,custom 仍是 '',同样报错——也就是说这个
缺陷在只放开预设档位的配置下 100% 复现。

同文件的积分兑换路径(/pay/balance/points-exchange)早已写成
custom: validCustom ? custom : '0',余额充值这条只是漏了同样的处理。

- BalanceServiceRest:custom_price 的 pattern 改为整体可选分组
  ^(?:(?:0|[1-9]\d*)(?:\.\d{1,2})?)?$,显式放行空串;rest_charge_create 把空串
  归一化为 '0',与参数声明的 default 语义对齐,避免 AssetAmount::balance() 拿到
  空串后抛 balance_decimal_invalid
- credits.js:非自定义金额时显式送 '0',与积分兑换路径写法一致
- 契约测试补 4 条断言:模式可选、允许空串、回调归一化、前端送 '0'

两层都改是有意的:JS 会被浏览器与 CDN 缓存,旧缓存仍会发空串,后端放行兜住;
后端放行则保证任何客户端都不再被这个字段卡住。放宽仅针对空串,金额格式校验
不放松——007、abc、-1、1e3、1.234 与带首尾空格的值仍一律拒绝。

验证:新 pattern 逐例实测,''、0、1、9.99、9999.99 放行,007、abc、-1、1e3、
1.234、带空格全部拒绝;变异验证把 pattern 改回旧写法后该用例立刻转红,证明回归
测试确实拦得住此缺陷;node --test 全量 370 项 / 370 通过;PHPUnit 44 项 / 106
断言通过。断言未直接比对含反斜杠的整串——JS 字面量与 PHP 单引号串的反斜杠层数
不同会静默比对失败,故拆为不含反斜杠的片段。
2026-09-18 23:41:27 +08:00
kate522 ca9dc7794f refactor(pay): 流水号扩至 22 字符,移除资金流水归档方案与删单守卫
四件事合在一笔,因为它们落在同一批文件里——OrderRepositoryCore 与流水契约测试
同时承载号码规则与订单清理逻辑,硬拆会产生引用不通的中间提交。

1) 号码熵。{YmdHis}{6 位随机} 在同一秒内只有 10^6 的取值空间,高并发下单会因
   随机后缀碰撞触发 uniq_order_number / uniq_flow_number 冲突(每秒 3 单约
   2.6 天撞一次,每秒 10 单约每天 4 次)。后缀扩到 8 位后空间升至 10^8
   (+6.6 bit),撞号频率下降两个数量级。
   选 8 位纯数字而非等长 base36:保持定长纯数字形态,号码仍可被运营从截图或
   邮件整段复制、电话口述;且表默认排序规则是 utf8mb4_*_ci(大小写不敏感),
   混入大小写字母会让唯一索引把 aB 与 Ab 判为重复行,反而凭空制造冲突。22 字符
   仍满足 VARCHAR(32) 与网关 out_trade_no ≤ 32,拼上重试后缀 P999 后为 26 字符。

2) 自带订单号形态校验。create() 此前对调用方传入的 order_number 不做检查,传数组
   会因 (string) 转换把字面量 "Array" 写进列。现收紧为「全数字、20 或 22 位」,
   不合规即弃用并回落到分配器(WP_DEBUG 下留痕)。放行 20 位是为了兼容扩位前
   分配的历史号码——该方法原有的注释就声明服务于导入、补单与幂等重放。

3) 流水表补 KEY idx_flow_created (created_at)。后台「资金流水」页默认视图是
   WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n 加同条件 COUNT(*),界面
   上的日期区间筛选也只有 created_at 可用。此前没有任何以 created_at 为前导列的
   索引(idx_flow_event_time 的前导列是 event_type,筛选时可空),于是每次翻页
   都是全表扫描 + filesort,且与表大小无关地重复付出。

4) 移除归档方案与删单守卫。归档表 + 每日清理 cron 需要配套的归档浏览入口才闭环,
   未闭环时运营会误以为记录丢失;而第 3 条的索引已覆盖它要解决的主要问题,且
   不必在财务表上执行 DELETE。一并撤回删单 / 清理订单时对财务记录的拦截。

- BusinessNumber:后缀位数与上界收敛为 SUFFIX_DIGITS / SUFFIX_MAX,去掉三处
  重复的魔法数字;类文档写明选型依据,避免日后被改回字母方案
- OrderRepositoryCore:新增 ORDER_NUMBER_PATTERN 与 is_scalar 守卫;
  clear_closed_orders 与 delete_order 恢复单路签名(去掉 $force 与
  $include_orders_with_records)
- DBMigratorPart2:流水表新增 idx_flow_created,并删除两张归档表 DDL
- PayOrders:清理与删除订单回到单路处理,移除 orders_clear_blocked /
  order_delete_blocked 两条提示
- 删除 FinancialFlowRetention 类与 functions.php 中的引导注册
- 测试:BusinessNumberTest 期望值改 22 字符、越界用例改 10^8、补纯数字与 P999
  余量断言;流水契约测试并入索引断言、归档移除的反向断言、号码重试与形态断言

按用户要求不写双路逻辑与数据迁移。新索引依赖 dbDelta 在下次迁移时补到既有表;
若此前已跑过迁移,两张归档空表需手工 DROP(未写迁移,故不自动处理)。

验证:PHPUnit 44 项 / 106 断言全部通过;node --test(已追踪用例)333 项 /
333 通过,含本地专用用例共 370 项全部通过;php -l 全部改动的 PHP 文件通过;
残留引用扫描确认 FinancialFlowRetention、两张归档表名、has_financial_records、
count_closed_orders_with_records、financial_records_exists_sql、两条 blocked
提示、$force 与 $include_orders_with_records 均已无残留。
2026-09-18 23:17:36 +08:00
kate522 3bec2a5777 fix(icons): 图标类名统一为 FA5 语法并修正一处失效图标名
主题打包的是 Font Awesome 5,solid 字形必须带样式类 fas。全站有 18 处仍写成
FA4 的裸 fa fa-*,FA5 下并不存在对应字形,表现为图标位置空白;而 FA5-only
契约此前只扫描部分运行时文件,没有覆盖模板与渲染器,这些写法一直没被拦下。

同时发现一处真实的失效图标名:profile-level.php 用的是 fa-line-chart。这是
FA4 名称,FA5 把该系列重命名为 chart-*,正确写法是 fa-chart-line。

- 18 处 "fa fa-*" 补上 fas:首页模板、组件标记、评论表单、用户等级页、菜单
  装饰、SchemaPayment 促销标签默认值、VIP 服务弹窗、ContentRenderer /
  GeneralRenderer、后台卡片样式校验、pay-vip.js
- profile-level.php 的 fa-line-chart 修正为 fa-chart-line
- 三处注释里的示例值同步为合法形态("fa fas fa-star"),避免被当成正确写法
  抄走;credits.css 注释中直接写出旧字体族名的一处改为描述性表述,否则会被
  FA5-only 契约当作违规命中
- legacy-navigation-contract:FA 资产断言改为跟随 aksrt_enqueue_fontawesome()
  这一间接层(注册点已从脚本文件移到 inc/helpers/aksrt-icons.php),并把
  fa-zibsvg-<key> 识别为项目自有 SVG sprite 族,而非 Font Awesome 字形
- vip-points-exchange-modal 契约同步断言 fa fas fa-angle-right

验证:改动前在干净 HEAD 上跑 node --test(已追踪用例)为 327 项 / 325 通过 /
2 失败,两项失败正是本次修复的失效图标名断言与 FA 版本号断言;改动后 333 项 /
333 通过(含 5 个本地专用用例共 370 项全部通过)。php -l 全部改动的 PHP 文件
通过。
2026-09-18 23:17:05 +08:00
kate522 aad83072a8 refactor(pay): 统一流水号,移除订单/流水/提现三类号码前缀
订单号、资金流水号、提现单号此前各自带前缀配置,同一笔业务在不同界面会
呈现多个号码口径,对账与客诉排查都要先做一次前缀换算。现收敛为单一流水号
分配器,不再按业务类型区分号码形态。

- 删除 payment_order_prefix / payment_flow_prefix /
  payment_withdrawal_prefix 三项配置项及其生成、校验链路
- BusinessNumber::allocate() 成为唯一入口,输出 {YmdHis}{6 位随机},
  共 20 位无前缀,满足 VARCHAR(32) 与网关 out_trade_no ≤ 32 的限制
- 订单、资金流水、提现、卡密渠道交易号四处生成点全部改走该分配器
- 后台支付中心表头「订单号 / 提现单号」统一为「流水号」
- 前台用户中心与订单卡片文案同步统一;前台订单详情合并为单行流水号,
  账本流水号不再下发到前台,避免同一单据出现两个对外号码
- 同步更新 financial-flow / pay-card / card-pass-order-numbers 契约测试
  与 BusinessNumberTest 断言

按用户要求不做双路逻辑与数据迁移,直接替换为统一方案,历史行不做转换。

验证:node --test tests/**/*.test.cjs 362/364 通过,新增 37 项契约;失败的
2 项在改动前的干净 HEAD 基线上同样失败(基线 327 项 / 失败 2 项),分别命中
遗留的 fa fa- 标记与 FA 版本号断言,与本次改动无关。phpunit 44 项 / 103
断言全部通过;php -l 全部改动的 PHP 文件通过。
2026-09-18 22:14:27 +08:00
kate522 8b999444b2 fix(icons): 图标卡片支持内置 SVG 与自定义 SVG,修复前端图标不显示
顶部多功能组件的图标卡片只能显示 FA5 图标。后台「选择图标 → 内置 SVG」
页签写入的是 zibsvg-{key},但渲染器把它拼成了
<i class="fa fas fa-zibsvg-hot">,浏览器没有这个名字的字形,前端图标位置
空白。

根因:aksrt_icon_cover_card() 与 aksrt_icon_card() 只实现了 FA 类名一条
路径,未走全站统一渲染器 aksrt_icon_markup();函数内那条用于跳过前缀拼接的
判断用的是 aksrtsvg-(任何选择器都不会产生的废弃前缀),形同虚设。

- 新增 aksrt_card_icon_inner() 统一解析器,横版 / 竖版卡片均委派给它,
  FA5 类名、zibsvg- 内置图标、customsvg- 内联 SVG 三种格式统一渲染
- aksrt_icon_card() 的 icon 值改用 aksrt_sanitize_icon_value(),
  原 sanitize_text_field 会把 customsvg-<svg> 的标签整段剥成裸前缀
- SchemaProvider::sanitize_field_value() 补 type === 'icon' 分支:
  分组 / 重复器内的图标子字段原先落到 sanitize_text_field,
  header_slider_card_option.cards[].icon 的 customsvg- 在保存时即被剥空
- PageConfigMetaBox::sanitize_card_options() 同步改用图标净化
- aksrt_svg_kses_tags() 补齐 circle / ellipse / rect / polygon / line,
  与选择器端 cleanSvg() 的接受范围对齐;原先粘贴以 <circle> 为主的图标
  会在 kses 阶段被静默剥离形状
- icon-card-contract 新增 3 项契约

内置 SVG 的 sprite 由 svg-icon.js 每页注入 body,无需额外入队。

未写数据迁移;已存量的 zibsvg- / FA 值无需转换,customsvg- 存量值本就被
旧净化逻辑破坏,不在此次范围内。

验证:node --test tests/widget-system/*.test.cjs 311 项全部通过(原 308 +
新增 3);php -l 4 个改动文件通过;隔离 PHP 探针覆盖 9 种取值并走完整
「保存净化 → 前端渲染」链路,修复前 / 后输出逐项对比。
2026-09-18 20:05:14 +08:00
kate522 95ff0761b7 fix(vip): 积分兑换弹窗改服务端渲染并与 Zibll 1:1 对齐
/user/vip/ 的积分兑换会员弹窗原先是前端自行拼装的,结构与层级都与参考主题
Zibll 不一致:Zibll 走 zibpay_get_vip_points_exchange_modal() 服务端渲染静态
壳,前端只按需开合。改为同一模型,前端不再拼 HTML。

- VipServiceModal 新增 get_points_exchange_modal_html():门控(未登录/未开启
  返回空串)、vip-card 套餐选择、积分不足提示与提交表单;wp_footer 挂载输出
- pay-vip.js 移除 renderPointsModal / pointsPackage / exchangeFromModal /
  vipPointsPay,改为自包含开合(.in + 内联 display:flex + 独立遮罩 +
  body.modal-open + ESC),保留 aksrt-vip-points-toast 的 load/success/danger 契约
- 移除 /user/vip/ 页面底部独立积分兑换入口及其样式(uc-vip-points-link)
- credits.js 清理依赖全站不存在选择器的死代码 initVipPointsExchange()
- vip-modal.css 删除 71 行自实现样式,改为 .modal[data-aksrt-vip-points-modal]
  显隐契约;层级 100050 < 遮罩 100055 < 弹窗 100060 < notyf 10000000
- 新增 vip-points-exchange-modal 契约测试(7 项),toast-contract 同步

静止态必须显式 display:none,否则 .flex{display:flex} 会覆盖 .modal{display:none}
形成隐形遮罩拦截整页点击。

未写数据迁移。

验证:node --test tests/widget-system/*.test.cjs 308 项全部通过;php -l 全部
改动文件通过;隔离渲染桩确认积分充足 / 积分不足 / 功能关闭 / 未登录四种情形
的 HTML 产物正确。
2026-09-18 20:05:05 +08:00
kate522 d2447f8aa6 fix(pay): 修复后台收款接口显示顺序不可用并接入收银台排序
后台「收款接口 → 收款接口显示顺序」有三处缺陷叠加:
1) sortable 渲染组件读取 item.label,而 schema 写的是 title,导致 6 行
   只剩上下箭头;
2) SchemaProvider 的 sortable 分支遍历 items 定义重建数组,而该字段的
   键顺序本身就是配置值,用户排序被静默改回 schema 静态顺序;
3) 真正构建收银台支付方式列表的 aksrt_list_cash_payment_methods() 使用
   硬编码顺序,Gateway::get_available()/apply_sdk_order() 那套排序逻辑
   无任何调用方,配置对前台毫无影响。

改为:schema 项统一使用 label(JS 与 PHP 两处对齐);sortable 持久化按
提交键顺序重建,items 定义仅作白名单;新增唯一排序实现
aksrt_sort_payment_methods()(语义对齐参考主题 zibpay_get_payment_methods
+ pay_sdk_order 键顺序),并接入内容收银台、余额充值、购买积分与会员
购买四个入口。卡密输入盒改为按渠道存在与否判断,不再假定卡密固定在末位。

清理死代码:无调用方的 Gateway::get_available/apply_sdk_order/
get_status_summary、VipServicePackages 中不可达的支付方式列表回退分支、
零引用的 settings-defaults 模块及 schema/index.js 未使用的导出、支付
sortable 的 fields 死元数据、无拖拽实现却声明的 cursor:move。

未配置 pay_sdk_order 时顺序保持不变;未写数据迁移。

验证:php -l 8 个改动文件全部通过;新增 pay-sdk-order-contract 契约测试
(6 项);node --test tests/widget-system/*.test.cjs 301 项全部通过;
后台 SPA 已重新构建。
2026-09-18 19:01:19 +08:00
kate522 03871f0342 fix(pay): 渠道名称禁折行,消除三字渠道名的零余量换行
实测(bootstrap+main 真实加载组合):芯片文本行可用宽度 46px 对
三字渠道名「支付宝」(雅黑 14px×3=42px)近乎零余量,站点字体渲染
的细微差异(字距/亚像素舍入)即触发 CJK 逐字换行,参考主题实测为
恒单行。

- .payment-method-radio > div 增加 white-space:nowrap,锁定与参考
  主题同等的单行表现(渠道名集合均为 2-3 字短名,无溢出风险)
- main.min.css 重建,契约测试锁定(323 项全部通过)
2026-09-18 18:38:07 +08:00
kate522 89801236eb fix(pay): 方法行还原 Zibll 原样(不换行),项目收缩成芯片一行排满
上一轮给方法行加的 hh(flex-wrap:wrap)方向错误:行级换行使项目保持
「图标左+文字右」的宽形态直接断行(3+2 布局),永远不会收缩成参考
主题的芯片形态。Zibll 的真实机制是行不换行、项目经 flex 收缩后由项
内 hh 将文字折到图标下方,形成「图标上/文字下」窄芯片一行排满。

- 三处方法行还原为 flex mb10(与参考主题逐字一致):充值/积分模板
  与会员 pay-vip.js
- 图标不变形由既有防护保障(flex:none + width:auto + max-width:none
  抵消 bootstrap 全局 img 钳制)
- 无头 Chrome 实测(bootstrap+main 真实加载组合,400px 弹窗):5 渠
  道一行 5 个芯片(62-73px、图标 35px),6 渠道一行 6 个(53-62px、
  stripe 宽形 84px),与 Zibll 同 fixture 形态一致
- 契约测试同步:行标记锁定无 hh、img 四属性防变形断言保留(322 项
  全部通过)
2026-09-18 18:26:50 +08:00
kate522 be07db3409 fix(pay): 抵消 bootstrap 全局 img max-width 对支付图标的钳制
AKSRT 的 bootstrap.min.css 将 BS3 .img-responsive 选择器组误并入了裸
img(img{display:block;max-width:100%;height:auto};Zibll 的裸 img 不
在该组),导致 flex 压缩场景下 max-width:100% 把支付方式图标钳制到被
压窄的项宽——上一轮的 flex:none 防不住 max-width,5 渠道时图标仍可
被压成细条。

- .payment-method-radio img 显式还原 width:auto + max-width:none
  (连同上一轮 flex:none,图标在任何压缩场景恒为固有比例宽)
- 契约测试改为提取规则块逐属性断言(原正则被规则内注释的右花括号
  截断)
- 无头 Chrome 真实加载组合(bootstrap+main)实测:5 渠道 400px 弹窗
  图标 35x35、项宽 112-183px,无任何挤压
2026-09-18 18:18:31 +08:00
kate522 e9dd928859 fix(pay): 多渠道时支付方式图标不再被 flex 压缩变形
用户配置 5-6 个收款渠道时(400px modal-mini 内 6 项),方法行项目
被 flex 压缩导致图标变形:方形 logo 被压至 31px/22px,宽形 stripe
logo(2468:1024)被压成 35px 方形。经无头 Chrome 几何实证,参考主题
同场景存在完全相同的缺陷(两主题 main.css 该组件逐条一致),属 Zibll
组件固有问题而非复刻偏差;按用户要求修复且不改变正常场景视觉:

- main.css:.payment-method-radio img 增加 flex:none(图标恒为固
  有比例宽,高 35px 不变);main.min.css 经 build-public-assets 重建
- 三个弹窗方法行加 hh(.flex.hh{flex-wrap:wrap}):空间不足时整行
  换行而非压缩图标——充值/积分(服务端模板)与会员(pay-vip.js)
  三处同步
- 契约测试锁定 img flex:none 与三处行标记(322 项全部通过)
- 验证:无头 Chrome 实测 6 渠道 370/360/330/300px 容器下全部图标
  恒定固有比例宽度,行自动换行
2026-09-18 18:13:58 +08:00
kate522 6075028520 fix(pay): 支付方式字样与按钮 UI 三弹窗统一对齐 Zibll
渠道展示名与按钮 UI 与参考主题 zibpay_get_payment_method_args /
zibpay_get_initiate_pay_input 对齐:

- 渠道名:aksrt_list_cash_payment_methods 增加 Zibll 基准展示名映射
  (余额/支付宝/微信/PayPal/Stripe),充值与会员弹窗不再显示网关
  内部名「站内余额」「微信支付」;积分弹窗手写「余额/卡密」与之同源
- 会员按钮字样:cta_label(立即开通/立即续费)→ Zibll 会员弹窗
  initiate_pay_input(4) 默认「立即支付」,与充值/积分弹窗一致
- 无可用支付方式:禁用按钮+自实现文案 → Zibll 同款
  <span class="badg px12 c-yellow-2">暂时无法支付,请与客服联系</span>
- 清理:按钮残留类 aksrt-vip-submit 与对应 CSS,加载/禁用态统一挂
  .initiate-pay(三弹窗按钮共用);renderMethods 空数组重复提示与
  不可达 reason 映射删除
- 契约测试同步锁定(无 cta_label/aksrt-vip-submit、立即支付字样、
  badg 提示、渠道名映射)

弹窗宽度差异(充值/积分 400px modal-mini、会员 600px)为 Zibll
原生设计,已在壳几何对齐时确认一致。
2026-09-18 17:48:54 +08:00
kate522 ec1107c65a fix(vip): 会员弹窗壳几何 1:1 对齐 Zibll(600px/整屏高度阈值/8px 圆角)
滚动条与尺寸偏差根因是 aksrt-modal-runtime 壳覆盖偏离参考主题
modal_pay_uservip 的几何:

- 宽度 min(560px,100%) → min(600px, calc(100vw - 20px)):对齐
  bootstrap3 ≥768px .modal-dialog 600px + .modal.flex .modal-dialog
  的 max-width calc(100% - 20px)
- 高度 min(90vh,760px) → calc(100vh - 20px):760px 阈值过低是侧边
  滚动条根因;对齐 .modal.flex .modal-content 的整屏阈值
- overflow auto → hidden + overflow-y auto + touch 滚动:对齐
  .modal.flex .modal-content,正常桌面内容不再出现滚动条
- 圆角 12px → var(--main-radius, 8px):对齐 .modal-content
- 移除 .payvip-modal 本地 padding(18/16px),内边距回归 .box-body
  15px(参考主题无 .payvip-modal 专属 padding)
- 契约测试同步锁定桌面壳几何、无本地 padding 与移动端 bottom 圆角
2026-09-18 17:34:00 +08:00
kate522 db0d63b99f fix(pay): 会员卡密单密码默认关闭,卡密盒与其他弹窗一致显示卡号+密码
会员购买/续费弹窗卡密盒此前仅显示卡密输入,与其他两个弹窗(卡号+密码)
不一致。根因是 pay_vip_pass_charge_only_password 的默认值偏离参考主题:

- PaymentRuntime:vip 类型 only_password 由「未设置即视为开启」改为
  !empty(),与其他两类型同构(参考主题 default false)
- ChargeCard:get_public_options 的 vip 回退值 only_password true→false
- SchemaPayment + recharge.js:会员卡密单密码模式开关 default true→false
- 契约测试同步锁定默认关闭与无 isset 回退
- admin SPA 重新构建

已保存过设置且该键被持久化为开启的站点,需在后台「余额充值 → 卡密
兑换 → 会员卡密单密码模式」手动关闭一次。
2026-09-18 17:26:50 +08:00
kate522 721fa29b8d fix(vip): 会员弹窗支付方式渠道对齐 Zibll initiate_pay_input 组件
支付方式 UI 不再自实现(uc-vip-shop__method 文本 radio),改为与其他
两个弹窗(充值/积分)完全同源的 Zibll zibpay_get_initiate_pay_input
组件结构:

- 支付方式行:.payment-method-radio.hollow-radio + logo 图 +
  data-for/data-value 选中模型(多渠道时展示「请选择支付方式」)
- 我的余额盒:data-controller == balance 条件显隐,展示货币符号与余额
  (视图模型透传 wallet_balance / currency_mark)
- 卡密盒:Zibll password_box 同构(mb10 padding-h10 padding-w6 +
  desc muted-box + form-control 卡号/卡密输入)
- hidden payment_method + initiate-pay 按钮类(mt6 but jb-red
  btn-block radius)
- 服务端 payment_method_logo_url() 统一映射 pay-*-logo.svg
- 显隐统一走 data-controller(==/!=)通用匹配,卡密选中隐藏 .charge-box
- 清理 uc-vip-shop__method* 死样式与旧 change 监听;契约测试同步锁定
  (.gitignore 白名单补 card-pass-modal-redeem.test.cjs)
2026-09-18 12:44:38 +08:00
kate522 e72d0710e6 fix(frontend): 购买积分弹窗壳静止态隐藏,修复整页不可点击回归
根因与 2026-09-17 充值弹窗故障同构:新增的购买积分弹窗壳同为服务端
静态输出(.modal.flex.jc.fade),credits.css 仅对充值壳做了静止态
display:none 压制,积分壳被 main.css 的 .flex{display:flex} 拉起永久
铺满全屏,叠加 .fade 的 opacity:0 形成隐形遮罩拦截整页点击。

修复:credits.css 追加 .modal[data-aksrt-points-pay-modal]{display:none}
与 .in{display:flex};打开态由 initPointsPay 的内联 display:flex 驱动,
关闭撤除内联样式后即回到隐藏规则。balance-charge-modal 契约测试同步
纳入积分壳护栏,防同类回归。
2026-09-18 12:31:38 +08:00
kate522 b88eab0bfe feat(pay): 卡密兑换入口收敛到三个购买弹窗并对齐参考主题
- 移除余额卡片独立「卡密兑换」通用弹窗,兑换入口分布到充值/积分/会员三个弹窗的支付方式中
- 充值弹窗补齐卡密 logo 与仅卡密时的金额选择隐藏逻辑
- 积分购买弹窗 1:1 复刻参考主题 points-pay modal(商品档位/自定义/支付方式/立即支付),REST 改为 product+custom 契约
- 会员弹窗注入卡密渠道:视图模型透传 card_pass,前端渲染卡密盒并直兑 /pay/card-pass/redeem type=vip
- 后台卡密兑换拆分为余额/积分/会员三个独立开关,VIP 段旧会员卡密组清理
- REST type 改为必填(balance/vip/credits),三类型独立闸门
- 测试重写为 card-pass-modal-redeem 契约,321 项全部通过
2026-09-18 12:25:18 +08:00