@shzlx/shop
v0.0.3
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 商品已按安全规范进行内容逃脱和验证
