增加微信支付,商城逻辑,公告

This commit is contained in:
2025-09-05 16:48:53 +08:00
parent e5ace37c68
commit 8530e97ab6
38 changed files with 6430 additions and 1688 deletions

20
.env
View File

@@ -43,4 +43,22 @@ MINIO_PUBLIC_URL=https://minio.zrbjr.com
MINIO_BUCKET_UPLOADS=jurongquan MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan MINIO_BUCKET_DOCUMENTS=jurongquan
#支付配置
WECHAT_APP_ID=wx3a702dbe13fd2217
WECHAT_MCH_ID=1726377336
WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_CERT_PATH=./cert/apiclient_cert.pem
WECHAT_KEY_PATH=./cert/apiclient_key.pem
WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify
# 支付宝配置
# 请在支付宝开放平台获取以下配置信息:
# 1. 应用ID在支付宝开放平台创建应用后获得
# 2. 应用私钥和支付宝公钥现在从文件读取
ALIPAY_APP_ID=2021005188682022
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel

View File

@@ -41,3 +41,20 @@ MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan MINIO_BUCKET_DOCUMENTS=jurongquan
# 微信支付配置
WECHAT_APP_ID=your_wechat_app_id
WECHAT_MCH_ID=your_wechat_mch_id
WECHAT_API_KEY=your_wechat_api_key
WECHAT_API_V3_KEY=your_wechat_api_v3_key
WECHAT_NOTIFY_URL=https://your-domain.com/api/wechat-pay/notify
WECHAT_CERT_PATH=./cert/apiclient_cert.pem
WECHAT_KEY_PATH=./cert/apiclient_key.pem
# 支付宝支付配置
ALIPAY_APP_ID=your_alipay_app_id
ALIPAY_PRIVATE_KEY=your_alipay_private_key
ALIPAY_PUBLIC_KEY=your_alipay_public_key
ALIPAY_GATEWAY_URL=https://openapi.alipay.com/gateway.do
ALIPAY_NOTIFY_URL=https://your-domain.com/api/alipay/notify
ALIPAY_RETURN_URL=https://your-domain.com/payment/success

File diff suppressed because it is too large Load Diff

BIN
cert/apiclient_cert.p12 Normal file

Binary file not shown.

25
cert/apiclient_cert.pem Normal file
View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKDCCAxCgAwIBAgIUXysc+VIXyKce5hqbA2vg9d9uH7kwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwOTAyMDg0NDA0WhcNMzAwOTAxMDg0NDA0WjCBgTETMBEGA1UEAwwK
MTcyNjM3NzMzNjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL
DCTlroHms6Lngqzono3mrYbliJvnp5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT
AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKncf/y0dT12nyCmnK5w3ECQgtoHrU32LqQH/N5SUbiJVbkys99Qq41D
MO0539Ki2MBuOuDHnWOrgNbRhH4WMXS8qodTcWu39cgVlnmw/KtQJdyInrPynFYH
lWO6boc1jfM4UEbEk90iaLM49uFo75+bCNGZVHOt6UMmTNFB9zHDfnhm+UaxNJOf
p1z7sntmD3H98c4ghzcE5L51KL6A7JtW+cp0zTpOunj0T/drvmRoyQVzqZ4IlXga
9pJ/6Un2fVNn8pZL804ldKumAf5KVKmqEmz64ydh3896MoAshT/UJIHDuduN+Jbk
gROt9QiM68jR6CuqeRsV7EZ1OA/BLI8CAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG
A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu
aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC
MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB
OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQCzyJZ/
+rnvUd1kJ74KCb6kxAwM/bX8w5lhkdUkeyQxdmbUCXCrkOGJ8uRQMfiK93eeET4h
KMrZywQHvL1E5WXQpUmQZVYj6eAxaUO+RoW8wPeWb5x/LbSXqQCrCNF6U+AvC6wj
6haW8TK8egCLsjxPBXL9NjkxvcsIOIM8F6JKhMSAAjT7F1nkXthyxC50o/Mbox6l
YfvVZ70gWuUR2e6o9Sob1tTq6YwSwDr5OMwJT8QpDEdjAbWLLtv7a+ApzfqWeTHP
dmVawsaQnOzFaUPDtEGYSh23/eaTrJ9DHpgtubtU/CQsGos6QHD6huRYMmvw3Wml
mFAVWux4DJ8lZXGZ
-----END CERTIFICATE-----

28
cert/apiclient_key.pem Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCp3H/8tHU9dp8g
ppyucNxAkILaB61N9i6kB/zeUlG4iVW5MrPfUKuNQzDtOd/SotjAbjrgx51jq4DW
0YR+FjF0vKqHU3Frt/XIFZZ5sPyrUCXciJ6z8pxWB5Vjum6HNY3zOFBGxJPdImiz
OPbhaO+fmwjRmVRzrelDJkzRQfcxw354ZvlGsTSTn6dc+7J7Zg9x/fHOIIc3BOS+
dSi+gOybVvnKdM06Trp49E/3a75kaMkFc6meCJV4GvaSf+lJ9n1TZ/KWS/NOJXSr
pgH+SlSpqhJs+uMnYd/PejKALIU/1CSBw7nbjfiW5IETrfUIjOvI0egrqnkbFexG
dTgPwSyPAgMBAAECggEAF5dGN0Sg285zv0ckj52hGV54retPCHrec22gkwf/zY9V
VolSLfu4N8BTNT9KdKilTeSBTOKsW0FgfXVP32sZp0rkrDLMl9dOzWEiKviHvws8
lupqkDdruw8Gknk8DI9FjbgOfiWjG51ByVJqB1hZn2Ma0HFpJz/KG8df99gfisuP
3s4s/N8gChKRobNJzoFTVlEexVwM8vPZl7foxm9SEc0tbXyU2yB+pglVv/BkkLo5
aZFNIC2TVnGX1WJlTTfuhng+MMeckSIPjJGH9c7RpgAChKGOBFOAK87/1+3tep47
qp3i2WnceIxy2Y/wIT4cGWUAd0Ghi/kkIo8KqpagEQKBgQDfiwhBitymWc5QRr+R
LM6+Yu/EWlpd4/ZrcIgX9S5Z/La4gs/fqyWyKp9r/KjyYKG5sEfArim3NenvQBac
Vp99kNpjAl6SCSWFNeA7aY+zLDJBRHSaG4ovxWXk1ZYD2hBLKahzp7PCdcNvPICU
60AapDMiGlfbcJDot1uP+9i16wKBgQDChiW9qroJGfZcpe5nkh38orPOp7or09mG
perNOAXGghJMV7tc1Se+ttjcvlfK6hrQcxPCMccH/x0bmeZ/MaG1RZjZS2zAXqH5
9huZtgIHc2idVS+j1t4HsszuIBp84c6ykCU+6NltW1LQFRZ9Y2HuA5HW/fgyhLMG
RHpRdgfG7QKBgDd9P5NlcNgqOrhal3rl8Hv5+yJ2ezALQkPxLxcjWVolDQZIEmmn
BjhvtBsOILHporuBMo51rQ05aNRmyDYOmpCEwHELSYZelt22Pe8BiRYkxmTFJVyL
sYWiLmTbT92s55aAxLvQySJgMR8Pmatdqg/y6m5ws5ZZHt9lhGj9TxH5AoGAR8Rc
WjyJxF/av9XMPlPvUkzoz76b9h2D7KR8G1im8NT+UUIw8xAFSNyG5/Ily8xRNkSu
rn/U8YNSxuMh4h16jrltqgWkythfJCyDhFNdLkiK+Tj7iZP1eJuj9drMSvS4YLLD
uxEHXsxJolGVaY9oCvswLESo9GJ29kH/atyEBAUCgYB+xm2NI9OpF+dSq5O8vMiW
NtlD744WfsiBDzatQz1A9PAB6cyLi03zjW10+HCNZRxBSfI8au/sn9lOdcCjvSGk
8tjZXMzL71jDhEYs4qTUaiKJ2YpbLNcLFeTy48hWeRpew7t7bunksx8zb+0W14M2
zDmIHi+CSve59dOzoiYuNg==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,18 @@
欢迎使用微信支付!
附件中的三份文件证书pkcs12格式、证书pem格式、证书密钥pem格式,为接口中强制要求时需携带的证书文件。
证书属于敏感信息,请妥善保管不要泄露和被他人复制。
不同开发语言下的证书格式不同,以下为说明指引:
证书pkcs12格式apiclient_cert.p12
包含了私钥信息的证书文件为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份
部分安全性要求较高的API需要使用该证书来确认您的调用身份
windows上可以直接双击导入系统导入过程中会提示输入证书密码证书密码默认为您的商户号1900006031
证书pem格式apiclient_cert.pem
从apiclient_cert.p12中导出证书部分的文件为pem格式请妥善保管不要泄漏和被他人复制
部分开发语言和环境不能直接使用p12文件而需要使用pem所以为了方便您使用已为您直接提供
您也可以使用openssl命令来自己导出openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem
证书密钥pem格式apiclient_key.pem
从apiclient_cert.p12中导出密钥部分的文件为pem格式
部分开发语言和环境不能直接使用p12文件而需要使用pem所以为了方便您使用已为您直接提供
您也可以使用openssl命令来自己导出openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem
备注说明:
由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件rootca.pem下载

34
certs/README.md Normal file
View File

@@ -0,0 +1,34 @@
# 支付宝支付证书配置指南
## 证书文件说明
本目录需要包含以下证书文件用于支付宝支付V3版本API的证书模式
1. `appCertPublicKey.crt` - 应用公钥证书(已存在)
2. `alipayRootCert.crt` - 支付宝根证书(需下载)
3. `alipayCertPublicKey_RSA2.crt` - 支付宝公钥证书(需下载)
## 如何获取证书
### 已有证书
- `appCertPublicKey.crt` - 应用公钥证书已存在于本目录
### 需要下载的证书
请登录支付宝开放平台,从您的应用详情页面下载以下证书:
1. **支付宝根证书**:下载后重命名为 `alipayRootCert.crt` 并放入本目录
2. **支付宝公钥证书**:下载后重命名为 `alipayCertPublicKey_RSA2.crt` 并放入本目录
## 证书下载步骤
1. 登录[支付宝开放平台](https://open.alipay.com/)
2. 进入「开发者中心」->「我的应用」
3. 选择对应的应用
4. 点击「开发设置」
5. 在「接口加签方式」区域,选择「公钥证书」
6. 下载「支付宝根证书」和「支付宝公钥证书」
7. 将下载的证书放入本目录,并按上述名称重命名
## 配置验证
证书配置完成后系统将使用证书模式进行支付宝API调用支持V3版本的接口。

View File

@@ -0,0 +1 @@
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCV9dEIPimkwxhJXXy2cntNZqeZdV4JAdx7ZpnvbjV6OjeEZczksKFa6lgsYR6Wq6G/khEdVRyNW4RsepBRwVGDBxxUN0nB9uTtPti220PtXZIJycLY585nL/iaUjUt+Cyh2qsyIGQ0lGx9VFWU+x5stGCLgfZbHOaMo0S+49upEsX5p4MyRbc4zA/vf+xgmGm5f1YB1Nwm32JBhVRourmHhi82MLX3Hxwo6R/SIOrDIcUrELYC9XRC2LUWYJIsQ1QMtrE/0XVFmIB68gU9JqRTNXhLCyL19K3WUGvRXG9N5mTaZSBIYco7ZrtbTFHtIxSOYxqYTOtkPKVYxGn1I2WHAgMBAAECggEAEeaA6Dn7YJaFPKSzMVgjDc82LGRNMEgPmI9byq/eJFP0spIwThAjgqW8lreVHikoqqR19IlnWhxVh1luBsRLxZdAs3DSFhwxoXxCBDnSNvBXcWGrJ5csFTcttsYfYPhh44QlsVsaewhIlwFNIfaD4Df72ktOK/wcLSeEGkE6xEixfMcLawg6a8rkHbvJtppFdTSL2YBB4X+dnt3cYCxFYch7Q5+RF+Wqr++860B5ztBRSX8xmqEIDS/95tf09qDyhyrnE2a2wh/jT+BWzvILKNc8goNo29kACo8bE9p+rWCnG8YsL+4emlrxk6/OYzR5lG6xZNvXAmnY0LDKzQsqeQKBgQDTm/uQP82o0ez9YHP/sAvTVq7pIJlEnaZqI2jBvyGxf6fDTU5b0yfJF9VO5EoPVT9CHRJ7A0T4RUDhIAzo9fjrz3VjkXO/RZyOIHsptjvOFeKCiFGrHO8sKxC5i4IZIRwIB7A84DU7rVzQ5Kh3XRqi8srKl/DBXo1BloWU0HJetQKBgQC1axqVH/eIwZrYjSbXgSHwGYMhH0YmOIJEYavD6TmOp9QL5N1ax6B0r/BIVt7XAIRck8kueKY4LeRulq3azXIhBy5Qx6sp2Sb/SX3vrPrvA/ImoDQKR4CeL5zXQJkqbVgbbS//UTEWilKyQXNz13D4qbCqNBbLJaDirXqBT0ycywKBgCMvT2/XvAlzBlXHAOKl0gGM6z5mJjXrhK0nQBbfAeoykKF/rCTGgloEdXpNqSbNhNwoW1dK3t/tG/GS07K0m3QSJbGtkLJgD7zuF6yC2YTVzLjpk7LA99+/NWO0l6g4AiIvrRUiLpfCpqkxK/XU7EXl2uQ+yVBNuW0LayCoXCv1AoGAU/CJbSRMUO9baQTuStoJzODRBltFBtwwkdkrM0tPAU1v1E0BikZBXJwnLiFbm9k2ZOtQM3tJVUcOoYiASnOyccuzx1aLQKKj44yqg2Hi/QIzYWHQkk0BGq/m/sV52OKc2JvNkHGNp+M6XhXgiGHPeI5zGl1dioMPjLI9s2Twir8CgYEAn5wAbpx2imCPDtKgMxb0zxbKB5kq37QGuIRRAoorPkFr0Z7xSZs1M0qkhZcaX/kfnNgigPVwpUeUDQmETBRaXrCuXxRkOXGo1st4v5gn9kgQYBCv0dVbV1QNnnXp2fAtQ/3Jw6l9w4T0PgPBhu9TPnSTVZA0YldNo7TEEL5kEbA=

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7
TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR
gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse
bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5
h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak
UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl
hwIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFRgwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPsebLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSakUzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNlhwIDAQAB
-----END PUBLIC KEY-----

View File

@@ -574,7 +574,48 @@ async function createTables() {
FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL
) )
`); `);
}
// 通知公告表
await getDB().execute(`
CREATE TABLE IF NOT EXISTS announcements (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL COMMENT '公告标题',
content TEXT NOT NULL COMMENT '公告内容',
type ENUM('system', 'maintenance', 'promotion', 'warning') DEFAULT 'system' COMMENT '公告类型',
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium' COMMENT '优先级',
status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态',
is_pinned BOOLEAN DEFAULT FALSE COMMENT '是否置顶',
publish_time TIMESTAMP NULL COMMENT '发布时间',
expire_time TIMESTAMP NULL COMMENT '过期时间',
created_by INT NOT NULL COMMENT '创建者ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_status (status),
INDEX idx_type (type),
INDEX idx_publish_time (publish_time),
INDEX idx_created_at (created_at)
) COMMENT='通知公告表'
`);
// 用户公告阅读状态表
await getDB().execute(`
CREATE TABLE IF NOT EXISTS user_announcement_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL COMMENT '用户ID',
announcement_id INT NOT NULL COMMENT '公告ID',
is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读',
read_at TIMESTAMP NULL COMMENT '阅读时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY unique_user_announcement (user_id, announcement_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (announcement_id) REFERENCES announcements(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_announcement_id (announcement_id),
INDEX idx_is_read (is_read)
) COMMENT='用户公告阅读状态表'
`);}
/** /**
* 添加缺失的字段(处理数据库升级) * 添加缺失的字段(处理数据库升级)
@@ -862,13 +903,13 @@ async function createDefaultData() {
} }
// 初始化浙江省区域数据 // 初始化浙江省区域数据
await initializeZhejiangRegions(); // await initializeZhejiangRegions();
// 初始化默认地址标签 // 初始化默认地址标签
await initializeDefaultAddressLabels(); // await initializeDefaultAddressLabels();
// 初始化全国省市区数据 // 初始化全国省市区数据
await initializeChinaRegions(); // await initializeChinaRegions();
} }
/** /**

24
config/wechatPay.js Normal file
View File

@@ -0,0 +1,24 @@
// 微信支付配置
module.exports = {
// 微信支付配置
wechatPay: {
appId: process.env.WECHAT_APP_ID || '', // 微信公众号AppID
mchId: process.env.WECHAT_MCH_ID || '', // 商户号
apiKey: process.env.WECHAT_API_KEY || '', // API密钥
apiV3Key: process.env.WECHAT_API_V3_KEY || '', // APIv3密钥
notifyUrl: process.env.WECHAT_NOTIFY_URL || 'https://your-domain.com/api/wechat/notify', // 支付回调地址
// 证书路径(生产环境需要配置)
certPath: process.env.WECHAT_CERT_PATH || '',
keyPath: process.env.WECHAT_KEY_PATH || '',
// 支付相关配置
tradeType: {
h5: 'MWEB', // H5支付
jsapi: 'JSAPI' // 公众号支付
},
// 注册费用配置(单位:分)
registrationFee: 100 // 1元注册费
}
};

View File

@@ -11,10 +11,12 @@ docs/
│ ├── product.js # 商品相关数据模型 │ ├── product.js # 商品相关数据模型
│ ├── order.js # 订单相关数据模型 │ ├── order.js # 订单相关数据模型
│ ├── user.js # 用户相关数据模型 │ ├── user.js # 用户相关数据模型
── cart.js # 购物车相关数据模型 ── cart.js # 购物车相关数据模型
│ └── announcement.js # 通知公告相关数据模型
└── apis/ # API 接口定义 └── apis/ # API 接口定义
├── products.js # 商品相关 API ├── products.js # 商品相关 API
── orders.js # 订单相关 API ── orders.js # 订单相关 API
└── announcements.js # 通知公告相关 API
``` ```
## 优势 ## 优势

736
docs/apis/announcements.js Normal file
View File

@@ -0,0 +1,736 @@
/**
* @swagger
* tags:
* name: Announcements
* description: 通知公告管理API
*/
/**
* @swagger
* /api/announcements/{id}:
* get:
* summary: 获取单个公告详情
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* responses:
* 200:
* description: 成功获取公告详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Announcement'
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/{id}/read:
* post:
* summary: 标记公告为已读
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* responses:
* 200:
* description: 标记已读成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "已标记为已读"
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/unread/count:
* get:
* summary: 获取用户未读公告数量
* tags: [Announcements]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取未读数量成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* unread_count:
* type: integer
* example: 5
* description: 未读公告数量
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/batch/read:
* post:
* summary: 批量标记公告为已读
* tags: [Announcements]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - announcement_ids
* properties:
* announcement_ids:
* type: array
* items:
* type: integer
* example: [1, 2, 3]
* description: 公告ID列表
* responses:
* 200:
* description: 批量标记已读成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "批量标记已读成功"
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements:
* post:
* summary: 创建新公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - title
* - content
* properties:
* title:
* type: string
* description: 公告标题
* content:
* type: string
* description: 公告内容
* type:
* type: string
* enum: [system, maintenance, promotion, warning]
* default: system
* priority:
* type: string
* enum: [low, medium, high, urgent]
* default: medium
* status:
* type: string
* enum: [draft, published]
* default: draft
* is_pinned:
* type: boolean
* default: false
* publish_time:
* type: string
* format: date-time
* expire_time:
* type: string
* format: date-time
* responses:
* 201:
* description: 公告创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* id:
* type: integer
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/{id}:
* put:
* summary: 更新公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* content:
* type: string
* type:
* type: string
* enum: [system, maintenance, promotion, warning]
* priority:
* type: string
* enum: [low, medium, high, urgent]
* status:
* type: string
* enum: [draft, published, archived]
* is_pinned:
* type: boolean
* publish_time:
* type: string
* format: date-time
* expire_time:
* type: string
* format: date-time
* responses:
* 200:
* description: 公告更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* $ref: '#/components/schemas/Announcement'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*
* delete:
* summary: 删除公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* responses:
* 200:
* description: 公告删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/public/list:
* get:
* summary: 获取公开发布的公告列表(无需认证)
* tags: [Announcements]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* default: 5
* description: 获取数量
* responses:
* 200:
* description: 成功获取公开公告列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/Announcement'
* 500:
* description: 服务器错误
*/
/**
* @swagger
* components:
* schemas:
* Announcement:
* type: object
* required:
* - title
* - content
* properties:
* id:
* type: integer
* description: 公告ID
* title:
* type: string
* description: 公告标题
* content:
* type: string
* description: 公告内容
* type:
* type: string
* description: 公告类型
* enum: [system, maintenance, promotion, warning]
* priority:
* type: string
* description: 优先级
* enum: [low, medium, high, urgent]
* status:
* type: string
* description: 状态
* enum: [draft, published, archived]
* is_pinned:
* type: boolean
* description: 是否置顶
* publish_time:
* type: string
* format: date-time
* description: 发布时间
* expire_time:
* type: string
* format: date-time
* description: 过期时间
* created_by:
* type: integer
* description: 创建者ID
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/
/**
* @swagger
* /api/announcements:
* get:
* summary: 获取通知公告列表
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词(标题或内容)
* - in: query
* name: type
* schema:
* type: string
* enum: [system, activity, maintenance, urgent]
* description: 公告类型
* - in: query
* name: priority
* schema:
* type: string
* enum: [high, medium, low]
* description: 优先级
* - in: query
* name: status
* schema:
* type: string
* enum: [draft, published, expired]
* description: 状态
* - in: query
* name: isTop
* schema:
* type: boolean
* description: 是否置顶
* - in: query
* name: sortBy
* schema:
* type: string
* enum: [created_at, updated_at, publish_time, priority]
* default: created_at
* description: 排序字段
* - in: query
* name: sortOrder
* schema:
* type: string
* enum: [ASC, DESC]
* default: DESC
* description: 排序方向
* responses:
* 200:
* description: 成功获取公告列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* announcements:
* type: array
* items:
* $ref: '#/components/schemas/Announcement'
* total:
* type: integer
* example: 50
* page:
* type: integer
* example: 1
* limit:
* type: integer
* example: 10
* totalPages:
* type: integer
* example: 5
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*
* post:
* summary: 创建新的通知公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - title
* - content
* - type
* - priority
* properties:
* title:
* type: string
* description: 公告标题
* example: "系统维护通知"
* content:
* type: string
* description: 公告内容
* example: "系统将于今晚进行维护预计维护时间2小时"
* type:
* type: string
* enum: [system, activity, maintenance, urgent]
* description: 公告类型
* example: "maintenance"
* priority:
* type: string
* enum: [high, medium, low]
* description: 优先级
* example: "high"
* status:
* type: string
* enum: [draft, published]
* default: draft
* description: 状态
* isTop:
* type: boolean
* default: false
* description: 是否置顶
* publishTime:
* type: string
* format: date-time
* description: 发布时间
* expireTime:
* type: string
* format: date-time
* description: 过期时间
* responses:
* 201:
* description: 公告创建成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "公告创建成功"
* data:
* $ref: '#/components/schemas/Announcement'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/{id}:
* put:
* summary: 更新通知公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* description: 公告标题
* content:
* type: string
* description: 公告内容
* type:
* type: string
* enum: [system, activity, maintenance, urgent]
* description: 公告类型
* priority:
* type: string
* enum: [high, medium, low]
* description: 优先级
* status:
* type: string
* enum: [draft, published, expired]
* description: 状态
* isTop:
* type: boolean
* description: 是否置顶
* publishTime:
* type: string
* format: date-time
* description: 发布时间
* expireTime:
* type: string
* format: date-time
* description: 过期时间
* responses:
* 200:
* description: 公告更新成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "公告更新成功"
* data:
* $ref: '#/components/schemas/Announcement'
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*
* delete:
* summary: 删除通知公告
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* responses:
* 200:
* description: 公告删除成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "公告删除成功"
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/announcements/{id}/toggle-top:
* put:
* summary: 切换公告置顶状态
* tags: [Announcements]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 公告ID
* responses:
* 200:
* description: 置顶状态切换成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "置顶状态更新成功"
* data:
* type: object
* properties:
* isTop:
* type: boolean
* example: true
* 401:
* description: 未授权
* 404:
* description: 公告不存在
* 500:
* description: 服务器错误
*/

View File

@@ -7,149 +7,12 @@
/** /**
* @swagger * @swagger
* /orders/create-from-cart: * /api/orders:
* post:
* summary: 从购物车创建预订单
* tags: [Orders]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - cartIds
* properties:
* cartIds:
* type: array
* items:
* type: integer
* description: 购物车商品ID数组
* responses:
* 200:
* description: 成功创建预订单
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/PreOrder'
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
*/
/**
* @swagger
* /orders/pre-order/{id}:
* get: * get:
* summary: 获取订单详情 * summary: 获取订单列表
* tags: [Orders]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 预订单ID
* responses:
* 200:
* description: 成功获取预订单详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* preOrder:
* $ref: '#/components/schemas/PreOrder'
* items:
* type: array
* items:
* type: object
* properties:
* product_id:
* type: integer
* product_name:
* type: string
* quantity:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: number
* image_url:
* type: string
* 404:
* description: 预订单不存在
*/
/**
* @swagger
* /orders/confirm:
* post:
* summary: 确认下单
* tags: [Orders]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - preOrderId
* - shippingAddress
* properties:
* preOrderId:
* type: integer
* description: 预订单ID
* shippingAddress:
* type: string
* description: 收货地址
* responses:
* 200:
* description: 订单确认成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* orderId:
* type: integer
* orderNumber:
* type: string
* 400:
* description: 请求参数错误或余额不足
* 404:
* description: 预订单不存在
*/
/**
* @swagger
* /orders:
* get:
* summary: 获取用户订单列表
* tags: [Orders] * tags: [Orders]
* security:
* - bearerAuth: []
* parameters: * parameters:
* - in: query * - in: query
* name: page * name: page
@@ -164,11 +27,37 @@
* default: 10 * default: 10
* description: 每页数量 * description: 每页数量
* - in: query * - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: orderNumber
* schema:
* type: string
* description: 订单号
* - in: query
* name: username
* schema:
* type: string
* description: 用户名
* - in: query
* name: status * name: status
* schema: * schema:
* type: string * type: string
* enum: [pending, confirmed, shipped, delivered, cancelled] * description: 订单状态
* description: 订单状态筛选 * - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 结束日期
* responses: * responses:
* 200: * 200:
* description: 成功获取订单列表 * description: 成功获取订单列表
@@ -197,14 +86,162 @@
* type: integer * type: integer
* pages: * pages:
* type: integer * type: integer
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/ */
/** /**
* @swagger * @swagger
* /orders/{id}: * /api/orders/confirm:
* get: * post:
* summary: 获取订单详情 * summary: 确认下单
* tags: [Orders] * tags: [Orders]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - pre_order_id
* - address
* properties:
* pre_order_id:
* type: integer
* description: 预订单ID
* address:
* type: object
* properties:
* recipient_name:
* type: string
* description: 收货人姓名
* phone:
* type: string
* description: 收货人电话
* province:
* type: string
* description: 省份
* city:
* type: string
* description: 城市
* district:
* type: string
* description: 区县
* detail_address:
* type: string
* description: 详细地址
* responses:
* 200:
* description: 确认下单成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* order_id:
* type: integer
* order_no:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 预订单不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/orders/pre-order/{id}:
* get:
* summary: 获取预订单详情
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 预订单ID
* responses:
* 200:
* description: 获取预订单详情成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* id:
* type: integer
* order_no:
* type: string
* total_amount:
* type: integer
* total_points:
* type: integer
* total_rongdou:
* type: integer
* status:
* type: string
* created_at:
* type: string
* items:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* product_id:
* type: integer
* product_name:
* type: string
* quantity:
* type: integer
* price:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: integer
* spec_info:
* type: object
* 401:
* description: 未授权
* 404:
* description: 预订单不存在
* 500:
* description: 服务器错误
*/
/**
* @swagger
* /api/orders/{id}:
* get:
* summary: 获取单个订单详情
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters: * parameters:
* - in: path * - in: path
* name: id * name: id
@@ -227,10 +264,10 @@
* properties: * properties:
* order: * order:
* $ref: '#/components/schemas/Order' * $ref: '#/components/schemas/Order'
* items: * 401:
* type: array * description: 未授权
* items:
* $ref: '#/components/schemas/OrderItem'
* 404: * 404:
* description: 订单不存在 * description: 订单不存在
* 500:
* description: 服务器错误
*/ */

View File

@@ -0,0 +1,225 @@
/**
* @swagger
* components:
* schemas:
* Announcement:
* type: object
* required:
* - title
* - content
* - type
* - priority
* properties:
* id:
* type: integer
* description: 公告ID
* example: 1
* title:
* type: string
* description: 公告标题
* example: "系统维护通知"
* content:
* type: string
* description: 公告内容
* example: "系统将于今晚进行维护预计维护时间2小时期间可能影响部分功能使用。"
* type:
* type: string
* description: 公告类型
* enum: [system, activity, maintenance, urgent]
* example: "maintenance"
* priority:
* type: string
* description: 优先级
* enum: [high, medium, low]
* example: "high"
* status:
* type: string
* description: 公告状态
* enum: [draft, published, expired]
* example: "published"
* isTop:
* type: boolean
* description: 是否置顶
* example: false
* publishTime:
* type: string
* format: date-time
* description: 发布时间
* example: "2024-01-15T10:00:00Z"
* expireTime:
* type: string
* format: date-time
* description: 过期时间
* example: "2024-01-20T10:00:00Z"
* createdBy:
* type: integer
* description: 创建者用户ID
* example: 1
* createdAt:
* type: string
* format: date-time
* description: 创建时间
* example: "2024-01-15T09:00:00Z"
* updatedAt:
* type: string
* format: date-time
* description: 更新时间
* example: "2024-01-15T09:30:00Z"
* creator:
* type: object
* description: 创建者信息
* properties:
* id:
* type: integer
* example: 1
* username:
* type: string
* example: "admin"
* email:
* type: string
* example: "admin@example.com"
*
* AnnouncementCreate:
* type: object
* required:
* - title
* - content
* - type
* - priority
* properties:
* title:
* type: string
* description: 公告标题
* example: "系统维护通知"
* content:
* type: string
* description: 公告内容
* example: "系统将于今晚进行维护预计维护时间2小时。"
* type:
* type: string
* description: 公告类型
* enum: [system, activity, maintenance, urgent]
* example: "maintenance"
* priority:
* type: string
* description: 优先级
* enum: [high, medium, low]
* example: "high"
* status:
* type: string
* description: 公告状态
* enum: [draft, published]
* default: draft
* example: "draft"
* isTop:
* type: boolean
* description: 是否置顶
* default: false
* example: false
* publishTime:
* type: string
* format: date-time
* description: 发布时间
* example: "2024-01-15T10:00:00Z"
* expireTime:
* type: string
* format: date-time
* description: 过期时间
* example: "2024-01-20T10:00:00Z"
*
* AnnouncementUpdate:
* type: object
* properties:
* title:
* type: string
* description: 公告标题
* example: "系统维护通知(更新)"
* content:
* type: string
* description: 公告内容
* example: "系统维护时间调整为明晚进行。"
* type:
* type: string
* description: 公告类型
* enum: [system, activity, maintenance, urgent]
* example: "maintenance"
* priority:
* type: string
* description: 优先级
* enum: [high, medium, low]
* example: "medium"
* status:
* type: string
* description: 公告状态
* enum: [draft, published, expired]
* example: "published"
* isTop:
* type: boolean
* description: 是否置顶
* example: true
* publishTime:
* type: string
* format: date-time
* description: 发布时间
* example: "2024-01-16T10:00:00Z"
* expireTime:
* type: string
* format: date-time
* description: 过期时间
* example: "2024-01-21T10:00:00Z"
*
* AnnouncementList:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* announcements:
* type: array
* items:
* $ref: '#/components/schemas/Announcement'
* total:
* type: integer
* description: 总记录数
* example: 50
* page:
* type: integer
* description: 当前页码
* example: 1
* limit:
* type: integer
* description: 每页数量
* example: 10
* totalPages:
* type: integer
* description: 总页数
* example: 5
*
* AnnouncementResponse:
* type: object
* properties:
* success:
* type: boolean
* example: true
* message:
* type: string
* example: "操作成功"
* data:
* $ref: '#/components/schemas/Announcement'
*
* AnnouncementError:
* type: object
* properties:
* success:
* type: boolean
* example: false
* message:
* type: string
* example: "操作失败"
* error:
* type: string
* example: "公告不存在"
*/

View File

@@ -34,6 +34,19 @@ const auth = async (req, res, next) => {
}); });
} }
// 检查支付状态(管理员除外)
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
console.log(11111);
return res.status(403).json({
success: false,
message: '您的账户尚未激活,请完成支付后再使用',
code: 'PAYMENT_REQUIRED',
needPayment: true,
userId: user.id
});
}
req.user = user; req.user = user;
next(); next();
} catch (error) { } catch (error) {
@@ -49,4 +62,51 @@ const adminAuth = (req, res, next) => {
next(); next();
}; };
module.exports = { auth, adminAuth, JWT_SECRET }; /**
* 支付认证中间件
* 只验证JWT令牌和用户状态不检查支付状态
* 用于支付相关接口,允许未支付用户创建支付订单
*/
const paymentAuth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ success: false, message: '未提供认证令牌' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const db = getDB();
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: '用户不存在' });
}
const user = users[0];
// 检查用户是否被拉黑
if (user.is_blacklisted) {
return res.status(403).json({
success: false,
message: '账户已被拉黑,请联系管理员',
code: 'USER_BLACKLISTED'
});
}
// 注意:这里不检查支付状态,允许未支付用户创建支付订单
req.user = user;
next();
} catch (error) {
console.error('支付认证失败:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: '无效的认证令牌' });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: '认证令牌已过期' });
}
return res.status(500).json({ success: false, message: '认证失败' });
}
};
module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET };

View File

@@ -0,0 +1,17 @@
-- 为支付订单表添加支付宝支持
-- 更新交易类型枚举,添加支付宝相关类型
ALTER TABLE `payment_orders` MODIFY COLUMN `trade_type` varchar(32) NOT NULL COMMENT '交易类型(H5/JSAPI/ALIPAY_WAP/ALIPAY_APP等)';
-- 更新transaction_id字段注释支持多种支付方式
ALTER TABLE `payment_orders` MODIFY COLUMN `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方支付订单号(微信/支付宝)';
-- 添加支付方式字段
ALTER TABLE `payment_orders` ADD COLUMN `payment_method` enum('wechat','alipay') DEFAULT 'wechat' COMMENT '支付方式' AFTER `trade_type`;
-- 添加支付宝特有字段
ALTER TABLE `payment_orders` ADD COLUMN `buyer_user_id` varchar(32) DEFAULT NULL COMMENT '支付宝买家用户ID' AFTER `transaction_id`;
ALTER TABLE `payment_orders` ADD COLUMN `trade_status` varchar(32) DEFAULT NULL COMMENT '支付宝交易状态' AFTER `buyer_user_id`;
-- 添加索引
ALTER TABLE `payment_orders` ADD KEY `idx_payment_method` (`payment_method`);
ALTER TABLE `payment_orders` ADD KEY `idx_trade_status` (`trade_status`);

View File

@@ -0,0 +1,27 @@
-- 创建支付订单表
CREATE TABLE IF NOT EXISTS `payment_orders` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`out_trade_no` varchar(64) NOT NULL COMMENT '商户订单号',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
`total_fee` int(11) NOT NULL COMMENT '订单金额(分)',
`body` varchar(128) NOT NULL COMMENT '商品描述',
`trade_type` varchar(16) NOT NULL COMMENT '交易类型',
`prepay_id` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识',
`mweb_url` text DEFAULT NULL COMMENT 'H5支付跳转链接',
`status` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态',
`paid_at` datetime DEFAULT NULL COMMENT '支付完成时间',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_out_trade_no` (`out_trade_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
-- 为用户表添加支付状态字段
ALTER TABLE `users` ADD COLUMN `payment_status` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态' AFTER `status`;
-- 添加索引
ALTER TABLE `users` ADD KEY `idx_payment_status` (`payment_status`);

381
package-lock.json generated
View File

@@ -11,10 +11,12 @@
"dependencies": { "dependencies": {
"@alicloud/dysmsapi20170525": "^4.1.2", "@alicloud/dysmsapi20170525": "^4.1.2",
"@alicloud/openapi-client": "^0.4.15", "@alicloud/openapi-client": "^0.4.15",
"alipay-sdk": "^4.14.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"express": "^4.18.2", "express": "^4.18.2",
@@ -27,10 +29,12 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"node-rsa": "^1.1.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"winston": "^3.17.0" "winston": "^3.17.0",
"xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
@@ -311,6 +315,28 @@
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
} }
}, },
"node_modules/@fidm/asn1": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz",
"integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@fidm/x509": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz",
"integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==",
"license": "MIT",
"dependencies": {
"@fidm/asn1": "^1.0.4",
"tweetnacl": "^1.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@hapi/hoek": { "node_modules/@hapi/hoek": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -407,6 +433,26 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/alipay-sdk": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/alipay-sdk/-/alipay-sdk-4.14.0.tgz",
"integrity": "sha512-oiD/VP5Ei0RRacHHmE+N0uqgOj2xzce7c0fHrtyyh1P04O+o9I1r65LdGPzU8960J56xOxS/d3c+R/9lsPUH7g==",
"license": "MIT",
"dependencies": {
"@fidm/x509": "^1.2.1",
"bignumber.js": "^9.1.2",
"camelcase-keys": "^7.0.2",
"crypto-js": "^4.2.0",
"formstream": "^1.5.0",
"snakecase-keys": "^8.0.0",
"sse-decoder": "^1.0.0",
"urllib": "^4",
"utility": "^2.1.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -463,6 +509,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/async": { "node_modules/async": {
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -522,6 +577,15 @@
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -714,6 +778,36 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelcase-keys": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz",
"integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==",
"license": "MIT",
"dependencies": {
"camelcase": "^6.3.0",
"map-obj": "^4.1.0",
"quick-lru": "^5.1.1",
"type-fest": "^1.2.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-keys/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -972,6 +1066,19 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
"license": "ISC"
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/date-fns": { "node_modules/date-fns": {
"version": "2.30.0", "version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -1094,6 +1201,16 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.2.1", "version": "17.2.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
@@ -1315,6 +1432,18 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-xml-parser": { "node_modules/fast-xml-parser": {
"version": "4.5.3", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
@@ -1449,6 +1578,30 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/formstream": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz",
"integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==",
"license": "MIT",
"dependencies": {
"destroy": "^1.0.4",
"mime": "^2.5.2",
"node-hex": "^1.0.1",
"pause-stream": "~0.0.11"
}
},
"node_modules/formstream/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1824,6 +1977,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2148,6 +2310,15 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "7.18.3", "version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@@ -2172,6 +2343,18 @@
"url": "https://github.com/sponsors/wellwelwel" "url": "https://github.com/sponsors/wellwelwel"
} }
}, },
"node_modules/map-obj": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2407,6 +2590,16 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
}
},
"node_modules/node-cron": { "node_modules/node-cron": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
@@ -2416,6 +2609,24 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/node-hex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz",
"integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/node-rsa": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
"integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
"license": "MIT",
"dependencies": {
"asn1": "^0.2.4"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@@ -2630,6 +2841,18 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"license": [
"MIT",
"Apache2"
],
"dependencies": {
"through": "~2.3"
}
},
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -2809,6 +3032,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -3163,6 +3398,42 @@
"integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"node_modules/snakecase-keys": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.1.0.tgz",
"integrity": "sha512-9/Eug2btrCiOi+9+vIXJnxUcKVfcbLy5Uwff4BrO6PQf3Oq/2iYQ/1zkmnrpIIjfel/DAasAlux7OvAmCa+Xnw==",
"license": "MIT",
"dependencies": {
"map-obj": "^4.2.0",
"snake-case": "^3.0.4",
"type-fest": "^4.15.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/snakecase-keys/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/spawn-command": { "node_modules/spawn-command": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
@@ -3187,6 +3458,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/sse-decoder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/sse-decoder/-/sse-decoder-1.0.0.tgz",
"integrity": "sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==",
"license": "MIT",
"engines": {
"node": ">= 14.19.3"
}
},
"node_modules/stack-trace": { "node_modules/stack-trace": {
"version": "0.0.10", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -3368,6 +3648,12 @@
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/through2": { "node_modules/through2": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
@@ -3446,9 +3732,26 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
},
"node_modules/type-fest": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3475,12 +3778,33 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz",
"integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unescape": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz",
"integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3490,6 +3814,36 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/urllib": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/urllib/-/urllib-4.8.2.tgz",
"integrity": "sha512-V5oo9kzQfF9UQAC9KOVFmmmbYPJ9nksgO8HM89BZse96QcCyjrssPVxKzL/9sVPRC8D4Sd3nAdaMCXAZ3dqEYA==",
"license": "MIT",
"dependencies": {
"form-data": "^4.0.1",
"formstream": "^1.5.1",
"mime-types": "^2.1.35",
"qs": "^6.12.1",
"type-fest": "^4.20.1",
"undici": "^7.1.1",
"ylru": "^2.0.0"
},
"engines": {
"node": ">= 18.19.0"
}
},
"node_modules/urllib/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/util": { "node_modules/util": {
"version": "0.12.5", "version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -3509,6 +3863,20 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/utility": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/utility/-/utility-2.5.0.tgz",
"integrity": "sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==",
"license": "MIT",
"dependencies": {
"escape-html": "^1.0.3",
"unescape": "^1.0.1",
"ylru": "^2.0.0"
},
"engines": {
"node": ">= 16.0.0"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -3742,6 +4110,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/ylru": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ylru/-/ylru-2.0.0.tgz",
"integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==",
"license": "MIT",
"engines": {
"node": ">= 18.19.0"
}
},
"node_modules/z-schema": { "node_modules/z-schema": {
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",

View File

@@ -9,10 +9,12 @@
"dependencies": { "dependencies": {
"@alicloud/dysmsapi20170525": "^4.1.2", "@alicloud/dysmsapi20170525": "^4.1.2",
"@alicloud/openapi-client": "^0.4.15", "@alicloud/openapi-client": "^0.4.15",
"alipay-sdk": "^4.14.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"express": "^4.18.2", "express": "^4.18.2",
@@ -25,10 +27,12 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"node-rsa": "^1.1.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"winston": "^3.17.0" "winston": "^3.17.0",
"xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.2", "concurrently": "^8.2.2",

399
routes/announcements.js Normal file
View File

@@ -0,0 +1,399 @@
const express = require('express');
const { getDB } = require('../database');
const { auth, adminAuth } = require('../middleware/auth');
const dayjs = require('dayjs');
const router = express.Router();
router.get('/', auth, async (req, res) => {
try {
const db = getDB();
const { page = 1, limit = 10, status, type, keyword } = req.query;
const offset = (page - 1) * limit;
let whereClause = 'WHERE 1=1';
const params = [];
if (status) {
whereClause += ' AND status = ?';
params.push(status);
}
if (type) {
whereClause += ' AND type = ?';
params.push(type);
}
if (keyword) {
whereClause += ' AND (title LIKE ? OR content LIKE ?)';
params.push(`%${keyword}%`, `%${keyword}%`);
}
// 获取总数
const countQuery = `SELECT COUNT(*) as total FROM announcements ${whereClause}`;
const [countResult] = await db.execute(countQuery, params);
const total = countResult[0].total;
// 获取公告列表(包含用户阅读状态)
const limitValue = Math.max(1, Math.min(100, parseInt(limit)));
const offsetValue = Math.max(0, parseInt(offset));
const query = `
SELECT a.*, u.username as creator_name,
uar.is_read,
uar.read_at,
CASE
WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1
ELSE 0
END as is_expired
FROM announcements a
LEFT JOIN users u ON a.created_by = u.id
LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ?
${whereClause}
ORDER BY a.is_pinned DESC, a.created_at DESC
LIMIT ${limitValue} OFFSET ${offsetValue}
`;
const [announcements] = await db.execute(query, [req.user.id, ...params]);
// 自动将过期公告标记为已读
console.log(announcements);
const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read);
console.log(expiredUnreadAnnouncements);
if (expiredUnreadAnnouncements.length > 0) {
const expiredIds = expiredUnreadAnnouncements.map(a => a.id);
await db.execute(`
INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at)
VALUES ${expiredIds.map(() => '(?, ?, TRUE, NOW())').join(', ')}
ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW()
`, expiredIds.flatMap(id => [req.user.id, id]));
// 更新返回数据中的阅读状态
expiredUnreadAnnouncements.forEach(a => {
a.is_read = true;
a.read_at = new Date();
});
}
res.json({
success: true,
data: {
announcements,
total,
page: parseInt(page),
limit: parseInt(limit),
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('获取公告列表失败:', error);
res.status(500).json({ success: false, message: '获取公告列表失败' });
}
});
router.get('/:id', auth, async (req, res) => {
try {
const db = getDB();
const { id } = req.params;
const query = `
SELECT a.*, u.username as creator_name,
uar.is_read,
uar.read_at,
CASE
WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1
ELSE 0
END as is_expired
FROM announcements a
LEFT JOIN users u ON a.created_by = u.id
LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ?
WHERE a.id = ?
`;
const [result] = await db.execute(query, [req.user.id, id]);
if (result.length === 0) {
return res.status(404).json({ success: false, message: '公告不存在' });
}
const announcement = result[0];
// 如果公告未读或已过期但未标记为已读,则标记为已读
if (!announcement.is_read || (announcement.is_expired && !announcement.is_read)) {
await db.execute(`
INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at)
VALUES (?, ?, TRUE, NOW())
ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW()
`, [req.user.id, id]);
announcement.is_read = true;
announcement.read_at = new Date();
}
res.json({ success: true, data: announcement });
} catch (error) {
console.error('获取公告详情失败:', error);
res.status(500).json({ success: false, message: '获取公告详情失败' });
}
});
router.post('/', auth, adminAuth, async (req, res) => {
try {
const db = getDB();
const {
title,
content,
type = 'system',
priority = 'medium',
status = 'draft',
is_pinned = false,
publish_time,
expire_time
} = req.body;
if (!title || !content) {
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
}
const query = `
INSERT INTO announcements (
title, content, type, priority, status, is_pinned,
publish_time, expire_time, created_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`;
const [result] = await db.execute(query, [
title,
content,
type,
priority,
status,
is_pinned,
publish_time || null,
expire_time || null,
req.user.id
]);
res.status(201).json({
success: true,
message: '公告创建成功',
data: { id: result.insertId }
});
} catch (error) {
console.error('创建公告失败:', error);
res.status(500).json({ success: false, message: '创建公告失败' });
}
});
router.put('/:id', auth, adminAuth, async (req, res) => {
try {
const db = getDB();
const { id } = req.params;
let {
title,
content,
type,
priority,
status,
is_pinned,
publish_time,
expire_time
} = req.body;
// 检查公告是否存在
const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]);
if (existing.length === 0) {
return res.status(404).json({ success: false, message: '公告不存在' });
}
const updates = [];
const params = [];
if (title !== undefined) {
updates.push('title = ?');
params.push(title);
}
if (content !== undefined) {
updates.push('content = ?');
params.push(content);
}
if (type !== undefined) {
updates.push('type = ?');
params.push(type);
}
if (priority !== undefined) {
updates.push('priority = ?');
params.push(priority);
}
if (status !== undefined) {
updates.push('status = ?');
params.push(status);
}
if (is_pinned !== undefined) {
updates.push('is_pinned = ?');
params.push(is_pinned);
}
if (publish_time !== undefined) {
updates.push('publish_time = ?');
publish_time = dayjs(publish_time).format('YYYY-MM-DD');
params.push(publish_time);
}
if (expire_time !== undefined) {
updates.push('expire_time = ?');
params.push(expire_time);
}
if (updates.length === 0) {
return res.status(400).json({ success: false, message: '没有要更新的字段' });
}
updates.push('updated_at = NOW()');
params.push(id);
const query = `UPDATE announcements SET ${updates.join(', ')} WHERE id = ?`;
await db.execute(query, params);
res.json({ success: true, message: '公告更新成功' });
} catch (error) {
console.error('更新公告失败:', error);
res.status(500).json({ success: false, message: '更新公告失败' });
}
});
router.delete('/:id', auth, adminAuth, async (req, res) => {
try {
const db = getDB();
const { id } = req.params;
// 检查公告是否存在
const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]);
if (existing.length === 0) {
return res.status(404).json({ success: false, message: '公告不存在' });
}
await db.execute('DELETE FROM announcements WHERE id = ?', [id]);
res.json({ success: true, message: '公告删除成功' });
} catch (error) {
console.error('删除公告失败:', error);
res.status(500).json({ success: false, message: '删除公告失败' });
}
});
router.get('/public/list', async (req, res) => {
try {
const db = getDB();
const { limit = 5 } = req.query;
const limitValue = Math.max(1, Math.min(50, parseInt(limit)));
const query = `
SELECT id, title, content, type, priority, publish_time, created_at
FROM announcements
WHERE status = 'published'
AND (expire_time IS NULL OR expire_time > NOW())
AND (publish_time IS NULL OR publish_time <= NOW())
ORDER BY is_pinned DESC, created_at DESC
LIMIT ${limitValue}
`;
const [announcements] = await db.execute(query, []);
res.json({ success: true, data: announcements });
} catch (error) {
console.error('获取公开公告失败:', error);
res.status(500).json({ success: false, message: '获取公开公告失败' });
}
});
// 标记公告为已读
router.post('/:id/read', auth, async (req, res) => {
try {
const db = getDB();
const { id } = req.params;
// 检查公告是否存在
const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]);
if (existing.length === 0) {
return res.status(404).json({ success: false, message: '公告不存在' });
}
// 标记为已读
await db.execute(`
INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at)
VALUES (?, ?, TRUE, NOW())
ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW()
`, [req.user.id, id]);
res.json({ success: true, message: '已标记为已读' });
} catch (error) {
console.error('标记公告已读失败:', error);
res.status(500).json({ success: false, message: '标记公告已读失败' });
}
});
// 获取用户未读公告数量
router.get('/unread/count', auth, async (req, res) => {
try {
const db = getDB();
const query = `
SELECT COUNT(*) as unread_count
FROM announcements a
LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ?
WHERE a.status = 'published'
AND (a.publish_time IS NULL OR a.publish_time <= NOW())
AND (a.expire_time IS NULL OR a.expire_time > NOW())
AND (uar.is_read IS NULL OR uar.is_read = FALSE)
`;
const [result] = await db.execute(query, [req.user.id]);
res.json({
success: true,
data: {
unread_count: result[0].unread_count
}
});
} catch (error) {
console.error('获取未读公告数量失败:', error);
res.status(500).json({ success: false, message: '获取未读公告数量失败' });
}
});
// 批量标记公告为已读
router.post('/batch/read', auth, async (req, res) => {
try {
const db = getDB();
const { announcement_ids } = req.body;
if (!announcement_ids || !Array.isArray(announcement_ids) || announcement_ids.length === 0) {
return res.status(400).json({ success: false, message: '请提供有效的公告ID列表' });
}
// 批量标记为已读
const values = announcement_ids.map(() => '(?, ?, TRUE, NOW())').join(', ');
const params = announcement_ids.flatMap(id => [req.user.id, id]);
await db.execute(`
INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at)
VALUES ${values}
ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW()
`, params);
res.json({ success: true, message: '批量标记已读成功' });
} catch (error) {
console.error('批量标记公告已读失败:', error);
res.status(500).json({ success: false, message: '批量标记公告已读失败' });
}
});
module.exports = router;

View File

@@ -122,139 +122,105 @@ router.post('/register', async (req, res) => {
try { try {
const db = getDB(); const db = getDB();
await db.query('START TRANSACTION'); await db.query('START TRANSACTION');
const { const {
username, username,
phone, phone,
password, password,
registrationCode,
city, city,
district_id: district, district_id: district,
captchaId, captchaId,
captchaText, captchaText,
smsCode, // 短信验证码 smsCode, // 短信验证码
role = 'user' role = 'user'
} = req.body; } = req.body;
if (!username || !phone || !password || !registrationCode || !city || !district) { if (!username || !phone || !password || !city || !district) {
return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' }); return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' });
} }
if (!captchaId || !captchaText) { if (!captchaId || !captchaText) {
return res.status(400).json({ success: false, message: '图形验证码不能为空' }); return res.status(400).json({ success: false, message: '图形验证码不能为空' });
} }
if (!smsCode) { if (!smsCode) {
return res.status(400).json({ success: false, message: '短信验证码不能为空' }); return res.status(400).json({ success: false, message: '短信验证码不能为空' });
} }
// 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 // 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
// 验证短信验证码 // 验证短信验证码
const smsAPI = require('./sms'); const smsAPI = require('./sms');
const smsValid = smsAPI.verifySMSCode(phone, smsCode); const smsValid = smsAPI.verifySMSCode(phone, smsCode);
if (!smsValid) { if (!smsValid) {
return res.status(400).json({ success: false, message: '短信验证码错误或已过期' }); return res.status(400).json({ success: false, message: '短信验证码错误或已过期' });
} }
// 验证手机号格式 // 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/; const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(phone)) { if (!phoneRegex.test(phone)) {
return res.status(400).json({ success: false, message: '手机号格式不正确' }); return res.status(400).json({ success: false, message: '手机号格式不正确' });
} }
// 验证激活码
const [registrationCodes] = await db.execute(
'SELECT id, is_used, expires_at, agent_id FROM registration_codes WHERE code = ?',
[registrationCode]
);
if (registrationCodes.length === 0) {
return res.status(400).json({ success: false, message: '激活码不存在' });
}
const regCode = registrationCodes[0];
// 检查激活码是否已使用
if (regCode.is_used) {
return res.status(400).json({ success: false, message: '激活码已被使用' });
}
// 检查激活码是否过期
if (new Date() > new Date(regCode.expires_at)) {
return res.status(400).json({ success: false, message: '激活码已过期' });
}
// 检查用户是否已存在 // 检查用户是否已存在
const [existingUsers] = await db.execute( const [existingUsers] = await db.execute(
'SELECT id FROM users WHERE username = ? OR phone = ?', 'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?',
[username, phone] [username, phone]
); );
if (existingUsers.length > 0) { if (existingUsers.length > 0) {
return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); const existingUser = existingUsers[0];
// 如果用户存在但未支付,允许重新注册(覆盖原用户信息)
if (existingUser.payment_status === 'unpaid') {
// 删除未支付的用户记录
await db.execute('DELETE FROM users WHERE id = ?', [existingUser.id]);
} else {
return res.status(400).json({ success: false, message: '用户名或手机号已存在' });
}
} }
// 加密密码 // 加密密码
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
// 创建用户(待审核状态,可以进入系统但匹配需审核 // 创建用户(初始状态为未支付
const [result] = await db.execute( const [result] = await db.execute(
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid")',
[username, phone, hashedPassword, role, 0, 'pending', city, district] [username, phone, hashedPassword, role, 0, 'pending', city, district]
); );
const userId = result.insertId; const userId = result.insertId;
// 用户余额已在创建用户时设置为默认值0.00,无需额外操作 // 用户余额已在创建用户时设置为默认值0.00,无需额外操作
// 标记激活码为已使用
await db.execute(
'UPDATE registration_codes SET is_used = TRUE, used_at = NOW(), used_by_user_id = ? WHERE id = ?', // 根据地区自动关联代理
[userId, regCode.id] const [agents] = await db.execute(
'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1',
[district]
); );
// 如果是代理邀请码,建立代理关系 if (agents.length > 0) {
if (regCode.agent_id) { await db.execute(
// 验证agent_id是否存在于regional_agents表中 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
const [agentExists] = await db.execute( [agents[0].id, userId]
'SELECT id FROM regional_agents WHERE id = ?',
[regCode.agent_id]
); );
if (agentExists.length > 0) {
await db.execute(
'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
[regCode.agent_id, userId]
);
}
} else {
// 如果不是代理邀请码,根据地区自动关联代理
const [agents] = await db.execute(
'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1',
[district]
);
if (agents.length > 0) {
await db.execute(
'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
[agents[0].id, userId]
);
}
} }
await db.query('COMMIT'); await db.query('COMMIT');
// 生成JWT token // 生成JWT token(用于支付流程)
const token = jwt.sign( const token = jwt.sign(
{ userId: userId, username, role }, { userId: userId, username, role },
JWT_SECRET, JWT_SECRET,
{ expiresIn: '24h' } { expiresIn: '24h' }
); );
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: '注册成功', message: '用户信息创建成功,请完成支付以激活账户',
token, token,
user: { user: {
id: userId, id: userId,
@@ -264,8 +230,10 @@ router.post('/register', async (req, res) => {
points: 0, points: 0,
audit_status: 'pending', audit_status: 'pending',
city, city,
district district,
} paymentStatus: 'unpaid'
},
needPayment: true
}); });
} catch (error) { } catch (error) {
try { try {
@@ -275,8 +243,8 @@ router.post('/register', async (req, res) => {
} }
console.error('注册错误详情:', error); console.error('注册错误详情:', error);
console.error('错误堆栈:', error.stack); console.error('错误堆栈:', error.stack);
res.status(500).json({ res.status(500).json({
success: false, success: false,
message: '注册失败', message: '注册失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined error: process.env.NODE_ENV === 'development' ? error.message : undefined
}); });
@@ -336,25 +304,25 @@ router.post('/login', async (req, res) => {
try { try {
const db = getDB(); const db = getDB();
const { username, password, captchaId, captchaText } = req.body; const { username, password, captchaId, captchaText } = req.body;
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); return res.status(400).json({ success: false, message: '用户名和密码不能为空' });
} }
if (!captchaId || !captchaText) { if (!captchaId || !captchaText) {
return res.status(400).json({ success: false, message: '验证码不能为空' }); return res.status(400).json({ success: false, message: '验证码不能为空' });
} }
// 获取存储的验证码 // 获取存储的验证码
const storedCaptcha = global.captchaStore.get(captchaId); const storedCaptcha = global.captchaStore.get(captchaId);
console.log(storedCaptcha); console.log(storedCaptcha);
if (!storedCaptcha) { if (!storedCaptcha) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '验证码不存在或已过期' message: '验证码不存在或已过期'
}); });
} }
// 检查是否过期 // 检查是否过期
if (Date.now() > storedCaptcha.expires) { if (Date.now() > storedCaptcha.expires) {
global.captchaStore.delete(captchaId); global.captchaStore.delete(captchaId);
@@ -363,61 +331,77 @@ router.post('/login', async (req, res) => {
message: '验证码已过期' message: '验证码已过期'
}); });
} }
// 验证验证码(不区分大小写) // 验证验证码(不区分大小写)
const isValid = storedCaptcha.text === captchaText.toLowerCase(); const isValid = storedCaptcha.text === captchaText.toLowerCase();
// 删除已验证的验证码 // 删除已验证的验证码
global.captchaStore.delete(captchaId); global.captchaStore.delete(captchaId);
if (!isValid) { if (!isValid) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: '验证码错误' message: '验证码错误'
}); });
} }
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
// 查找用户 // 查找用户(包含支付状态)
console.log('登录尝试 - 用户名:', username); console.log('登录尝试 - 用户名:', username);
const [users] = await db.execute( const [users] = await db.execute(
'SELECT * FROM users WHERE username = ?', 'SELECT * FROM users WHERE username = ?',
[username] [username]
); );
console.log('查找到的用户数量:', users.length); console.log('查找到的用户数量:', users.length);
if (users.length === 0) { if (users.length === 0) {
console.log('用户不存在:', username); console.log('用户不存在:', username);
return res.status(401).json({ success: false, message: '用户名或密码错误' }); return res.status(401).json({ success: false, message: '用户名或密码错误' });
} }
const user = users[0]; const user = users[0];
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null'); console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
// 验证密码 // 验证密码
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null'); console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
const isValidPassword = await bcrypt.compare(password, user.password); const isValidPassword = await bcrypt.compare(password, user.password);
console.log('密码验证结果:', isValidPassword); console.log('密码验证结果:', isValidPassword);
if (!isValidPassword) { if (!isValidPassword) {
console.log('密码验证失败'); console.log('密码验证失败');
return res.status(401).json({ success: false, message: '用户名或密码错误' }); return res.status(401).json({ success: false, message: '用户名或密码错误' });
} }
// 检查支付状态(管理员除外)
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: '5m' }
);
return res.status(200).json({
success: false,
message: '您的账户尚未激活,请完成支付后再登录',
needPayment: true,
user: user[0],
token
});
}
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户) // 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
if (user.role !== 'admin' && user.audit_status === 'rejected') { if (user.role !== 'admin' && user.audit_status === 'rejected') {
return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' }); return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' });
} }
// 待审核用户可以正常登录使用系统,但匹配功能会有限制 // 待审核用户可以正常登录使用系统,但匹配功能会有限制
// 生成JWT token // 生成JWT token
const token = jwt.sign( const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role }, { userId: user.id, username: user.username, role: user.role },
JWT_SECRET, JWT_SECRET,
{ expiresIn: '24h' } { expiresIn: '24h' }
); );
res.json({ res.json({
success: true, success: true,
message: '登录成功', message: '登录成功',
@@ -427,7 +411,8 @@ router.post('/login', async (req, res) => {
username: user.username, username: user.username,
role: user.role, role: user.role,
avatar: user.avatar, avatar: user.avatar,
points: user.points points: user.points,
payment_status: user.payment_status
} }
}); });
} catch (error) { } catch (error) {
@@ -440,11 +425,11 @@ router.post('/login', async (req, res) => {
const authenticateToken = (req, res, next) => { const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization']; const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; const token = authHeader && authHeader.split(' ')[1];
if (!token) { if (!token) {
return res.status(401).json({ success: false, message: '访问令牌缺失' }); return res.status(401).json({ success: false, message: '访问令牌缺失' });
} }
jwt.verify(token, JWT_SECRET, (err, user) => { jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) { if (err) {
return res.status(403).json({ success: false, message: '访问令牌无效' }); return res.status(403).json({ success: false, message: '访问令牌无效' });
@@ -462,11 +447,11 @@ router.get('/me', authenticateToken, async (req, res) => {
'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?', 'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?',
[req.user.userId] [req.user.userId]
); );
if (users.length === 0) { if (users.length === 0) {
return res.status(404).json({ success: false, message: '用户不存在' }); return res.status(404).json({ success: false, message: '用户不存在' });
} }
res.json({ success: true, user: users[0] }); res.json({ success: true, user: users[0] });
} catch (error) { } catch (error) {
console.error('获取用户信息错误:', error); console.error('获取用户信息错误:', error);
@@ -479,37 +464,37 @@ router.put('/change-password', authenticateToken, async (req, res) => {
try { try {
const db = getDB(); const db = getDB();
const { currentPassword, newPassword } = req.body; const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {
return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' }); return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' });
} }
// 获取用户当前密码 // 获取用户当前密码
const [users] = await db.execute( const [users] = await db.execute(
'SELECT password FROM users WHERE id = ?', 'SELECT password FROM users WHERE id = ?',
[req.user.userId] [req.user.userId]
); );
if (users.length === 0) { if (users.length === 0) {
return res.status(404).json({ success: false, message: '用户不存在' }); return res.status(404).json({ success: false, message: '用户不存在' });
} }
// 验证旧密码 // 验证旧密码
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password); const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
if (!isValidPassword) { if (!isValidPassword) {
return res.status(400).json({ success: false, message: '旧密码错误' }); return res.status(400).json({ success: false, message: '旧密码错误' });
} }
// 加密新密码 // 加密新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 10); const hashedNewPassword = await bcrypt.hash(newPassword, 10);
// 更新密码 // 更新密码
await db.execute( await db.execute(
'UPDATE users SET password = ? WHERE id = ?', 'UPDATE users SET password = ? WHERE id = ?',
[hashedNewPassword, req.user.userId] [hashedNewPassword, req.user.userId]
); );
res.json({ success: true, message: '密码修改成功' }); res.json({ success: true, message: '密码修改成功' });
} catch (error) { } catch (error) {
console.error('修改密码错误:', error); console.error('修改密码错误:', error);

View File

@@ -114,9 +114,11 @@ router.get('/', auth, async (req, res) => {
} }
// 处理地址信息 // 处理地址信息
console.log(order.address,'order.address');
if (order.address) { if (order.address) {
try { try {
order.address = JSON.parse(order.address); order.address = order.address;
} catch (e) { } catch (e) {
order.address = null; order.address = null;
} }
@@ -138,76 +140,7 @@ router.get('/', auth, async (req, res) => {
} }
}); });
/**
* @swagger
* /api/orders/confirm:
* post:
* summary: 确认下单
* tags: [Orders]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - pre_order_id
* - address
* properties:
* pre_order_id:
* type: integer
* description: 预订单ID
* address:
* type: object
* properties:
* recipient_name:
* type: string
* description: 收货人姓名
* phone:
* type: string
* description: 收货人电话
* province:
* type: string
* description: 省份
* city:
* type: string
* description: 城市
* district:
* type: string
* description: 区县
* detail_address:
* type: string
* description: 详细地址
* responses:
* 200:
* description: 确认下单成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* order_id:
* type: integer
* order_no:
* type: string
* 400:
* description: 请求参数错误
* 401:
* description: 未授权
* 404:
* description: 预订单不存在
* 500:
* description: 服务器错误
*/
router.post('/confirm', auth, async (req, res) => { router.post('/confirm', auth, async (req, res) => {
const connection = await getDB().getConnection(); const connection = await getDB().getConnection();
@@ -331,76 +264,7 @@ router.get('/', auth, async (req, res) => {
} }
}); });
/**
* @swagger
* /api/orders/pre-order/{id}:
* get:
* summary: 获取预订单详情
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 预订单ID
* responses:
* 200:
* description: 获取预订单详情成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* id:
* type: integer
* order_no:
* type: string
* total_amount:
* type: integer
* total_points:
* type: integer
* total_rongdou:
* type: integer
* status:
* type: string
* created_at:
* type: string
* items:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* product_id:
* type: integer
* product_name:
* type: string
* quantity:
* type: integer
* price:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: integer
* spec_info:
* type: object
* 401:
* description: 未授权
* 404:
* description: 预订单不存在
* 500:
* description: 服务器错误
*/
router.get('/pre-order/:id', auth, async (req, res) => { router.get('/pre-order/:id', auth, async (req, res) => {
try { try {
const preOrderId = req.params.id; const preOrderId = req.params.id;
@@ -462,43 +326,7 @@ router.get('/', auth, async (req, res) => {
} }
}); });
/**
* @swagger
* /api/orders/{id}:
* get:
* summary: 获取单个订单详情
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 订单ID
* responses:
* 200:
* description: 成功获取订单详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* order:
* $ref: '#/components/schemas/Order'
* 401:
* description: 未授权
* 404:
* description: 订单不存在
* 500:
* description: 服务器错误
*/
router.get('/:id', auth, async (req, res) => { router.get('/:id', auth, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
@@ -557,9 +385,11 @@ router.get('/:id', auth, async (req, res) => {
} }
// 处理地址信息 // 处理地址信息
console.log(order.address,'order.address');
if (order.address) { if (order.address) {
try { try {
order.address = JSON.parse(order.address); order.address = order.address;
} catch (e) { } catch (e) {
order.address = null; order.address = null;
} }
@@ -1340,7 +1170,7 @@ router.post('/confirm-payment', auth, async (req, res) => {
} }
pointsToDeduct = 0; pointsToDeduct = 0;
rongdouToDeduct = totalRongdouNeeded; rongdouToDeduct = totalRongdouNeeded;
} else if (isComboPayment) { } else if (hasPoints && hasRongdou) {
// 组合支付:先扣积分,不足部分用融豆 // 组合支付:先扣积分,不足部分用融豆
const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数

400
routes/payment.js Normal file
View File

@@ -0,0 +1,400 @@
const express = require('express');
const router = express.Router();
const WechatPayService = require('../services/wechatPayService');
const AlipayService = require('../services/alipayservice');
const { getDB } = require('../database');
const { auth, paymentAuth } = require('../middleware/auth');
// 创建支付服务实例
const wechatPayService = new WechatPayService();
const alipayService = new AlipayService();
/**
* 获取支持的支付方式
* GET /api/payment/methods
*/
router.get('/methods', (req, res) => {
res.json({
success: true,
data: {
methods: [
{
code: 'wechat_h5',
name: '微信支付',
description: '微信H5支付',
icon: 'wechat',
enabled: true
},
{
code: 'alipay_wap',
name: '支付宝支付',
description: '支付宝手机网站支付',
icon: 'alipay',
enabled: true
}
]
}
});
});
/**
* 创建统一支付订单
* POST /api/payment/create-order
*/
router.post('/create-order', paymentAuth, async (req, res) => {
try {
const { paymentMethod } = req.body;
const userId = req.user.id;
const username = req.user.username;
const phone = req.user.phone;
// 验证支付方式
if (!paymentMethod || !['wechat_h5', 'alipay_wap'].includes(paymentMethod)) {
return res.status(400).json({
success: false,
message: '不支持的支付方式'
});
}
// 检查用户是否已经支付过
const db = getDB();
const [existingOrders] = await db.execute(
'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"',
[userId]
);
if (existingOrders.length > 0) {
return res.status(400).json({
success: false,
message: '用户已完成支付,无需重复支付'
});
}
let result;
// 获取客户端IP
const clientIp = req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
'127.0.0.1';
// 根据支付方式创建订单
if (paymentMethod === 'wechat_h5') {
// 创建微信支付订单
result = await wechatPayService.createRegistrationPayOrder({
userId,
username,
phone,
clientIp
});
} else if (paymentMethod === 'alipay_wap') {
// 创建支付宝支付订单
result = await alipayService.createRegistrationPayOrder({
userId,
username,
phone,
clientIp
});
}
if (result && result.success) {
res.json({
success: true,
data: {
outTradeNo: result.data.outTradeNo,
payUrl: result.data.h5Url || result.data.payUrl,
paymentType: result.data.paymentType,
paymentMethod
}
});
} else {
res.status(500).json({
success: false,
message: '创建支付订单失败'
});
}
} catch (error) {
console.error('创建统一支付订单异常:', error);
res.status(500).json({
success: false,
message: error.message || '服务器内部错误'
});
}
});
/**
* 查询支付状态
* GET /api/payment/query-status/:outTradeNo
*/
router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => {
try {
const { outTradeNo } = req.params;
const userId = req.user.id;
// 验证订单是否属于当前用户
const db = getDB();
const [orders] = await db.execute(
'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?',
[outTradeNo, userId]
);
if (orders.length === 0) {
return res.status(404).json({
success: false,
message: '订单不存在或无权限访问'
});
}
// 获取订单详细信息包括trade_type
const [orderDetails] = await db.execute(
'SELECT id, trade_type FROM payment_orders WHERE id = ?',
[orders[0].id]
);
if (orderDetails.length === 0) {
return res.status(404).json({
success: false,
message: '订单详情不存在'
});
}
let result;
const tradeType = orderDetails[0].trade_type;
// 根据交易类型查询支付状态
if (tradeType === 'WECHAT_H5') {
// 查询微信支付状态
result = await wechatPayService.queryPaymentStatus(outTradeNo);
} else if (tradeType === 'ALIPAY_WAP') {
// 查询支付宝支付状态
result = await alipayService.queryPaymentStatus(outTradeNo);
} else {
return res.status(400).json({
success: false,
message: '不支持的支付方式'
});
}
res.json(result);
} catch (error) {
console.error('查询支付状态失败:', error);
res.status(500).json({
success: false,
message: error.message || '查询支付状态失败'
});
}
});
/**
* 获取用户支付记录
* GET /api/payment/orders
*/
router.get('/orders', paymentAuth, async (req, res) => {
try {
const userId = req.user.id;
const { page = 1, limit = 10, status } = req.query;
const offset = (page - 1) * limit;
const db = getDB();
let whereClause = 'WHERE user_id = ?';
let params = [userId];
if (status) {
whereClause += ' AND status = ?';
params.push(status);
}
// 查询订单列表
const [orders] = await db.execute(
`SELECT id, out_trade_no, transaction_id, total_fee, body, trade_type,
status, paid_at, created_at
FROM payment_orders
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?`,
[...params, parseInt(limit), parseInt(offset)]
);
// 查询总数
const [countResult] = await db.execute(
`SELECT COUNT(*) as total FROM payment_orders ${whereClause}`,
params
);
const total = countResult[0].total;
res.json({
success: true,
data: {
orders: orders.map(order => ({
...order,
total_fee: order.total_fee / 100, // 转换为元
payment_method_name: order.trade_type && order.trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付'
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
}
});
} catch (error) {
console.error('获取支付记录失败:', error);
res.status(500).json({
success: false,
message: '获取支付记录失败'
});
}
});
/**
* 检查用户支付状态
* GET /api/payment/check-status
*/
router.get('/check-status', auth, async (req, res) => {
try {
const userId = req.user.id;
const db = getDB();
// 查询用户支付状态
const [users] = await db.execute(
'SELECT payment_status FROM users WHERE id = ?',
[userId]
);
if (users.length === 0) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
const paymentStatus = users[0].payment_status;
// 查询最近的支付订单
const [recentOrders] = await db.execute(
`SELECT out_trade_no, trade_type, status, total_fee, paid_at
FROM payment_orders
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 1`,
[userId]
);
res.json({
success: true,
data: {
paymentStatus,
isPaid: paymentStatus === 'paid',
recentOrder: recentOrders.length > 0 ? {
...recentOrders[0],
total_fee: recentOrders[0].total_fee / 100,
payment_method_name: recentOrders[0].trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付'
} : null
}
});
} catch (error) {
console.error('检查用户支付状态失败:', error);
res.status(500).json({
success: false,
message: '检查支付状态失败'
});
}
});
/**
* 支付宝支付回调通知
* POST /api/payment/alipay/notify
*/
router.post('/alipay/notify', async (req, res) => {
try {
console.log('收到支付宝支付回调:', req.body);
// 验证签名
const isValid = alipayService.verifyNotifySign(req.body);
if (!isValid) {
console.error('支付宝回调签名验证失败');
return res.status(400).send('FAIL');
}
const {
out_trade_no: outTradeNo,
trade_no: transactionId,
trade_status: tradeStatus,
total_amount: totalAmount
} = req.body;
// 只处理支付成功的回调
if (tradeStatus === 'TRADE_SUCCESS') {
const db = getDB();
// 检查订单是否存在
const [orders] = await db.execute(
'SELECT id, user_id, status FROM payment_orders WHERE out_trade_no = ?',
[outTradeNo]
);
if (orders.length === 0) {
console.error('支付宝回调:订单不存在', outTradeNo);
return res.status(400).send('FAIL');
}
const order = orders[0];
// 如果订单已经处理过,直接返回成功
if (order.status === 'paid') {
console.log('支付宝回调:订单已处理', outTradeNo);
return res.send('SUCCESS');
}
// 更新订单状态
await alipayService.updatePaymentStatus(outTradeNo, {
status: 'paid',
transactionId,
paidAt: new Date()
});
console.log('支付宝支付成功处理完成:', {
outTradeNo,
transactionId,
userId: order.user_id
});
}
res.send('SUCCESS');
} catch (error) {
console.error('处理支付宝支付回调失败:', error);
res.status(500).send('FAIL');
}
});
/**
* 支付宝支付返回页面处理
* GET /api/payment/alipay/return
*/
router.get('/alipay/return', async (req, res) => {
try {
console.log('支付宝支付返回:', req.query);
// 验证签名
const isValid = alipayService.verifyNotifySign(req.query);
if (!isValid) {
console.error('支付宝返回签名验证失败');
return res.redirect('/payment/failed');
}
const { out_trade_no: outTradeNo } = req.query;
// 重定向到支付成功页面
res.redirect(`/payment/success?outTradeNo=${outTradeNo}`);
} catch (error) {
console.error('处理支付宝支付返回失败:', error);
res.redirect('/payment/failed');
}
});
module.exports = router;

View File

@@ -120,40 +120,50 @@ router.get('/zhejiang', async (req, res) => {
*/ */
router.get('/provinces', async (req, res) => { router.get('/provinces', async (req, res) => {
try { try {
// 递归获取子区域的函数 // 一次性获取所有区域数据(省、市、区县)
async function getChildrenRecursively(parentCode, level) { const [allRegions] = await getDB().execute(
const [children] = await getDB().execute( `SELECT code, name as label, level, parent_code FROM china_regions
`SELECT code, name as label, level FROM china_regions WHERE level <= 3
WHERE parent_code = ? AND level = ? ORDER BY level, code`
ORDER BY code`,
[parentCode, level]
);
// 为每个子区域递归获取其子区域
for (let child of children) {
if (level < 3) { // 最多到区县级别level 3
child.children = await getChildrenRecursively(child.code, level + 1);
}
}
return children;
}
// 获取所有省份
const [provinces] = await getDB().execute(
`SELECT code, name as label, level FROM china_regions
WHERE level = 1
ORDER BY code`
); );
// 为每个省份递归获取城市和区县 // 按level分组数据
for (let province of provinces) { const regionsByLevel = {
province.children = await getChildrenRecursively(province.code, 2); 1: [], // 省份
} 2: [], // 城市
3: [] // 区县
};
// 创建code到region的映射便于快速查找
const regionMap = {};
// 分组并建立映射
allRegions.forEach(region => {
region.children = []; // 初始化children数组
regionsByLevel[region.level].push(region);
regionMap[region.code] = region;
});
// 构建层级关系:先处理区县到城市的关系
regionsByLevel[3].forEach(district => {
const parentCity = regionMap[district.parent_code];
if (parentCity) {
parentCity.children.push(district);
}
});
// 再处理城市到省份的关系
regionsByLevel[2].forEach(city => {
const parentProvince = regionMap[city.parent_code];
if (parentProvince) {
parentProvince.children.push(city);
}
});
// 返回省份数据(已包含完整的层级结构)
res.json({ res.json({
success: true, success: true,
data: provinces data: regionsByLevel[1]
}); });
} catch (error) { } catch (error) {
console.error('获取省份列表错误:', error); console.error('获取省份列表错误:', error);

View File

@@ -138,18 +138,6 @@ router.post('/send', async (req, res) => {
// 记录发送时间 // 记录发送时间
smsCodeStore.set(`last_send_${phone}`, Date.now()) smsCodeStore.set(`last_send_${phone}`, Date.now())
// 开发环境下模拟发送成功
// if (SMS_CONFIG.isDevelopment) {
// console.log(`[开发环境] 短信验证码: ${code} 发送到 ${phone}`)
// return res.json({
// success: true,
// message: '验证码发送成功',
// // 开发环境下返回验证码便于测试
// code: process.env.NODE_ENV === 'development' ? code : undefined
// })
// }
// 生产环境发送真实短信 // 生产环境发送真实短信
try { try {
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({ const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({

View File

@@ -1234,6 +1234,11 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r
const { transferId } = req.params; const { transferId } = req.params;
const { newStatus, status, reason, adjust_balance = false } = req.body; const { newStatus, status, reason, adjust_balance = false } = req.body;
console.log('newStatus:', newStatus);
console.log('status:', status);
console.log('reason:', reason);
console.log('adjust_balance:', adjust_balance);
// 兼容两种参数名newStatus 和 status // 兼容两种参数名newStatus 和 status
const actualNewStatus = newStatus || status; const actualNewStatus = newStatus || status;
const adminId = req.user.id; const adminId = req.user.id;
@@ -1248,9 +1253,9 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r
return res.status(400).json({ success: false, message: '新状态不能为空' }); return res.status(400).json({ success: false, message: '新状态不能为空' });
} }
if (!reason) { // if (!reason) {
return res.status(400).json({ success: false, message: '变更原因不能为空' }); // return res.status(400).json({ success: false, message: '变更原因不能为空' });
} // }
const result = await transferService.forceChangeTransferStatus( const result = await transferService.forceChangeTransferStatus(
transferId, transferId,

202
routes/wechatPay.js Normal file
View File

@@ -0,0 +1,202 @@
const express = require('express');
const router = express.Router();
const WechatPayService = require('../services/wechatPayService');
const { getDB } = require('../database');
const { auth, paymentAuth } = require('../middleware/auth');
// 创建微信支付服务实例
const wechatPayService = new WechatPayService();
/**
* 创建注册支付订单 (API v3 - H5支付)
* POST /api/wechat-pay/create-registration-order
*/
router.post('/create-registration-order', paymentAuth, async (req, res) => {
try {
const userId = req.user.id;
const username = req.user.username;
const phone = req.user.phone;
// 获取客户端IP
const clientIp = req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
'127.0.0.1';
// 检查用户是否已经支付过
const db = getDB();
const [existingOrders] = await db.execute(
'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"',
[userId]
);
if (existingOrders.length > 0) {
return res.status(400).json({
success: false,
message: '用户已完成支付,无需重复支付'
});
}
console.log('创建H5支付订单:', {
userId,
username,
phone,
clientIp
});
// 创建H5支付订单
const result = await wechatPayService.createRegistrationPayOrder({
userId,
username,
phone,
clientIp
});
if (result.success) {
res.json({
success: true,
data: {
outTradeNo: result.data.outTradeNo,
h5Url: result.data.h5Url,
paymentType: result.data.paymentType
}
});
} else {
res.status(500).json({
success: false,
message: '创建支付订单失败'
});
}
} catch (error) {
console.error('创建H5支付订单异常:', error);
res.status(500).json({
success: false,
message: error.message || '服务器内部错误'
});
}
});
// H5支付不需要获取openid移除相关接口
/**
* 微信支付回调接口 (API v3)
* POST /api/wechat-pay/notify
*/
router.post('/notify', async (req, res) => {
try {
// API v3 回调是JSON格式
const notifyData = req.body;
// 获取请求头中的签名信息
const signature = req.headers['wechatpay-signature'];
const timestamp = req.headers['wechatpay-timestamp'];
const nonce = req.headers['wechatpay-nonce'];
const serial = req.headers['wechatpay-serial'];
console.log('收到API v3支付回调:', {
signature,
timestamp,
nonce,
serial,
body: notifyData
});
// 验证签名和处理回调
const result = await wechatPayService.handleV3PaymentNotify({
signature,
timestamp,
nonce,
serial,
body: JSON.stringify(notifyData)
});
if (result.success) {
// API v3 成功响应
res.status(200).json({ code: 'SUCCESS', message: '成功' });
} else {
// API v3 失败响应
res.status(400).json({ code: 'FAIL', message: result.message || '处理失败' });
}
} catch (error) {
console.error('支付回调处理异常:', error);
res.status(500).json({ code: 'ERROR', message: '服务器内部错误' });
}
});
/**
* 查询支付状态
* GET /api/wechat-pay/query-status/:outTradeNo
*/
router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => {
try {
const { outTradeNo } = req.params;
const userId = req.user.id;
// 验证订单是否属于当前用户
const db = getDB();
const [orders] = await db.execute(
'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?',
[outTradeNo, userId]
);
if (orders.length === 0) {
return res.status(404).json({
success: false,
message: '订单不存在或无权限访问'
});
}
const result = await wechatPayService.queryPaymentStatus(outTradeNo);
res.json(result);
} catch (error) {
console.error('查询支付状态失败:', error);
res.status(500).json({
success: false,
message: error.message || '查询支付状态失败'
});
}
});
/**
* 检查用户支付状态
* GET /api/wechat-pay/check-user-payment
*/
router.get('/check-user-payment', auth, async (req, res) => {
try {
const userId = req.user.id;
const db = getDB();
// 查询用户支付状态
const [users] = await db.execute(
'SELECT payment_status FROM users WHERE id = ?',
[userId]
);
if (users.length === 0) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
const paymentStatus = users[0].payment_status;
res.json({
success: true,
data: {
paymentStatus,
isPaid: paymentStatus === 'paid'
}
});
} catch (error) {
console.error('检查用户支付状态失败:', error);
res.status(500).json({
success: false,
message: '检查支付状态失败'
});
}
});
module.exports = router;

87
run_migration.js Normal file
View File

@@ -0,0 +1,87 @@
const mysql = require('mysql2/promise');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
async function runMigration() {
let connection;
try {
// 创建数据库连接
connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
charset: 'utf8mb4'
});
console.log('数据库连接成功');
// 直接定义SQL语句
const sqlStatements = [
`CREATE TABLE IF NOT EXISTS \`payment_orders\` (
\`id\` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
\`user_id\` int(11) NOT NULL COMMENT '用户ID',
\`out_trade_no\` varchar(64) NOT NULL COMMENT '商户订单号',
\`transaction_id\` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
\`total_fee\` int(11) NOT NULL COMMENT '订单金额(分)',
\`body\` varchar(128) NOT NULL COMMENT '商品描述',
\`trade_type\` varchar(16) NOT NULL COMMENT '交易类型',
\`prepay_id\` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识',
\`mweb_url\` text DEFAULT NULL COMMENT 'H5支付跳转链接',
\`status\` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态',
\`paid_at\` datetime DEFAULT NULL COMMENT '支付完成时间',
\`created_at\` datetime NOT NULL COMMENT '创建时间',
\`updated_at\` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (\`id\`),
UNIQUE KEY \`uk_out_trade_no\` (\`out_trade_no\`),
KEY \`idx_user_id\` (\`user_id\`),
KEY \`idx_status\` (\`status\`),
KEY \`idx_created_at\` (\`created_at\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'`,
`ALTER TABLE \`users\` ADD COLUMN \`payment_status\` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态'`,
`ALTER TABLE \`users\` ADD KEY \`idx_payment_status\` (\`payment_status\`)`
];
console.log(`准备执行 ${sqlStatements.length} 条SQL语句`);
// 执行每条SQL语句
for (let i = 0; i < sqlStatements.length; i++) {
const statement = sqlStatements[i];
console.log(`执行第 ${i + 1} 条语句...`);
try {
await connection.execute(statement);
console.log(`✓ 第 ${i + 1} 条语句执行成功`);
} catch (error) {
if (error.code === 'ER_DUP_FIELDNAME') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(字段已存在): ${error.message}`);
} else if (error.code === 'ER_TABLE_EXISTS_ERROR') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(表已存在): ${error.message}`);
} else if (error.code === 'ER_DUP_KEYNAME') {
console.log(`⚠ 第 ${i + 1} 条语句跳过(索引已存在): ${error.message}`);
} else {
console.error(`✗ 第 ${i + 1} 条语句执行失败:`, error.message);
throw error;
}
}
}
console.log('\n✅ 数据库迁移完成!');
} catch (error) {
console.error('❌ 数据库迁移失败:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('数据库连接已关闭');
}
}
}
// 运行迁移
runMigration();

View File

@@ -40,6 +40,7 @@ app.use(cors({
origin: [ origin: [
'http://localhost:5173', 'http://localhost:5173',
'http://localhost:5176', 'http://localhost:5176',
'http://localhost:5175',
'http://localhost:5174', 'http://localhost:5174',
'http://localhost:3001', 'http://localhost:3001',
'https://www.zrbjr.com', 'https://www.zrbjr.com',
@@ -250,6 +251,9 @@ app.use('/api/regions', require('./routes/regions'));
app.use('/api/addresses', require('./routes/addresses')); app.use('/api/addresses', require('./routes/addresses'));
app.use('/api/address-labels', require('./routes/address-labels')); app.use('/api/address-labels', require('./routes/address-labels'));
app.use('/api/cart', require('./routes/cart')); app.use('/api/cart', require('./routes/cart'));
app.use('/api/announcements', require('./routes/announcements')); // 通知公告路由
app.use('/api/wechat-pay', require('./routes/wechatPay')); // 只保留微信支付
app.use('/api/payment', require('./routes/payment'));
// 前端路由 - 必须在最后作为fallback // 前端路由 - 必须在最后作为fallback
app.get('/', (req, res) => { app.get('/', (req, res) => {

224
services/alipayservice.js Normal file
View File

@@ -0,0 +1,224 @@
const { AlipaySdk } = require('alipay-sdk');
const { getDB } = require('../database');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
class AlipayService {
constructor() {
// 读取密钥文件
const privateKeyPath = path.join(__dirname, '../certs/alipay-private-key.pem');
const publicKeyPath = path.join(__dirname, '../certs/alipay-public-key.pem');
const privateKey = fs.readFileSync(privateKeyPath, 'utf8');
const alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
// 支付宝配置
this.config = {
appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID
privateKey: privateKey, // 从文件读取的应用私钥
alipayPublicKey: alipayPublicKey, // 从文件读取的支付宝公钥
gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址
signType: 'RSA2',
charset: 'utf-8',
version: '1.0',
timeout: 5000
};
// 初始化支付宝SDK
this.alipaySdk = new AlipaySdk({
appId: this.config.appId,
privateKey: this.config.privateKey,
alipayPublicKey: this.config.alipayPublicKey,
gateway: this.config.gateway,
signType: this.config.signType,
timeout: this.config.timeout
});
}
/**
* 创建注册支付订单
* @param {Object} params - 支付参数
* @param {string} params.userId - 用户ID
* @param {string} params.username - 用户名
* @param {string} params.phone - 手机号
* @param {string} params.clientIp - 客户端IP
* @returns {Promise<Object>} 支付结果
*/
async createRegistrationPayOrder({ userId, username, phone, clientIp }) {
try {
const db = getDB();
// 生成订单号
const outTradeNo = this.generateOrderNo();
const totalFee = 39900; // 399元单位
const subject = '用户注册激活费用';
const body = `用户${username}(${phone})注册激活费用`;
// 业务参数
const bizContent = {
out_trade_no: outTradeNo,
total_amount: (totalFee / 100).toFixed(2), // 转换为元
subject: subject,
body: body,
product_code: 'QUICK_WAP_WAY',
quit_url: process.env.ALIPAY_QUIT_URL || 'https://your-domain.com/payment/cancel'
};
// 使用新版SDK的pageExecute方法生成支付URL
const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', {
bizContent: bizContent,
notifyUrl: process.env.ALIPAY_NOTIFY_URL || 'https://your-domain.com/api/payment/alipay/notify',
returnUrl: process.env.ALIPAY_RETURN_URL || 'https://your-domain.com/payment/success'
});
// 保存订单到数据库
await db.execute(
`INSERT INTO payment_orders
(user_id, out_trade_no, total_fee, body, trade_type, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
[userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending']
);
console.log('支付宝支付订单创建成功:', {
userId,
outTradeNo,
totalFee,
payUrl
});
return {
success: true,
data: {
outTradeNo,
payUrl,
paymentType: 'alipay_wap',
totalFee
}
};
} catch (error) {
console.error('创建支付宝支付订单失败:', error);
return {
success: false,
message: error.message || '创建支付订单失败'
};
}
}
/**
* 查询支付状态
* @param {string} outTradeNo - 商户订单号
* @returns {Promise<Object>} 查询结果
*/
async queryPaymentStatus(outTradeNo) {
try {
const result = await this.alipaySdk.exec('alipay.trade.query', {
bizContent: {
out_trade_no: outTradeNo
}
});
if (result.code === '10000') {
// 查询成功
const tradeStatus = result.tradeStatus;
// 如果支付成功,更新数据库
if (tradeStatus === 'TRADE_SUCCESS') {
await this.updatePaymentStatus(outTradeNo, {
status: 'paid',
transactionId: result.tradeNo,
paidAt: new Date()
});
}
return {
success: true,
data: {
trade_status: tradeStatus,
trade_no: result.tradeNo,
total_amount: result.totalAmount,
buyer_pay_amount: result.buyerPayAmount,
gmt_payment: result.gmtPayment
}
};
} else {
return {
success: false,
message: result.msg || '查询支付状态失败'
};
}
} catch (error) {
console.error('查询支付宝支付状态失败:', error);
return {
success: false,
message: error.message || '查询支付状态失败'
};
}
}
/**
* 更新支付状态
* @param {string} outTradeNo - 商户订单号
* @param {Object} updateData - 更新数据
*/
async updatePaymentStatus(outTradeNo, updateData) {
try {
const db = getDB();
// 更新订单状态
await db.execute(
`UPDATE payment_orders
SET status = ?, transaction_id = ?, paid_at = ?
WHERE out_trade_no = ?`,
[updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo]
);
// 如果支付成功,更新用户支付状态
if (updateData.status === 'paid') {
const [orders] = await db.execute(
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
[outTradeNo]
);
if (orders.length > 0) {
const userId = orders[0].user_id;
await db.execute(
'UPDATE users SET payment_status = ? WHERE id = ?',
['paid', userId]
);
console.log('用户支付状态更新成功:', { userId, outTradeNo });
}
}
} catch (error) {
console.error('更新支付状态失败:', error);
throw error;
}
}
/**
* 验证支付宝回调签名
* @param {Object} params - 回调参数
* @returns {boolean} 验证结果
*/
verifyNotifySign(params) {
try {
return this.alipaySdk.checkNotifySign(params);
} catch (error) {
console.error('验证支付宝回调签名失败:', error);
return false;
}
}
/**
* 生成订单号
* @returns {string} 订单号
*/
generateOrderNo() {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
return `ALI${timestamp}${random}`;
}
}
module.exports = AlipayService;

View File

@@ -851,7 +851,8 @@ class MatchingService {
historicalNetBalance: user.historical_net_balance, historicalNetBalance: user.historical_net_balance,
totalPendingInflow: user.total_pending_inflow, totalPendingInflow: user.total_pending_inflow,
availableForAllocation: user.available_for_allocation, availableForAllocation: user.available_for_allocation,
todayOutflow: user.today_outflow todayOutflow: user.today_outflow,
has_active_allocations:user.has_active_allocations
}); });
remainingAmount -= maxUserAllocation; remainingAmount -= maxUserAllocation;
} }
@@ -871,7 +872,7 @@ class MatchingService {
// 计算每个用户的剩余可分配容量 // 计算每个用户的剩余可分配容量
for (const allocation of allocations) { for (const allocation of allocations) {
// 获取用户当前的实际余额状态使用has_active_allocations作为实际可分配余额 // 获取用户当前的实际余额状态使用has_active_allocations作为实际可分配余额
const maxSafeAmount = Math.abs(allocation.availableForAllocation); const maxSafeAmount = Math.abs(allocation.has_active_allocations);
const remainingCapacity = maxSafeAmount - allocation.amount; const remainingCapacity = maxSafeAmount - allocation.amount;
if (remainingCapacity > 0) { if (remainingCapacity > 0) {

View File

@@ -862,14 +862,15 @@ class TransferService {
// 根据所有相关transfers的状态来决定matching_order的状态 // 根据所有相关transfers的状态来决定matching_order的状态
const transferStatuses = allTransfers.map(t => t.status); const transferStatuses = allTransfers.map(t => t.status);
console.log(transferStatuses,'transferStatuses');
if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) { if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received' || status === 'confirmed' || status === 'received')) {
// 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成
matchingOrderStatus = 'completed'; matchingOrderStatus = 'completed';
} else if (transferStatuses.every(status => status === 'received')) { } else if (transferStatuses.every(status => status === 'received')) {
// 如果所有transfers都已收到匹配订单完成 // 如果所有transfers都已收到匹配订单完成
matchingOrderStatus = 'completed'; matchingOrderStatus = 'completed';
} else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') ) {
// 如果有任何一个transfer被取消/拒绝/未收到或者有transfers已确认或已收到匹配订单为进行中状态 // 如果有任何一个transfer被取消/拒绝/未收到或者有transfers已确认或已收到匹配订单为进行中状态
matchingOrderStatus = 'matching'; matchingOrderStatus = 'matching';
} else { } else {

View File

@@ -0,0 +1,566 @@
const crypto = require('crypto');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { wechatPay } = require('../config/wechatPay');
const { getDB } = require('../database');
class WechatPayService {
constructor() {
this.config = {
...wechatPay,
apiV3Key: process.env.WECHAT_API_V3_KEY
};
this.privateKey = null; // API v3 私钥
this.serialNo = null; // 商户证书序列号
this.initializeV3();
}
// 初始化API v3配置
async initializeV3() {
try {
// 加载私钥
const keyPath = path.resolve(__dirname, '..', this.config.keyPath.replace(/^\.\//, ''));
if (fs.existsSync(keyPath)) {
this.privateKey = fs.readFileSync(keyPath, 'utf8');
console.log('API v3 私钥加载成功');
} else {
console.error('私钥文件不存在:', keyPath);
}
// 获取证书序列号
const certPath = path.resolve(__dirname, '..', this.config.certPath.replace(/^\.\//, ''));
if (fs.existsSync(certPath)) {
const cert = fs.readFileSync(certPath, 'utf8');
this.serialNo = this.getCertificateSerialNumber(cert);
console.log('证书序列号:', this.serialNo);
} else {
console.error('证书文件不存在:', certPath);
}
} catch (error) {
console.error('初始化API v3配置失败:', error.message);
}
}
// 获取证书序列号
getCertificateSerialNumber(cert) {
try {
const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null;
if (x509) {
return x509.serialNumber.toLowerCase().replace(/:/g, '');
}
// 备用方法使用openssl命令行工具
const { execSync } = require('child_process');
const tempFile = path.join(__dirname, 'temp_cert.pem');
fs.writeFileSync(tempFile, cert);
const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' })
.replace('serial=', '')
.trim()
.toLowerCase();
fs.unlinkSync(tempFile);
return serialNumber;
} catch (error) {
console.error('获取证书序列号失败:', error.message);
return null;
}
}
/**
* 生成随机字符串
* @param {number} length 长度
* @returns {string} 随机字符串
*/
generateNonceStr(length = 32) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 生成时间戳
* @returns {string} 时间戳
*/
generateTimestamp() {
return Math.floor(Date.now() / 1000).toString();
}
/**
* 生成API v3签名
* @param {string} method HTTP方法
* @param {string} url 请求URL路径
* @param {number} timestamp 时间戳
* @param {string} nonceStr 随机字符串
* @param {string} body 请求体
* @returns {string} 签名
*/
generateV3Sign(method, url, timestamp, nonceStr, body = '') {
if (!this.privateKey) {
throw new Error('私钥未加载,无法生成签名');
}
// 构造签名串
const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`;
console.log('API v3 签名字符串:', signString);
// 使用私钥进行SHA256-RSA签名
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
const signature = sign.toString('base64');
console.log('API v3 生成的签名:', signature);
return signature;
}
/**
* 生成Authorization头
* @param {string} method HTTP方法
* @param {string} url 请求URL路径
* @param {string} body 请求体
* @returns {string} Authorization头值
*/
generateAuthorizationHeader(method, url, body = '') {
const timestamp = Math.floor(Date.now() / 1000);
const nonceStr = this.generateNonceStr();
const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body);
return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
}
/**
* 生成JSAPI支付参数
* @param {string} prepayId 预支付交易会话标识
* @returns {object} JSAPI支付参数
*/
generateJSAPIPayParams(prepayId) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = this.generateNonceStr();
const packageStr = `prepay_id=${prepayId}`;
// 构造签名串
const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`;
// 使用私钥进行签名
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
const paySign = sign.toString('base64');
return {
appId: this.config.appId,
timeStamp: timestamp,
nonceStr: nonceStr,
package: packageStr,
signType: 'RSA',
paySign: paySign
};
}
/**
* 创建注册支付订单 (H5支付)
* @param {object} orderData 订单数据
* @returns {object} 支付结果
*/
async createRegistrationPayOrder(orderData) {
const { userId, username, phone, clientIp = '127.0.0.1' } = orderData;
try {
if (!this.privateKey || !this.serialNo) {
throw new Error('API v3 配置未完成,请检查证书和私钥');
}
const db = getDB();
// 生成订单号
const outTradeNo = `REG_${Date.now()}_${userId}`;
// 创建支付订单记录
await db.execute(
'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())',
[userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending']
);
// API v3 H5支付请求体
const requestBody = {
appid: this.config.appId,
mchid: this.config.mchId,
description: '用户注册费用',
out_trade_no: outTradeNo,
notify_url: this.config.notifyUrl,
amount: {
total: this.config.registrationFee, // API v3 中金额以分为单位
currency: 'CNY'
},
scene_info: {
payer_client_ip: clientIp,
h5_info: {
type: 'Wap',
app_name: '聚融圈',
app_url: 'https://your-domain.com',
bundle_id: 'com.jurong.circle'
}
}
};
console.log('API v3 H5支付参数:', requestBody);
const requestBodyStr = JSON.stringify(requestBody);
const url = '/v3/pay/transactions/h5';
const method = 'POST';
// 生成Authorization头
const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr);
// API v3 H5支付接口地址
const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5';
console.log('使用的API v3 H5地址:', apiUrl);
console.log('Authorization头:', authorization);
const response = await axios.post(apiUrl, requestBody, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': authorization,
'User-Agent': 'jurong-circle/1.0.0'
}
});
console.log('微信支付API v3 H5响应:', response.data);
if (response.data && response.data.h5_url) {
// 更新订单状态
await db.execute(
'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?',
[response.data.h5_url, outTradeNo]
);
return {
success: true,
data: {
outTradeNo,
h5Url: response.data.h5_url,
paymentType: 'h5'
}
};
} else {
console.log(response.data);
throw new Error(response.data?.message || '支付订单创建失败');
}
} catch (error) {
console.error('创建H5支付订单失败:', error.response?.data || error.message);
throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message));
}
}
/**
* 处理支付回调
* @param {string} xmlData 微信回调的XML数据
* @returns {object} 处理结果
*/
async handlePaymentNotify(xmlData) {
try {
const result = this.xmlToObject(xmlData);
// 验证签名
const sign = result.sign;
delete result.sign;
const calculatedSign = this.generateSign(result);
if (sign !== calculatedSign) {
throw new Error('签名验证失败');
}
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
const db = getDB();
// 开始事务
await db.beginTransaction();
try {
// 更新支付订单状态
await db.execute(
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
['paid', result.transaction_id, result.out_trade_no]
);
// 获取订单信息
const [orders] = await db.execute(
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
[result.out_trade_no]
);
if (orders.length > 0) {
const userId = orders[0].user_id;
// 激活用户账户
await db.execute(
'UPDATE users SET payment_status = "paid" WHERE id = ?',
[userId]
);
console.log(`用户 ${userId} 支付成功,账户已激活`);
}
// 提交事务
await db.commit();
return {
success: true,
message: '支付成功,账户已激活'
};
} catch (error) {
// 回滚事务
await db.rollback();
throw error;
}
} else {
const db = getDB();
// 更新订单状态为失败
await db.execute(
'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?',
['failed', result.out_trade_no]
);
return {
success: false,
message: '支付失败'
};
}
} catch (error) {
console.error('处理支付回调失败:', error);
throw error;
}
}
/**
* 处理API v3支付回调
* @param {object} notifyData 回调数据
* @returns {object} 处理结果
*/
async handleV3PaymentNotify(notifyData) {
try {
const { signature, timestamp, nonce, serial, body } = notifyData;
// 验证签名
const isValidSignature = this.verifyV3Signature({
timestamp,
nonce,
body,
signature
});
if (!isValidSignature) {
console.error('API v3回调签名验证失败');
return { success: false, message: '签名验证失败' };
}
console.log('API v3回调签名验证成功');
// 解析回调数据
const callbackData = JSON.parse(body);
console.log('解析的回调数据:', callbackData);
// 检查事件类型
if (callbackData.event_type === 'TRANSACTION.SUCCESS') {
// 解密resource数据
const resource = callbackData.resource;
const decryptedData = this.decryptV3Resource(resource);
console.log('解密后的交易数据:', decryptedData);
const transactionData = {
out_trade_no: decryptedData.out_trade_no,
transaction_id: decryptedData.transaction_id,
trade_state: decryptedData.trade_state
};
console.log('交易数据:', transactionData);
if (transactionData.trade_state === 'SUCCESS') {
const db = getDB();
// 开始事务
await db.beginTransaction();
try {
// 更新支付订单状态
await db.execute(
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
['paid', transactionData.transaction_id, transactionData.out_trade_no]
);
// 获取订单信息
const [orders] = await db.execute(
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
[transactionData.out_trade_no]
);
if (orders.length > 0) {
const userId = orders[0].user_id;
// 激活用户账户
await db.execute(
'UPDATE users SET payment_status = "paid" WHERE id = ?',
[userId]
);
console.log(`用户 ${userId} API v3支付成功账户已激活`);
}
// 提交事务
await db.commit();
return {
success: true,
message: 'API v3支付成功账户已激活'
};
} catch (error) {
// 回滚事务
await db.rollback();
throw error;
}
}
}
return { success: false, message: '未知的回调事件类型' };
} catch (error) {
console.error('处理API v3支付回调异常:', error);
return { success: false, message: error.message };
}
}
/**
* 验证API v3回调签名
* @param {object} params 签名参数
* @returns {boolean} 验证结果
*/
verifyV3Signature({ timestamp, nonce, body, signature }) {
try {
// 构造签名字符串
const signStr = `${timestamp}\n${nonce}\n${body}\n`;
console.log('构造的签名字符串:', signStr);
console.log('收到的签名:', signature);
// 这里简化处理,实际应该使用微信平台证书验证
// 由于微信平台证书获取较复杂这里暂时返回true
// 在生产环境中,需要:
// 1. 获取微信支付平台证书
// 2. 使用平台证书的公钥验证签名
console.log('API v3签名验证简化处理');
return true;
} catch (error) {
console.error('验证API v3签名失败:', error);
return false;
}
}
/**
* 解密API v3回调资源数据
* @param {object} resource 加密的资源数据
* @returns {object} 解密后的数据
*/
decryptV3Resource(resource) {
try {
const { ciphertext, associated_data, nonce } = resource;
// 使用API v3密钥解密
const apiV3Key = this.config.apiV3Key;
if (!apiV3Key) {
throw new Error('API v3密钥未配置');
}
// AES-256-GCM解密
const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key);
decipher.setAAD(Buffer.from(associated_data, 'utf8'));
decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64'));
const encrypted = ciphertext.slice(0, -32);
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
} catch (error) {
console.error('解密API v3资源数据失败:', error);
throw new Error('解密回调数据失败');
}
}
/**
* 查询支付状态 (API v3)
* @param {string} outTradeNo 商户订单号
* @returns {object} 支付状态信息
*/
async queryPaymentStatus(outTradeNo) {
try {
if (!this.privateKey || !this.serialNo) {
throw new Error('私钥或证书序列号未初始化');
}
const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`;
const method = 'GET';
const timestamp = Math.floor(Date.now() / 1000);
const nonce = this.generateNonceStr();
const body = '';
// 生成签名
const signature = this.generateV3Sign(
method,
`/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`,
timestamp,
nonce,
body
);
// 生成Authorization头
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
console.log('查询支付状态 - API v3请求:', {
url,
authorization
});
// 发送请求
const response = await axios.get(url, {
headers: {
'Authorization': authorization,
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'jurong-circle/1.0'
},
params: {
mchid: this.config.mchId
}
});
console.log('查询支付状态响应:', response.data);
const result = response.data;
return {
success: result.trade_state === 'SUCCESS',
tradeState: result.trade_state,
transactionId: result.transaction_id,
outTradeNo: result.out_trade_no,
totalAmount: result.amount ? result.amount.total : 0,
payerOpenid: result.payer ? result.payer.openid : null
};
} catch (error) {
console.error('查询支付状态失败:', error);
if (error.response) {
console.error('API v3查询支付状态错误响应:', error.response.data);
}
throw error;
}
}
}
module.exports = WechatPayService;