货币体系: 全平台统一使用积分(POINT)替代人民币(CNY)

后端:
- 模型 currency 默认值 CNY→POINT (MerchantProduct/WalletAccount/FulfillmentOrder)
- DashboardStats TotalSales/TotalFees 从 float64 改为 int64,去掉 /100.0 转换
- 上游 OpenOrderQuery.Amount 从 float64 改为 int64,直接返回积分整数
- 钱包/商品创建时 currency 默认 POINT

前端:
- 去掉 centsToYuan/yuanToCents 转换函数,金额直接用整数积分
- money() 显示从 ¥X.XX 改为 X 积分
- 商品售价/成本表单字段名改回 price_amount/cost_amount,precision 改 0
- 钱包调整表单 amount_yuan→amount,precision 改 0
- 手续费固定金额表单 precision 改 0,label 改积分
- 币种选项 CNY→POINT
- Dashboard 成交金额/手续费 suffix 元→积分,去掉 precision

数据库:
- 新增迁移 004: currency 默认值 CNY→POINT,存量数据更新
This commit is contained in:
yml2213
2026-07-30 14:30:34 +08:00
parent bafe41a3ea
commit 0de0ad9e9d
9 changed files with 52 additions and 69 deletions
+1 -1
View File
@@ -163,6 +163,6 @@ func ensureSelfMerchant(tx *gorm.DB) (*model.Merchant, error) {
func ensureWallet(tx *gorm.DB, merchantID uint) error { func ensureWallet(tx *gorm.DB, merchantID uint) error {
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&model.WalletAccount{ return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&model.WalletAccount{
MerchantID: merchantID, MerchantID: merchantID,
Currency: "CNY", Currency: "POINT",
}).Error }).Error
} }
@@ -0,0 +1,7 @@
-- 全平台统一使用积分(POINT)作为结算货币,不再使用人民币(CNY)。
ALTER TABLE merchant_products ALTER COLUMN currency SET DEFAULT 'POINT';
ALTER TABLE wallet_accounts ALTER COLUMN currency SET DEFAULT 'POINT';
ALTER TABLE fulfillment_orders ALTER COLUMN currency SET DEFAULT 'POINT';
UPDATE merchant_products SET currency = 'POINT' WHERE currency = 'CNY';
UPDATE wallet_accounts SET currency = 'POINT' WHERE currency = 'CNY';
UPDATE fulfillment_orders SET currency = 'POINT' WHERE currency = 'CNY';
+3 -3
View File
@@ -155,7 +155,7 @@ type MerchantProduct struct {
DisplayName string `gorm:"size:160" json:"display_name"` DisplayName string `gorm:"size:160" json:"display_name"`
PriceAmount int64 `gorm:"not null;default:0" json:"price_amount"` PriceAmount int64 `gorm:"not null;default:0" json:"price_amount"`
CostAmount int64 `gorm:"not null;default:0" json:"cost_amount"` CostAmount int64 `gorm:"not null;default:0" json:"cost_amount"`
Currency string `gorm:"size:12;not null;default:CNY" json:"currency"` Currency string `gorm:"size:12;not null;default:POINT" json:"currency"`
Stock int64 `gorm:"not null;default:-1" json:"stock"` Stock int64 `gorm:"not null;default:-1" json:"stock"`
Status string `gorm:"size:16;not null;default:active;index" json:"status"` Status string `gorm:"size:16;not null;default:active;index" json:"status"`
FulfillmentConfig string `gorm:"type:text" json:"fulfillment_config"` FulfillmentConfig string `gorm:"type:text" json:"fulfillment_config"`
@@ -170,7 +170,7 @@ type WalletAccount struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
MerchantID uint `gorm:"not null;uniqueIndex" json:"merchant_id"` MerchantID uint `gorm:"not null;uniqueIndex" json:"merchant_id"`
Currency string `gorm:"size:12;not null;default:CNY" json:"currency"` Currency string `gorm:"size:12;not null;default:POINT" json:"currency"`
AvailableBalance int64 `gorm:"not null;default:0" json:"available_balance"` AvailableBalance int64 `gorm:"not null;default:0" json:"available_balance"`
FrozenBalance int64 `gorm:"not null;default:0" json:"frozen_balance"` FrozenBalance int64 `gorm:"not null;default:0" json:"frozen_balance"`
} }
@@ -211,7 +211,7 @@ type FulfillmentOrder struct {
FeeFixedAmount int64 `gorm:"not null;default:0" json:"fee_fixed_amount"` FeeFixedAmount int64 `gorm:"not null;default:0" json:"fee_fixed_amount"`
ServiceFeeAmount int64 `gorm:"not null;default:0" json:"service_fee_amount"` ServiceFeeAmount int64 `gorm:"not null;default:0" json:"service_fee_amount"`
Amount int64 `gorm:"not null" json:"amount"` Amount int64 `gorm:"not null" json:"amount"`
Currency string `gorm:"size:12;not null;default:CNY" json:"currency"` Currency string `gorm:"size:12;not null;default:POINT" json:"currency"`
PaymentStatus string `gorm:"size:16;not null;default:pending;index" json:"payment_status"` PaymentStatus string `gorm:"size:16;not null;default:pending;index" json:"payment_status"`
FulfillmentStatus string `gorm:"size:16;not null;default:pending;index" json:"fulfillment_status"` FulfillmentStatus string `gorm:"size:16;not null;default:pending;index" json:"fulfillment_status"`
BuyerReference string `gorm:"size:128" json:"buyer_reference"` BuyerReference string `gorm:"size:128" json:"buyer_reference"`
+10 -10
View File
@@ -581,12 +581,12 @@ func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} {
// DashboardStats 仪表盘聚合指标。 // DashboardStats 仪表盘聚合指标。
type DashboardStats struct { type DashboardStats struct {
ProductCount int64 `json:"product_count"` ProductCount int64 `json:"product_count"`
MerchantCount int64 `json:"merchant_count"` MerchantCount int64 `json:"merchant_count"`
OrderCount int64 `json:"order_count"` OrderCount int64 `json:"order_count"`
TotalSales float64 `json:"total_sales"` TotalSales int64 `json:"total_sales"`
TotalFees float64 `json:"total_fees"` TotalFees int64 `json:"total_fees"`
PendingOrderCount int64 `json:"pending_order_count"` PendingOrderCount int64 `json:"pending_order_count"`
} }
// Dashboard 汇总商户维度的商品、商户、订单与金额统计。 // Dashboard 汇总商户维度的商品、商户、订单与金额统计。
@@ -605,11 +605,11 @@ func (s *FulfillmentService) Dashboard(merchantID uint, isPlatformAdmin bool) (*
s.db.Model(&model.FulfillmentOrder{}). s.db.Model(&model.FulfillmentOrder{}).
Where("merchant_id = ?", merchantID). Where("merchant_id = ?", merchantID).
Where("payment_status = ?", model.PaymentStatusPaid). Where("payment_status = ?", model.PaymentStatusPaid).
Select("COALESCE(SUM(amount),0) / 100.0").Scan(&stats.TotalSales) Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales)
s.db.Model(&model.FulfillmentOrder{}). s.db.Model(&model.FulfillmentOrder{}).
Where("merchant_id = ?", merchantID). Where("merchant_id = ?", merchantID).
Where("payment_status = ?", model.PaymentStatusPaid). Where("payment_status = ?", model.PaymentStatusPaid).
Select("COALESCE(SUM(service_fee_amount),0) / 100.0").Scan(&stats.TotalFees) Select("COALESCE(SUM(service_fee_amount),0)").Scan(&stats.TotalFees)
return stats, nil return stats, nil
} }
@@ -623,7 +623,7 @@ type OpenOrderQuery struct {
CannotShipReason string `json:"cannot_ship_reason,omitempty"` CannotShipReason string `json:"cannot_ship_reason,omitempty"`
Product *OpenOrderProduct `json:"product,omitempty"` Product *OpenOrderProduct `json:"product,omitempty"`
BuyerName string `json:"buyer_name"` BuyerName string `json:"buyer_name"`
Amount float64 `json:"amount"` Amount int64 `json:"amount"`
ProviderOrderNo string `json:"provider_order_no,omitempty"` ProviderOrderNo string `json:"provider_order_no,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
ShippedAt *time.Time `json:"shipped_at"` ShippedAt *time.Time `json:"shipped_at"`
@@ -691,7 +691,7 @@ func (s *FulfillmentService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, er
CanShip: canShip, CanShip: canShip,
CannotShipReason: reason, CannotShipReason: reason,
BuyerName: order.BuyerReference, BuyerName: order.BuyerReference,
Amount: float64(order.Amount) / 100.0, Amount: order.Amount,
ProviderOrderNo: order.ProviderOrderNo, ProviderOrderNo: order.ProviderOrderNo,
CreatedAt: order.CreatedAt, CreatedAt: order.CreatedAt,
ShippedAt: order.DeliveredAt, ShippedAt: order.DeliveredAt,
+2 -2
View File
@@ -32,7 +32,7 @@ func seedFulfillmentMerchant(t *testing.T, db *gorm.DB, code string, balance, st
} }
if err := db.Create(&model.WalletAccount{ if err := db.Create(&model.WalletAccount{
MerchantID: merchant.ID, MerchantID: merchant.ID,
Currency: "CNY", Currency: "POINT",
AvailableBalance: balance, AvailableBalance: balance,
}).Error; err != nil { }).Error; err != nil {
t.Fatalf("create wallet: %v", err) t.Fatalf("create wallet: %v", err)
@@ -47,7 +47,7 @@ func seedFulfillmentMerchant(t *testing.T, db *gorm.DB, code string, balance, st
SKU: "sku-basic", SKU: "sku-basic",
DisplayName: "测试商品", DisplayName: "测试商品",
PriceAmount: price, PriceAmount: price,
Currency: "CNY", Currency: "POINT",
Stock: stock, Stock: stock,
Status: model.ProductStatusActive, Status: model.ProductStatusActive,
} }
+2 -2
View File
@@ -91,7 +91,7 @@ func (s *MerchantService) CreateMerchant(in CreateMerchantInput, actorUserID uin
if err := tx.Create(merchant).Error; err != nil { if err := tx.Create(merchant).Error; err != nil {
return err return err
} }
if err := tx.Create(&model.WalletAccount{MerchantID: merchant.ID, Currency: "CNY"}).Error; err != nil { if err := tx.Create(&model.WalletAccount{MerchantID: merchant.ID, Currency: "POINT"}).Error; err != nil {
return err return err
} }
if err := tx.Create(&model.MerchantMember{ if err := tx.Create(&model.MerchantMember{
@@ -332,7 +332,7 @@ func (s *MerchantService) CreateMerchantProduct(merchantID uint, in CreateMercha
return nil, errors.New("库存只能为 -1 或非负整数") return nil, errors.New("库存只能为 -1 或非负整数")
} }
if in.Currency == "" { if in.Currency == "" {
in.Currency = "CNY" in.Currency = "POINT"
} }
in.Currency = strings.ToUpper(in.Currency) in.Currency = strings.ToUpper(in.Currency)
if in.Status == "" { if in.Status == "" {
+3 -5
View File
@@ -54,11 +54,10 @@ export default function Dashboard() {
<Col xs={24} sm={12} lg={8}> <Col xs={24} sm={12} lg={8}>
<Card> <Card>
<Statistic <Statistic
title="成交金额" title="成交积分"
value={stats?.total_sales ?? 0} value={stats?.total_sales ?? 0}
precision={2}
prefix={<DollarOutlined />} prefix={<DollarOutlined />}
suffix="" suffix="积分"
/> />
</Card> </Card>
</Col> </Col>
@@ -67,9 +66,8 @@ export default function Dashboard() {
<Statistic <Statistic
title="平台手续费" title="平台手续费"
value={stats?.total_fees ?? 0} value={stats?.total_fees ?? 0}
precision={2}
prefix={<PercentageOutlined />} prefix={<PercentageOutlined />}
suffix="" suffix="积分"
/> />
</Card> </Card>
</Col> </Col>
+13 -27
View File
@@ -212,11 +212,11 @@ export default function MerchantCenter() {
setEditingProduct(null) setEditingProduct(null)
productForm.resetFields() productForm.resetFields()
productForm.setFieldsValue({ productForm.setFieldsValue({
currency: 'CNY', currency: 'POINT',
stock: -1, stock: -1,
status: 'active', status: 'active',
price_yuan: 0, price_amount: 0,
cost_yuan: 0, cost_amount: 0,
}) })
setProductOpen(true) setProductOpen(true)
} }
@@ -225,8 +225,6 @@ export default function MerchantCenter() {
setEditingProduct(record) setEditingProduct(record)
productForm.setFieldsValue({ productForm.setFieldsValue({
...record, ...record,
price_yuan: centsToYuan(record.price_amount),
cost_yuan: centsToYuan(record.cost_amount),
}) })
setProductOpen(true) setProductOpen(true)
} }
@@ -236,11 +234,7 @@ export default function MerchantCenter() {
try { try {
const payload = { const payload = {
...values, ...values,
price_amount: yuanToCents(values.price_yuan),
cost_amount: yuanToCents(values.cost_yuan),
} }
delete payload.price_yuan
delete payload.cost_yuan
if (editingProduct) { if (editingProduct) {
await merchantApi.updateProduct(editingProduct.id, payload) await merchantApi.updateProduct(editingProduct.id, payload)
message.success('商品已更新') message.success('商品已更新')
@@ -259,7 +253,7 @@ export default function MerchantCenter() {
const values = await walletForm.validateFields() const values = await walletForm.validateFields()
try { try {
const walletData = await merchantApi.adjustWallet({ const walletData = await merchantApi.adjustWallet({
amount: yuanToCents(values.amount_yuan), amount: values.amount,
idempotency_key: values.idempotency_key, idempotency_key: values.idempotency_key,
note: values.note, note: values.note,
}) })
@@ -497,7 +491,7 @@ export default function MerchantCenter() {
<Descriptions size="small" bordered column={3} style={{ flex: 1 }}> <Descriptions size="small" bordered column={3} style={{ flex: 1 }}>
<Descriptions.Item label="可用余额">{money(wallet?.available_balance ?? 0)}</Descriptions.Item> <Descriptions.Item label="可用余额">{money(wallet?.available_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="冻结余额">{money(wallet?.frozen_balance ?? 0)}</Descriptions.Item> <Descriptions.Item label="冻结余额">{money(wallet?.frozen_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="币种">{wallet?.currency || 'CNY'}</Descriptions.Item> <Descriptions.Item label="币种">{wallet?.currency || 'POINT'}</Descriptions.Item>
</Descriptions> </Descriptions>
{canFinance && ( {canFinance && (
<Button <Button
@@ -604,14 +598,14 @@ export default function MerchantCenter() {
</Form.Item> </Form.Item>
</Space> </Space>
<Space size="middle" style={{ width: '100%' }}> <Space size="middle" style={{ width: '100%' }}>
<Form.Item name="price_yuan" label="售价" rules={[{ required: true }]} style={{ width: 180 }}> <Form.Item name="price_amount" label="售价(积分)" rules={[{ required: true }]} style={{ width: 180 }}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} /> <InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
<Form.Item name="cost_yuan" label="成本" style={{ width: 180 }}> <Form.Item name="cost_amount" label="成本(积分)" style={{ width: 180 }}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} /> <InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
<Form.Item name="currency" label="币种" style={{ width: 180 }}> <Form.Item name="currency" label="币种" style={{ width: 180 }}>
<Select options={[{ value: 'CNY', label: 'CNY' }]} /> <Select options={[{ value: 'POINT', label: 'POINT(积分)' }]} />
</Form.Item> </Form.Item>
</Space> </Space>
<Space size="middle" style={{ width: '100%' }}> <Space size="middle" style={{ width: '100%' }}>
@@ -635,8 +629,8 @@ export default function MerchantCenter() {
<Modal title="钱包调整" open={walletOpen} onOk={submitWalletAdjust} onCancel={() => setWalletOpen(false)} destroyOnClose> <Modal title="钱包调整" open={walletOpen} onOk={submitWalletAdjust} onCancel={() => setWalletOpen(false)} destroyOnClose>
<Form form={walletForm} layout="vertical" style={{ marginTop: 16 }}> <Form form={walletForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="amount_yuan" label="调整金额" rules={[{ required: true }]}> <Form.Item name="amount" label="调整积分" rules={[{ required: true }]}>
<InputNumber precision={2} style={{ width: '100%' }} placeholder="正数充值,负数扣减" /> <InputNumber precision={0} style={{ width: '100%' }} placeholder="正数充值,负数扣减" />
</Form.Item> </Form.Item>
<Form.Item name="idempotency_key" label="幂等键" rules={[{ required: true }]}> <Form.Item name="idempotency_key" label="幂等键" rules={[{ required: true }]}>
<Input /> <Input />
@@ -734,14 +728,6 @@ function SecretBlock({ appKey, secret }: { appKey?: string; secret: string }) {
) )
} }
function yuanToCents(value?: number | null) {
return Math.round(Number(value || 0) * 100)
}
function centsToYuan(value?: number | null) {
return Number(((value || 0) / 100).toFixed(2))
}
function featuresToList(features?: string) { function featuresToList(features?: string) {
const list = (features || '') const list = (features || '')
.split(/[,\s]+/) .split(/[,\s]+/)
@@ -768,7 +754,7 @@ function resolveEnabledTab(current: string, features?: string) {
} }
function money(value?: number | null) { function money(value?: number | null) {
return `¥${centsToYuan(value).toFixed(2)}` return `${Number(value || 0)} 积分`
} }
function moneyWithSign(value?: number | null) { function moneyWithSign(value?: number | null) {
+11 -19
View File
@@ -70,7 +70,7 @@ export default function PlatformMerchants() {
features: featureListToText(values.features), features: featureListToText(values.features),
fee_type: values.fee_type, fee_type: values.fee_type,
fee_rate_bp: Number(values.fee_rate_bp || 0), fee_rate_bp: Number(values.fee_rate_bp || 0),
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan), fee_fixed_amount: Number(values.fee_fixed_amount || 0),
}) })
message.success('商户已创建') message.success('商户已创建')
setCreateOpen(false) setCreateOpen(false)
@@ -93,7 +93,7 @@ export default function PlatformMerchants() {
features: featureListToText(values.features), features: featureListToText(values.features),
fee_type: values.fee_type, fee_type: values.fee_type,
fee_rate_bp: Number(values.fee_rate_bp || 0), fee_rate_bp: Number(values.fee_rate_bp || 0),
fee_fixed_amount: yuanToCents(values.fee_fixed_yuan), fee_fixed_amount: Number(values.fee_fixed_amount || 0),
}) })
message.success('商户设置已更新') message.success('商户设置已更新')
setSettingsOpen(false) setSettingsOpen(false)
@@ -131,7 +131,7 @@ export default function PlatformMerchants() {
width: 160, width: 160,
render: (_, r) => render: (_, r) =>
r.fee_type === 'fixed' r.fee_type === 'fixed'
? `固定 ¥${centsToYuan(r.fee_fixed_amount).toFixed(2)}/单` ? `固定 ${r.fee_fixed_amount} 积分/单`
: `${(r.fee_rate_bp / 100).toFixed(2)}%`, : `${(r.fee_rate_bp / 100).toFixed(2)}%`,
}, },
{ {
@@ -174,7 +174,7 @@ export default function PlatformMerchants() {
...record, ...record,
features: featuresToList(record.features), features: featuresToList(record.features),
fee_type: record.fee_type || 'rate', fee_type: record.fee_type || 'rate',
fee_fixed_yuan: centsToYuan(record.fee_fixed_amount), fee_fixed_amount: record.fee_fixed_amount,
}) })
setSettingsOpen(true) setSettingsOpen(true)
}} }}
@@ -221,7 +221,7 @@ export default function PlatformMerchants() {
features: featureOptions.map((item) => item.value), features: featureOptions.map((item) => item.value),
fee_type: 'rate', fee_type: 'rate',
fee_rate_bp: 0, fee_rate_bp: 0,
fee_fixed_yuan: 0, fee_fixed_amount: 0,
}) })
setCreateOpen(true) setCreateOpen(true)
}} }}
@@ -281,15 +281,15 @@ export default function PlatformMerchants() {
<Select <Select
options={[ options={[
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' }, { value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
{ value: 'fixed', label: '按固定金额(每单固定手续费' }, { value: 'fixed', label: '按固定金额(每单固定积分' },
]} ]}
/> />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}> <Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
{({ getFieldValue }) => {({ getFieldValue }) =>
getFieldValue('fee_type') === 'fixed' ? ( getFieldValue('fee_type') === 'fixed' ? (
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(" rules={[{ required: true }]}> <Form.Item name="fee_fixed_amount" label="每单固定手续费(积分" rules={[{ required: true }]}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} /> <InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
) : ( ) : (
<Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=0.01%,如 250=2.5%" rules={[{ required: true }]}> <Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=0.01%,如 250=2.5%" rules={[{ required: true }]}>
@@ -324,15 +324,15 @@ export default function PlatformMerchants() {
<Select <Select
options={[ options={[
{ value: 'rate', label: '按百分比(每单按订单金额比例收取)' }, { value: 'rate', label: '按百分比(每单按订单金额比例收取)' },
{ value: 'fixed', label: '按固定金额(每单固定手续费' }, { value: 'fixed', label: '按固定金额(每单固定积分' },
]} ]}
/> />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}> <Form.Item noStyle shouldUpdate={(prev, cur) => prev.fee_type !== cur.fee_type}>
{({ getFieldValue }) => {({ getFieldValue }) =>
getFieldValue('fee_type') === 'fixed' ? ( getFieldValue('fee_type') === 'fixed' ? (
<Form.Item name="fee_fixed_yuan" label="每单固定手续费(" rules={[{ required: true }]}> <Form.Item name="fee_fixed_amount" label="每单固定手续费(积分" rules={[{ required: true }]}>
<InputNumber min={0} precision={2} style={{ width: '100%' }} /> <InputNumber min={0} precision={0} style={{ width: '100%' }} />
</Form.Item> </Form.Item>
) : ( ) : (
<Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=0.01%,如 250=2.5%" rules={[{ required: true }]}> <Form.Item name="fee_rate_bp" label="手续费比例 BP1BP=0.01%,如 250=2.5%" rules={[{ required: true }]}>
@@ -381,11 +381,3 @@ function featureListToText(features?: string[]) {
function featureText(feature: string) { function featureText(feature: string) {
return featureOptions.find((item) => item.value === feature)?.label || feature return featureOptions.find((item) => item.value === feature)?.label || feature
} }
function yuanToCents(value?: number | null) {
return Math.round(Number(value || 0) * 100)
}
function centsToYuan(value?: number | null) {
return Number(((value || 0) / 100).toFixed(2))
}