增加微信支付,商城逻辑,公告
This commit is contained in:
20
.env
20
.env
@@ -43,4 +43,22 @@ MINIO_PUBLIC_URL=https://minio.zrbjr.com
|
||||
MINIO_BUCKET_UPLOADS=jurongquan
|
||||
MINIO_BUCKET_AVATARS=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
|
||||
17
.env.example
17
.env.example
@@ -41,3 +41,20 @@ MINIO_BUCKET_UPLOADS=jurongquan
|
||||
MINIO_BUCKET_AVATARS=jurongquan
|
||||
MINIO_BUCKET_PRODUCTS=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
BIN
cert/apiclient_cert.p12
Normal file
Binary file not shown.
25
cert/apiclient_cert.pem
Normal file
25
cert/apiclient_cert.pem
Normal 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
28
cert/apiclient_key.pem
Normal 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-----
|
||||
18
cert/证书使用说明.txt
Normal file
18
cert/证书使用说明.txt
Normal 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
34
certs/README.md
Normal 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版本的接口。
|
||||
1
certs/alipay-private-key.pem
Normal file
1
certs/alipay-private-key.pem
Normal 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=
|
||||
9
certs/alipay-public-key.pem
Normal file
9
certs/alipay-public-key.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7
|
||||
TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR
|
||||
gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse
|
||||
bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5
|
||||
h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak
|
||||
UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl
|
||||
hwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
3
certs/alipayCertPublicKey_RSA2.crt
Normal file
3
certs/alipayCertPublicKey_RSA2.crt
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
3
certs/alipayPublicKey_RSA2.crt
Normal file
3
certs/alipayPublicKey_RSA2.crt
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
3
certs/appCertPublicKey.crt
Normal file
3
certs/appCertPublicKey.crt
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFRgwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPsebLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSakUzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNlhwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -574,7 +574,48 @@ async function createTables() {
|
||||
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
24
config/wechatPay.js
Normal 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元注册费
|
||||
}
|
||||
};
|
||||
@@ -11,10 +11,12 @@ docs/
|
||||
│ ├── product.js # 商品相关数据模型
|
||||
│ ├── order.js # 订单相关数据模型
|
||||
│ ├── user.js # 用户相关数据模型
|
||||
│ └── cart.js # 购物车相关数据模型
|
||||
│ ├── cart.js # 购物车相关数据模型
|
||||
│ └── announcement.js # 通知公告相关数据模型
|
||||
└── apis/ # API 接口定义
|
||||
├── products.js # 商品相关 API
|
||||
└── orders.js # 订单相关 API
|
||||
├── orders.js # 订单相关 API
|
||||
└── announcements.js # 通知公告相关 API
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
736
docs/apis/announcements.js
Normal file
736
docs/apis/announcements.js
Normal 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: 服务器错误
|
||||
*/
|
||||
@@ -7,149 +7,12 @@
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/create-from-cart:
|
||||
* 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}:
|
||||
* /api/orders:
|
||||
* get:
|
||||
* 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: 获取用户订单列表
|
||||
* summary: 获取订单列表
|
||||
* tags: [Orders]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
@@ -164,11 +27,37 @@
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - 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
|
||||
* schema:
|
||||
* 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:
|
||||
* 200:
|
||||
* description: 成功获取订单列表
|
||||
@@ -197,14 +86,162 @@
|
||||
* type: integer
|
||||
* pages:
|
||||
* type: integer
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/{id}:
|
||||
* get:
|
||||
* summary: 获取订单详情
|
||||
* /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: 服务器错误
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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:
|
||||
* - in: path
|
||||
* name: id
|
||||
@@ -227,10 +264,10 @@
|
||||
* properties:
|
||||
* order:
|
||||
* $ref: '#/components/schemas/Order'
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/OrderItem'
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 订单不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
225
docs/schemas/announcement.js
Normal file
225
docs/schemas/announcement.js
Normal 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: "公告不存在"
|
||||
*/
|
||||
@@ -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;
|
||||
next();
|
||||
} catch (error) {
|
||||
@@ -49,4 +62,51 @@ const adminAuth = (req, res, 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 };
|
||||
17
migrations/add_alipay_support.sql
Normal file
17
migrations/add_alipay_support.sql
Normal 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`);
|
||||
27
migrations/create_payment_orders_table.sql
Normal file
27
migrations/create_payment_orders_table.sql
Normal 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
381
package-lock.json
generated
@@ -11,10 +11,12 @@
|
||||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^4.1.2",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"alipay-sdk": "^4.14.0",
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -27,10 +29,12 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-rsa": "^1.1.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
@@ -311,6 +315,28 @@
|
||||
"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": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
@@ -407,6 +433,26 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -463,6 +509,15 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"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": {
|
||||
"version": "3.2.6",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -714,6 +778,36 @@
|
||||
"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": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -972,6 +1066,19 @@
|
||||
"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": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
@@ -1094,6 +1201,16 @@
|
||||
"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": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
@@ -1315,6 +1432,18 @@
|
||||
"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": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
|
||||
@@ -1449,6 +1578,30 @@
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1824,6 +1977,15 @@
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
@@ -2172,6 +2343,18 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2407,6 +2590,16 @@
|
||||
"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": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
@@ -2416,6 +2609,24 @@
|
||||
"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": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||
@@ -2630,6 +2841,18 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
@@ -2809,6 +3032,18 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -3163,6 +3398,42 @@
|
||||
"integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==",
|
||||
"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": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
|
||||
@@ -3187,6 +3458,15 @@
|
||||
"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": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
@@ -3368,6 +3648,12 @@
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||
@@ -3446,9 +3732,26 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -3475,12 +3778,33 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -3490,6 +3814,36 @@
|
||||
"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": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
@@ -3509,6 +3863,20 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -3742,6 +4110,15 @@
|
||||
"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": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^4.1.2",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"alipay-sdk": "^4.14.0",
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -25,10 +27,12 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.3",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-rsa": "^1.1.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
|
||||
399
routes/announcements.js
Normal file
399
routes/announcements.js
Normal 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;
|
||||
221
routes/auth.js
221
routes/auth.js
@@ -122,139 +122,105 @@ router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
|
||||
const {
|
||||
username,
|
||||
phone,
|
||||
password,
|
||||
registrationCode,
|
||||
username,
|
||||
phone,
|
||||
password,
|
||||
city,
|
||||
district_id: district,
|
||||
district_id: district,
|
||||
captchaId,
|
||||
captchaText,
|
||||
smsCode, // 短信验证码
|
||||
role = 'user'
|
||||
} = req.body;
|
||||
|
||||
if (!username || !phone || !password || !registrationCode || !city || !district) {
|
||||
return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' });
|
||||
|
||||
if (!username || !phone || !password || !city || !district) {
|
||||
return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' });
|
||||
}
|
||||
|
||||
|
||||
if (!captchaId || !captchaText) {
|
||||
return res.status(400).json({ success: false, message: '图形验证码不能为空' });
|
||||
}
|
||||
|
||||
|
||||
if (!smsCode) {
|
||||
return res.status(400).json({ success: false, message: '短信验证码不能为空' });
|
||||
}
|
||||
|
||||
|
||||
// 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
||||
|
||||
|
||||
// 验证短信验证码
|
||||
const smsAPI = require('./sms');
|
||||
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
|
||||
if (!smsValid) {
|
||||
return res.status(400).json({ success: false, message: '短信验证码错误或已过期' });
|
||||
}
|
||||
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
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(
|
||||
'SELECT id FROM users WHERE username = ? OR phone = ?',
|
||||
'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?',
|
||||
[username, phone]
|
||||
);
|
||||
|
||||
|
||||
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 [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]
|
||||
);
|
||||
|
||||
|
||||
const userId = result.insertId;
|
||||
|
||||
|
||||
// 用户余额已在创建用户时设置为默认值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 (regCode.agent_id) {
|
||||
// 验证agent_id是否存在于regional_agents表中
|
||||
const [agentExists] = await db.execute(
|
||||
'SELECT id FROM regional_agents WHERE id = ?',
|
||||
[regCode.agent_id]
|
||||
|
||||
if (agents.length > 0) {
|
||||
await db.execute(
|
||||
'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
|
||||
[agents[0].id, userId]
|
||||
);
|
||||
|
||||
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');
|
||||
|
||||
// 生成JWT token
|
||||
|
||||
// 生成JWT token(用于支付流程)
|
||||
const token = jwt.sign(
|
||||
{ userId: userId, username, role },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '注册成功',
|
||||
message: '用户信息创建成功,请完成支付以激活账户',
|
||||
token,
|
||||
user: {
|
||||
id: userId,
|
||||
@@ -264,8 +230,10 @@ router.post('/register', async (req, res) => {
|
||||
points: 0,
|
||||
audit_status: 'pending',
|
||||
city,
|
||||
district
|
||||
}
|
||||
district,
|
||||
paymentStatus: 'unpaid'
|
||||
},
|
||||
needPayment: true
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
@@ -275,8 +243,8 @@ router.post('/register', async (req, res) => {
|
||||
}
|
||||
console.error('注册错误详情:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '注册失败',
|
||||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
@@ -336,25 +304,25 @@ router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const { username, password, captchaId, captchaText } = req.body;
|
||||
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, message: '用户名和密码不能为空' });
|
||||
}
|
||||
|
||||
|
||||
if (!captchaId || !captchaText) {
|
||||
return res.status(400).json({ success: false, message: '验证码不能为空' });
|
||||
}
|
||||
// 获取存储的验证码
|
||||
// 获取存储的验证码
|
||||
const storedCaptcha = global.captchaStore.get(captchaId);
|
||||
console.log(storedCaptcha);
|
||||
|
||||
|
||||
if (!storedCaptcha) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码不存在或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > storedCaptcha.expires) {
|
||||
global.captchaStore.delete(captchaId);
|
||||
@@ -363,61 +331,77 @@ router.post('/login', async (req, res) => {
|
||||
message: '验证码已过期'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 验证验证码(不区分大小写)
|
||||
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
||||
|
||||
|
||||
// 删除已验证的验证码
|
||||
global.captchaStore.delete(captchaId);
|
||||
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '验证码错误'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
||||
|
||||
// 查找用户
|
||||
|
||||
// 查找用户(包含支付状态)
|
||||
console.log('登录尝试 - 用户名:', username);
|
||||
const [users] = await db.execute(
|
||||
'SELECT * FROM users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
|
||||
console.log('查找到的用户数量:', users.length);
|
||||
if (users.length === 0) {
|
||||
console.log('用户不存在:', username);
|
||||
return res.status(401).json({ success: false, message: '用户名或密码错误' });
|
||||
}
|
||||
|
||||
|
||||
const user = users[0];
|
||||
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
|
||||
|
||||
|
||||
// 验证密码
|
||||
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
console.log('密码验证结果:', isValidPassword);
|
||||
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log('密码验证失败');
|
||||
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') {
|
||||
return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' });
|
||||
}
|
||||
// 待审核用户可以正常登录使用系统,但匹配功能会有限制
|
||||
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, username: user.username, role: user.role },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
@@ -427,7 +411,8 @@ router.post('/login', async (req, res) => {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
points: user.points
|
||||
points: user.points,
|
||||
payment_status: user.payment_status
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -440,11 +425,11 @@ router.post('/login', async (req, res) => {
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: '访问令牌缺失' });
|
||||
}
|
||||
|
||||
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
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 = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '用户不存在' });
|
||||
}
|
||||
|
||||
|
||||
res.json({ success: true, user: users[0] });
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
@@ -479,37 +464,37 @@ router.put('/change-password', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const db = getDB();
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' });
|
||||
}
|
||||
|
||||
|
||||
// 获取用户当前密码
|
||||
const [users] = await db.execute(
|
||||
'SELECT password FROM users WHERE id = ?',
|
||||
[req.user.userId]
|
||||
);
|
||||
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '用户不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 验证旧密码
|
||||
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
|
||||
|
||||
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({ success: false, message: '旧密码错误' });
|
||||
}
|
||||
|
||||
|
||||
// 加密新密码
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
|
||||
// 更新密码
|
||||
await db.execute(
|
||||
'UPDATE users SET password = ? WHERE id = ?',
|
||||
[hashedNewPassword, req.user.userId]
|
||||
);
|
||||
|
||||
|
||||
res.json({ success: true, message: '密码修改成功' });
|
||||
} catch (error) {
|
||||
console.error('修改密码错误:', error);
|
||||
|
||||
190
routes/orders.js
190
routes/orders.js
@@ -114,9 +114,11 @@ router.get('/', auth, async (req, res) => {
|
||||
}
|
||||
|
||||
// 处理地址信息
|
||||
console.log(order.address,'order.address');
|
||||
|
||||
if (order.address) {
|
||||
try {
|
||||
order.address = JSON.parse(order.address);
|
||||
order.address = order.address;
|
||||
} catch (e) {
|
||||
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) => {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
@@ -557,9 +385,11 @@ router.get('/:id', auth, async (req, res) => {
|
||||
}
|
||||
|
||||
// 处理地址信息
|
||||
console.log(order.address,'order.address');
|
||||
|
||||
if (order.address) {
|
||||
try {
|
||||
order.address = JSON.parse(order.address);
|
||||
order.address = order.address;
|
||||
} catch (e) {
|
||||
order.address = null;
|
||||
}
|
||||
@@ -1340,7 +1170,7 @@ router.post('/confirm-payment', auth, async (req, res) => {
|
||||
}
|
||||
pointsToDeduct = 0;
|
||||
rongdouToDeduct = totalRongdouNeeded;
|
||||
} else if (isComboPayment) {
|
||||
} else if (hasPoints && hasRongdou) {
|
||||
// 组合支付:先扣积分,不足部分用融豆
|
||||
const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数
|
||||
|
||||
|
||||
400
routes/payment.js
Normal file
400
routes/payment.js
Normal 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;
|
||||
@@ -120,40 +120,50 @@ router.get('/zhejiang', async (req, res) => {
|
||||
*/
|
||||
router.get('/provinces', async (req, res) => {
|
||||
try {
|
||||
// 递归获取子区域的函数
|
||||
async function getChildrenRecursively(parentCode, level) {
|
||||
const [children] = await getDB().execute(
|
||||
`SELECT code, name as label, level FROM china_regions
|
||||
WHERE parent_code = ? AND level = ?
|
||||
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`
|
||||
// 一次性获取所有区域数据(省、市、区县)
|
||||
const [allRegions] = await getDB().execute(
|
||||
`SELECT code, name as label, level, parent_code FROM china_regions
|
||||
WHERE level <= 3
|
||||
ORDER BY level, code`
|
||||
);
|
||||
|
||||
// 为每个省份递归获取城市和区县
|
||||
for (let province of provinces) {
|
||||
province.children = await getChildrenRecursively(province.code, 2);
|
||||
}
|
||||
// 按level分组数据
|
||||
const regionsByLevel = {
|
||||
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({
|
||||
success: true,
|
||||
data: provinces
|
||||
data: regionsByLevel[1]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取省份列表错误:', error);
|
||||
|
||||
@@ -138,18 +138,6 @@ router.post('/send', async (req, res) => {
|
||||
|
||||
// 记录发送时间
|
||||
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 {
|
||||
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
|
||||
|
||||
@@ -1234,6 +1234,11 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r
|
||||
|
||||
const { transferId } = req.params;
|
||||
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
|
||||
const actualNewStatus = newStatus || status;
|
||||
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: '新状态不能为空' });
|
||||
}
|
||||
|
||||
if (!reason) {
|
||||
return res.status(400).json({ success: false, message: '变更原因不能为空' });
|
||||
}
|
||||
// if (!reason) {
|
||||
// return res.status(400).json({ success: false, message: '变更原因不能为空' });
|
||||
// }
|
||||
|
||||
const result = await transferService.forceChangeTransferStatus(
|
||||
transferId,
|
||||
|
||||
202
routes/wechatPay.js
Normal file
202
routes/wechatPay.js
Normal 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
87
run_migration.js
Normal 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();
|
||||
@@ -40,6 +40,7 @@ app.use(cors({
|
||||
origin: [
|
||||
'http://localhost:5173',
|
||||
'http://localhost:5176',
|
||||
'http://localhost:5175',
|
||||
'http://localhost:5174',
|
||||
'http://localhost:3001',
|
||||
'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/address-labels', require('./routes/address-labels'));
|
||||
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
|
||||
app.get('/', (req, res) => {
|
||||
|
||||
224
services/alipayservice.js
Normal file
224
services/alipayservice.js
Normal 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;
|
||||
@@ -851,7 +851,8 @@ class MatchingService {
|
||||
historicalNetBalance: user.historical_net_balance,
|
||||
totalPendingInflow: user.total_pending_inflow,
|
||||
availableForAllocation: user.available_for_allocation,
|
||||
todayOutflow: user.today_outflow
|
||||
todayOutflow: user.today_outflow,
|
||||
has_active_allocations:user.has_active_allocations
|
||||
});
|
||||
remainingAmount -= maxUserAllocation;
|
||||
}
|
||||
@@ -871,7 +872,7 @@ class MatchingService {
|
||||
// 计算每个用户的剩余可分配容量
|
||||
for (const allocation of allocations) {
|
||||
// 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额)
|
||||
const maxSafeAmount = Math.abs(allocation.availableForAllocation);
|
||||
const maxSafeAmount = Math.abs(allocation.has_active_allocations);
|
||||
const remainingCapacity = maxSafeAmount - allocation.amount;
|
||||
|
||||
if (remainingCapacity > 0) {
|
||||
|
||||
@@ -862,14 +862,15 @@ class TransferService {
|
||||
|
||||
// 根据所有相关transfers的状态来决定matching_order的状态
|
||||
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都被取消/拒绝/未收到,匹配订单标记为已完成
|
||||
matchingOrderStatus = 'completed';
|
||||
} else if (transferStatuses.every(status => status === 'received')) {
|
||||
// 如果所有transfers都已收到,匹配订单完成
|
||||
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已确认或已收到,匹配订单为进行中状态
|
||||
matchingOrderStatus = 'matching';
|
||||
} else {
|
||||
|
||||
566
services/wechatPayService.js
Normal file
566
services/wechatPayService.js
Normal 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;
|
||||
Reference in New Issue
Block a user