功能:完善登录安全与客服手动下单
This commit is contained in:
@@ -122,7 +122,7 @@ func main() {
|
|||||||
addr := ":" + cfg.Port
|
addr := ":" + cfg.Port
|
||||||
log.Printf("游戏皮肤供货平台 API 启动: http://localhost%s", addr)
|
log.Printf("游戏皮肤供货平台 API 启动: http://localhost%s", addr)
|
||||||
log.Printf("数据库: PostgreSQL")
|
log.Printf("数据库: PostgreSQL")
|
||||||
log.Printf("默认管理员: admin / admin123")
|
log.Printf("管理员账号已初始化;请通过账号菜单及时修改初始密码")
|
||||||
log.Printf("开放接口鉴权: X-App-Key + X-Timestamp + X-Nonce + X-Sign (HMAC-SHA256)")
|
log.Printf("开放接口鉴权: X-App-Key + X-Timestamp + X-Nonce + X-Sign (HMAC-SHA256)")
|
||||||
if cfg.OpenAPIDebug {
|
if cfg.OpenAPIDebug {
|
||||||
log.Printf("开放接口调试日志: 开启 (OPEN_API_DEBUG=0 可关闭)")
|
log.Printf("开放接口调试日志: 开启 (OPEN_API_DEBUG=0 可关闭)")
|
||||||
|
|||||||
@@ -43,3 +43,21 @@ func (h *AuthHandler) Profile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OK(c, user)
|
response.OK(c, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type changePasswordReq struct {
|
||||||
|
CurrentPassword string `json:"current_password" binding:"required"`
|
||||||
|
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
||||||
|
var req changePasswordReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "请填写当前密码和至少 8 位的新密码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.ChangePassword(middleware.GetUserID(c), req.CurrentPassword, req.NewPassword); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) {
|
|||||||
response.Page(c, list, total, page, size)
|
response.Page(c, list, total, page, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListManualOrderProducts returns active products for order creation without granting product-management access.
|
||||||
|
func (h *MerchantHandler) ListManualOrderProducts(c *gin.Context) {
|
||||||
|
list, _, err := h.merchantSvc.ListMerchantProducts(middleware.GetMerchantID(c), 1, 100, true)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
type merchantManualOrderReq struct {
|
type merchantManualOrderReq struct {
|
||||||
ClientOrderNo string `json:"client_order_no" binding:"required,max=96"`
|
ClientOrderNo string `json:"client_order_no" binding:"required,max=96"`
|
||||||
SKU string `json:"sku" binding:"required,max=96"`
|
SKU string `json:"sku" binding:"required,max=96"`
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func Setup(h *Handlers) *gin.Engine {
|
|||||||
auth.Use(middleware.Tenant(h.Tenant))
|
auth.Use(middleware.Tenant(h.Tenant))
|
||||||
{
|
{
|
||||||
auth.GET("/auth/profile", h.Auth.Profile)
|
auth.GET("/auth/profile", h.Auth.Profile)
|
||||||
|
auth.PUT("/auth/password", h.Auth.ChangePassword)
|
||||||
auth.GET("/dashboard", h.Dashboard.Dashboard)
|
auth.GET("/dashboard", h.Dashboard.Dashboard)
|
||||||
|
|
||||||
// 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。
|
// 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。
|
||||||
@@ -155,6 +156,7 @@ func Setup(h *Handlers) *gin.Engine {
|
|||||||
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts)
|
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts)
|
||||||
merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus)
|
merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus)
|
||||||
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders)
|
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders)
|
||||||
|
merchant.GET("/orders/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListManualOrderProducts)
|
||||||
merchant.POST("/orders/manual", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateManualOrder)
|
merchant.POST("/orders/manual", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateManualOrder)
|
||||||
merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateTestOrder)
|
merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateTestOrder)
|
||||||
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink)
|
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"affiliate_dash/internal/model"
|
"affiliate_dash/internal/model"
|
||||||
"affiliate_dash/internal/pkg/jwt"
|
"affiliate_dash/internal/pkg/jwt"
|
||||||
@@ -54,6 +55,37 @@ func (s *AuthService) GetProfile(userID uint) (*model.User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChangePassword only changes the current account after its existing password is verified.
|
||||||
|
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {
|
||||||
|
if userID == 0 {
|
||||||
|
return errors.New("无效的用户身份")
|
||||||
|
}
|
||||||
|
if len(newPassword) < 8 {
|
||||||
|
return errors.New("新密码至少 8 位")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(newPassword) == "" {
|
||||||
|
return errors.New("新密码不能为空")
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
if err := s.db.First(&user, userID).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("用户不存在")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
|
||||||
|
return errors.New("当前密码错误")
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(newPassword)); err == nil {
|
||||||
|
return errors.New("新密码不能与当前密码相同")
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.db.Model(&user).Update("password_hash", string(hash)).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AuthService) EnsureAdmin() error {
|
func (s *AuthService) EnsureAdmin() error {
|
||||||
var count int64
|
var count int64
|
||||||
s.db.Model(&model.User{}).Where("role = ?", model.RoleAdmin).Count(&count)
|
s.db.Model(&model.User{}).Where("role = ?", model.RoleAdmin).Count(&count)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"affiliate_dash/internal/model"
|
"affiliate_dash/internal/model"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateAPIClientEnforcesPerMerchantLimit(t *testing.T) {
|
func TestCreateAPIClientEnforcesPerMerchantLimit(t *testing.T) {
|
||||||
@@ -174,3 +176,29 @@ func TestMemberCanBeUpdatedOrRemovedWithoutDeletingAccount(t *testing.T) {
|
|||||||
t.Fatalf("member relationship should be removed, count=%d err=%v", count, err)
|
t.Fatalf("member relationship should be removed, count=%d err=%v", count, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthServiceChangePasswordVerifiesCurrentPassword(t *testing.T) {
|
||||||
|
db := newServiceTestDB(t)
|
||||||
|
tenant := NewTenantService(db)
|
||||||
|
auth := NewAuthService(db, nil, tenant)
|
||||||
|
user, err := NewUserService(db, tenant).Create("password-owner", "old-password", "密码管理员", model.RoleAdmin, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create user: %v", err)
|
||||||
|
}
|
||||||
|
if err := auth.ChangePassword(user.ID, "wrong-password", "new-password"); err == nil {
|
||||||
|
t.Fatal("expected current password validation error")
|
||||||
|
}
|
||||||
|
if err := auth.ChangePassword(user.ID, "old-password", "new-password"); err != nil {
|
||||||
|
t.Fatalf("change password: %v", err)
|
||||||
|
}
|
||||||
|
var saved model.User
|
||||||
|
if err := db.First(&saved, user.ID).Error; err != nil {
|
||||||
|
t.Fatalf("load updated user: %v", err)
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("new-password")); err != nil {
|
||||||
|
t.Fatalf("new password should match: %v", err)
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(saved.PasswordHash), []byte("old-password")); err == nil {
|
||||||
|
t.Fatal("old password should no longer match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const authApi = {
|
|||||||
const res = await request.get('/auth/profile')
|
const res = await request.get('/auth/profile')
|
||||||
return res.data.data as User
|
return res.data.data as User
|
||||||
},
|
},
|
||||||
|
changePassword: (data: { current_password: string; new_password: string }) =>
|
||||||
|
request.put('/auth/password', data).then((r) => r.data.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dashboardApi = {
|
export const dashboardApi = {
|
||||||
@@ -62,6 +64,8 @@ export const merchantApi = {
|
|||||||
request.patch(`/merchant/products/${id}/status`, { status }).then((r) => r.data.data),
|
request.patch(`/merchant/products/${id}/status`, { status }).then((r) => r.data.data),
|
||||||
orders: (params?: Record<string, unknown>) =>
|
orders: (params?: Record<string, unknown>) =>
|
||||||
request.get('/merchant/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
|
request.get('/merchant/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
|
||||||
|
manualOrderProducts: () =>
|
||||||
|
request.get('/merchant/orders/products').then((r) => r.data.data as MerchantProduct[]),
|
||||||
createTestOrder: (data: {
|
createTestOrder: (data: {
|
||||||
sku: string
|
sku: string
|
||||||
buyer_reference?: string
|
buyer_reference?: string
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
Avatar,
|
||||||
Tag,
|
Tag,
|
||||||
Button,
|
Button,
|
||||||
|
message,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +22,7 @@ import {
|
|||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
KeyOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
OrderedListOutlined,
|
OrderedListOutlined,
|
||||||
@@ -27,6 +32,7 @@ import {
|
|||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/auth'
|
||||||
|
import { authApi } from '../api'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout
|
const { Header, Sider, Content } = Layout
|
||||||
@@ -207,6 +213,9 @@ export default function MainLayout() {
|
|||||||
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true'
|
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true'
|
||||||
})
|
})
|
||||||
const { user, logout, isAdmin, merchant, merchantPermissions } = useAuth()
|
const { user, logout, isAdmin, merchant, merchantPermissions } = useAuth()
|
||||||
|
const [passwordOpen, setPasswordOpen] = useState(false)
|
||||||
|
const [passwordSaving, setPasswordSaving] = useState(false)
|
||||||
|
const [passwordForm] = Form.useForm()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const isDocsPage = location.pathname.startsWith('/open-api')
|
const isDocsPage = location.pathname.startsWith('/open-api')
|
||||||
@@ -265,6 +274,25 @@ export default function MainLayout() {
|
|||||||
navigate(child.path)
|
navigate(child.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitPasswordChange = async () => {
|
||||||
|
try {
|
||||||
|
const values = await passwordForm.validateFields()
|
||||||
|
setPasswordSaving(true)
|
||||||
|
await authApi.changePassword({
|
||||||
|
current_password: values.current_password,
|
||||||
|
new_password: values.new_password,
|
||||||
|
})
|
||||||
|
setPasswordOpen(false)
|
||||||
|
passwordForm.resetFields()
|
||||||
|
logout()
|
||||||
|
navigate('/login')
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '密码修改失败')
|
||||||
|
} finally {
|
||||||
|
setPasswordSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userMenu: MenuProps['items'] = [
|
const userMenu: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
key: 'user-info',
|
key: 'user-info',
|
||||||
@@ -277,6 +305,12 @@ export default function MainLayout() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'change-password',
|
||||||
|
icon: <KeyOutlined />,
|
||||||
|
label: '修改密码',
|
||||||
|
onClick: () => setPasswordOpen(true),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
icon: <LogoutOutlined style={{ color: '#dc2626' }} />,
|
icon: <LogoutOutlined style={{ color: '#dc2626' }} />,
|
||||||
@@ -412,6 +446,32 @@ export default function MainLayout() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Modal
|
||||||
|
title="修改密码"
|
||||||
|
open={passwordOpen}
|
||||||
|
confirmLoading={passwordSaving}
|
||||||
|
onOk={submitPasswordChange}
|
||||||
|
onCancel={() => {
|
||||||
|
setPasswordOpen(false)
|
||||||
|
passwordForm.resetFields()
|
||||||
|
}}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={passwordForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item name="current_password" label="当前密码" rules={[{ required: true, message: '请输入当前密码' }]}>
|
||||||
|
<Input.Password autoComplete="current-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="new_password" label="新密码" rules={[{ required: true, min: 8, message: '新密码至少 8 位' }]}>
|
||||||
|
<Input.Password autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="confirm_password" label="确认新密码" dependencies={['new_password']} rules={[
|
||||||
|
{ required: true, message: '请再次输入新密码' },
|
||||||
|
({ getFieldValue }) => ({ validator: (_, value) => !value || getFieldValue('new_password') === value ? Promise.resolve() : Promise.reject(new Error('两次输入的密码不一致')) }),
|
||||||
|
]}>
|
||||||
|
<Input.Password autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Form, Input, Button, Typography, message, Space, Tag } from 'antd'
|
import { Form, Input, Button, Typography, message, Space } from 'antd'
|
||||||
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined, RightOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '../store/auth'
|
import { useAuth } from '../store/auth'
|
||||||
|
|
||||||
@@ -23,11 +23,6 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fillCredentials = (user: string, pass: string) => {
|
|
||||||
form.setFieldsValue({ username: user, password: pass })
|
|
||||||
message.info(`已填入账号: ${user}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-wrapper">
|
<div className="login-wrapper">
|
||||||
<div className="login-card">
|
<div className="login-card">
|
||||||
@@ -46,7 +41,6 @@ export default function Login() {
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
initialValues={{ username: 'admin', password: 'admin123' }}
|
|
||||||
requiredMark={false}
|
requiredMark={false}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -97,33 +91,10 @@ export default function Login() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{/* 快捷测试账号填充 */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '12px 14px',
|
|
||||||
borderRadius: 8,
|
|
||||||
background: 'rgba(255, 255, 255, 0.04)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8, textAlign: 'center' }}>
|
|
||||||
快速体验(演示账号列表):
|
|
||||||
</div>
|
|
||||||
<Space style={{ width: '100%', justifyContent: 'center' }} wrap>
|
|
||||||
<Tag
|
|
||||||
color="blue"
|
|
||||||
style={{ cursor: 'pointer', padding: '4px 10px', borderRadius: 4 }}
|
|
||||||
onClick={() => fillCredentials('admin', 'admin123')}
|
|
||||||
>
|
|
||||||
管理员 (admin)
|
|
||||||
</Tag>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography.Paragraph
|
<Typography.Paragraph
|
||||||
style={{ fontSize: 12, margin: 0, textAlign: 'center', color: '#64748b' }}
|
style={{ fontSize: 12, margin: 0, textAlign: 'center', color: '#64748b' }}
|
||||||
>
|
>
|
||||||
商户账号与成员密钥请联系平台管理员统一配置与创建
|
请使用平台管理员或商户负责人分配的账号登录
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -294,25 +294,18 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
|||||||
})
|
})
|
||||||
}, [loadWallet, merchantPermissions, walletFilterForm])
|
}, [loadWallet, merchantPermissions, walletFilterForm])
|
||||||
|
|
||||||
const newManualClientOrderNo = () => {
|
|
||||||
const suffix = window.crypto?.randomUUID?.().replaceAll('-', '') || `${Date.now()}${Math.random().toString(36).slice(2)}`
|
|
||||||
return `manual-${suffix.slice(0, 32)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const openManualOrderCreate = () => {
|
const openManualOrderCreate = () => {
|
||||||
manualOrderForm.resetFields()
|
manualOrderForm.resetFields()
|
||||||
manualOrderForm.setFieldsValue({
|
manualOrderForm.setFieldsValue({
|
||||||
client_order_no: newManualClientOrderNo(),
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
})
|
})
|
||||||
setManualOrderOpen(true)
|
setManualOrderOpen(true)
|
||||||
setManualOrderProductsLoading(true)
|
setManualOrderProductsLoading(true)
|
||||||
merchantApi.products({ page: 1, size: 100 })
|
merchantApi.manualOrderProducts()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const activeProducts = (data.list || []).filter((item) => item.status === 'active')
|
setManualOrderProducts(data || [])
|
||||||
setManualOrderProducts(activeProducts)
|
if (data.length > 0) {
|
||||||
if (activeProducts.length > 0) {
|
manualOrderForm.setFieldsValue({ sku: data[0].sku })
|
||||||
manualOrderForm.setFieldsValue({ sku: activeProducts[0].sku })
|
|
||||||
} else {
|
} else {
|
||||||
message.warning('没有可用商品')
|
message.warning('没有可用商品')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user