@ddj-v2/shop
v0.2.1
Published
HydroOJ shop module
Readme
shop
HydroOJ 硬幣與兌換商店外掛(繁體中文文件)。 以 https://github.com/cqzym1985/my-hydro-plugins 為基礎並新增供其他套件接入的功能
功能
- 硬幣發放與批次匯入。
- 商品管理與兌換。
- 支援商品
objectId(可重複綁定同一物件)。 - 支援無限供應(
num < 0)。 - 商品
description支援 Markdown,商城與兌換頁以原生|markdown|safe渲染。 - 可插拔購買邏輯(供其他套件接入)。
- 可註冊管理頁擴充入口(獨立頁面:
/shop/manage/entries)。 - 已提供 runtime
shopBridge,避免外掛間使用脆弱的相對路徑靜態匯入。
路由(目前啟用)
coin_show/coin/showcoin_inc/coin/inccoin_import/coin/importcoin_bill/coin/billcoin_mall/coin/mallcoin_myrecord/coin/myrecordcoin_exchange/coin/exchange/:idcoin_record/coin/recordgoods_add/goods/addgoods_manage/goods/manageshop_manage_entries/shop/manage/entriesgoods_edit/goods/:id/edituname_change/uname/changedomain_coin_setting/domain/coin
註:coin_gift 路由在目前版本預設未啟用(程式內已註解)。
對外 API
shop/index.ts 對外提供:
registerGoodsPurchaseModel(modelId, model)registerShopManageEntry(entry)getShopManageEntries()CoinModelGoodsModel
如何調用原生 method 發放硬幣
推薦方式:使用 CoinModel.inc(會寫入發放紀錄)
CoinModel.inc 會同時:
- 新增一筆硬幣帳單紀錄(
coincollection) - 調整使用者
coin_now - 當
asset = 1時,同步增加coin_all
import { CoinModel } from '../shop';
// 例:管理員給使用者 +20 硬幣
// 參數:userId, rootId, amount, text, asset, status?
await CoinModel.inc(targetUid, operatorUid, 20, '活動獎勵', 1);參數說明:
userId: 收款人 uidrootId: 操作者 uid(誰發放)amount: 正數加幣,負數扣幣text: 帳單說明asset:1代表會計入coin_all,0只變動coin_nowstatus(選填): 可用於綁定商品 ID 或其他狀態碼
進階方式:直接調用 Hydro 原生 UserModel.inc
若你只想調整餘額、不要寫入硬幣帳單,可直接使用 Hydro 原生方法:
import { UserModel } from 'hydrooj';
await UserModel.inc(targetUid, 'coin_now', 20);注意:這種做法不會留下 coin_bill 可查的發放紀錄,通常不建議拿來做正式發幣流程。
另外,執行時也會提供:
global.Hydro.shopBridgectx.provide('shop_bridge', shopBridge)
shopBridge 內容:
goodsModelregisterGoodsPurchaseModelregisterShopManageEntry
建議接入方式(runtime bridge)
interface ShopBridge {
goodsModel: {
add: (
name: string,
price: number,
num: number,
objectId?: string,
goodsId?: number,
purchaseModelId?: string,
data?: Record<string, unknown>,
description?: string,
redirectUrl?: string,
) => Promise<number | string>;
};
registerGoodsPurchaseModel: (
modelId: string,
model: {
purchase: (
uid: number,
goods: any,
amount: number,
) => Promise<boolean | { success: boolean; message?: string }> | (boolean | { success: boolean; message?: string });
}
) => void;
registerShopManageEntry: (entry: { key: string; title: string; href: string }) => void;
}
function getShopBridge(): ShopBridge | null {
return (global.Hydro as any)?.shopBridge || null;
}
const shopBridge = getShopBridge();
if (shopBridge) {
shopBridge.registerGoodsPurchaseModel('example_model', {
async purchase(uid, goods, amount) {
// 成功
return true;
// 或失敗(帶訊息)
// return { success: false, message: '你已擁有此商品' };
},
});
shopBridge.registerShopManageEntry({
key: 'example_manage',
title: '外掛管理入口',
href: '/example/manage',
});
}使用 ctx.inject 的注意事項
- 此專案整合建議使用:
ctx.inject(['Shop'], ...)(大寫) - 請保持與外掛實際註冊/載入方式一致,避免 inject key 不匹配
// 建議用法
ctx.inject(['Shop'], (c) => {
const shopBridge = (global.Hydro as any)?.shopBridge;
if (!shopBridge) return;
// ...
});registerShopManageEntry 的 href 建議寫法
下面提供一套可重複使用的最小模板,適合用在你自己的外掛管理頁。
1) 先註冊管理入口(key, title, href)
shopBridge.registerShopManageEntry({
key: 'example_manage',
title: 'Example 管理',
href: '/example/manage',
});重點:
- key 要全域唯一,建議用與
ctx.Route的相同(例如 example_manage)。 - href 請使用固定路徑,不要帶動態參數,方便管理頁入口穩定顯示。
- title 建議用清楚動詞,例如 新增、發佈、同步、設定。
2) 對應 href 的 Handler 範本
import { Context, Handler, PERM, Types, param } from 'hydrooj';
class ExampleManageHandler extends Handler {
async get() {
this.checkPerm(PERM.PERM_SET_PERM);
this.response.template = 'example_manage.html';
this.response.body = {
page_name: 'example_manage',
message: '',
};
}
@param('name', Types.String)
async post(domainId: string, name: string) {
this.checkPerm(PERM.PERM_SET_PERM);
// TODO: 在這裡放你的業務邏輯
this.response.template = 'example_manage.html';
this.response.body = {
page_name: 'example_manage',
message: `已完成:${name}`,
};
}
}
export function applyExample(ctx: Context) {
ctx.Route('example_manage', '/example/manage', ExampleManageHandler, PERM.PERM_SET_PERM);
}3) 可重複使用的 HTML 模板範本
{% extends "coin_base.html" %}
{% block coin_content %}
<div class="section">
<div class="section__header">
<h1 class="section__title">{{ _('Example 管理') }}</h1>
</div>
<div class="section__body">
{% if message %}
<blockquote class="note typo">
<p>{{ message }}</p>
</blockquote>
{% endif %}
<form method="post">
{{ form.form_text({
label: '名稱',
name: 'name',
required: true
}) }}
<button type="submit" class="rounded primary button">{{ _('提交') }}</button>
</form>
</div>
</div>
{% endblock %}4) 命名與落地建議
- Route name、page_name、manage entry key 建議統一同一前綴,便於維護。
- 模板建議放在外掛自己的 templates 目錄,避免和其他外掛重名。
- 若頁面是資料建立型流程,成功後可保留在原頁並顯示 message; 若是清單型流程,建議 redirect 到清單頁。
5) 常見錯誤
- href 打錯(例如寫成 herf)導致入口可見但無法進頁。
- Route 權限比入口預期高,造成點得進去但被拒絕。
- page_name 未設定,導致側欄 active 樣式不正確。
商品資料欄位
GoodsModel 主要欄位:
_id: number商品 IDobjectId?: string物件 ID(可重複)name: string商品名稱description?: string商品描述descriptionFormat?: 'markdown' | 'html'描述格式(預設markdown)redirectUrl?: string購買成功後跳轉連結(支援/path或http(s)://)price: number商品價格num: number庫存(-1或任何< 0代表無限)purchaseModelId?: string購買處理器 IDdata?: Record<string, unknown>擴充資料
外掛整合範例(徽章)
徽章外掛可在發佈商品時:
name使用badge.titledescription使用badge.contentpurchaseModelId使用badge_purchaseredirectUrl可設為/badge/mybadge(購買成功後直接跳到我的徽章)
如此可讓商城直接以 Markdown 顯示徽章說明內容。
注意事項
- 兌換有限庫存商品時會先扣庫存;若外掛處理器失敗,系統會自動回補庫存。
- 無限供應商品(
num < 0)不會扣庫存。 - 購買處理器可回傳
false或{ success: false, message }拒絕兌換;若有message會直接顯示給使用者。 - 取消訂單流程在目前版本已停用(
coin_myrecord的POST會回覆功能已停用)。
⚠️ HTML 渲染安全提醒
只有具有足夠權限的管理員或信任使用者才應該允許使用 HTML 渲染功能(descriptionFormat: 'html')。
- HTML 渲染允許使用者輸入 HTML 標籤,系統雖會進行安全清理(移除
<script>、事件屬性等),但建議:- 限制使用者權限:只給有管理權限的人士添加/編輯使用 HTML 格式的商品
- 定期審計:檢查已發佈的 HTML 商品內容,確認無不當內容
- 依賴清理函數:系統會自動清理危險標籤,但防禦原則是多層次的
- 若沒有明確需求,建議預設使用 Markdown 格式(更安全、更易維護)
- 外掛程式(如徽章)發佈的 HTML 商品已按安全規範進行內容逃脫和驗證
