Compare commits
2355 Commits
codex/wuyo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c1efebf27 | ||
|
|
0509df6408 | ||
|
|
89eb66c6f6 | ||
|
|
7ce4b93706 | ||
|
|
75ba586ac8 | ||
|
|
c34b72304d | ||
|
|
3cc85ed5d2 | ||
|
|
8153f284e5 | ||
|
|
1614bd5e95 | ||
|
|
cadb917d90 | ||
|
|
09f36608de | ||
|
|
cba68a8cc7 | ||
|
|
414b86fd56 | ||
|
|
df253a470d | ||
|
|
4e949067d3 | ||
|
|
8aa17b7a34 | ||
|
|
d5b5a60141 | ||
|
|
ef4e4e36ed | ||
|
|
5cf84a19c8 | ||
|
|
7210dd2111 | ||
|
|
071cd23ac9 | ||
|
|
211aff2578 | ||
|
|
c691669101 | ||
|
|
2b4eb17ec7 | ||
|
|
c62d951228 | ||
|
|
da4b77e6d6 | ||
|
|
9360c553a2 | ||
|
|
a843b643ed | ||
|
|
05e1f94e46 | ||
|
|
de26559e3e | ||
|
|
c2f005712b | ||
|
|
177d11dd47 | ||
|
|
c104a029c9 | ||
|
|
ee983673d5 | ||
|
|
132331733e | ||
|
|
1b849b0209 | ||
|
|
d8b5006e44 | ||
|
|
7fc43502fd | ||
|
|
62452b6343 | ||
|
|
2fde21f5da | ||
|
|
eca1d63842 | ||
|
|
f276164532 | ||
|
|
399fd8182d | ||
|
|
5c899b4705 | ||
|
|
fbe276d4f6 | ||
|
|
4be4bd1e09 | ||
|
|
bc8a4a15e9 | ||
|
|
77c91bd689 | ||
|
|
4bd48d8bc0 | ||
|
|
d8e43e89ba | ||
|
|
12f04a2f96 | ||
|
|
47eed75bca | ||
|
|
0ca4e4f8f1 | ||
|
|
a54d32941f | ||
|
|
559d025e6d | ||
|
|
141ac4654c | ||
|
|
9465c98702 | ||
|
|
11fd22f356 | ||
|
|
fc862c80fc | ||
|
|
bcfd191657 | ||
|
|
a9635163bc | ||
|
|
928a7b37a9 | ||
|
|
8e14aa5fb1 | ||
|
|
dc2c86275e | ||
|
|
e0c324ce06 | ||
|
|
67541f9208 | ||
|
|
e2a237f43b | ||
|
|
d1496b0c11 | ||
|
|
46ca3bf786 | ||
|
|
67834358d2 | ||
|
|
b6a326d996 | ||
|
|
ec6067facc | ||
|
|
e57f95c489 | ||
|
|
9ef800001f | ||
|
|
89fbe9231b | ||
|
|
289926294d | ||
|
|
9492eba22d | ||
|
|
bfe3073aa1 | ||
|
|
49ec0be34f | ||
|
|
1e0a8decc1 | ||
|
|
2c4128e065 | ||
|
|
d5443e9f77 | ||
|
|
bcb0636625 | ||
|
|
822df76e19 | ||
|
|
fc50035d65 | ||
|
|
b0d531e1af | ||
|
|
bf2f114762 | ||
|
|
a917949a5e | ||
|
|
cf11c4251e | ||
|
|
7f1358aa68 | ||
|
|
83f0fbee86 | ||
|
|
f84abed356 | ||
|
|
42d2447481 | ||
|
|
fb1c74ae67 | ||
|
|
20eaf3589c | ||
|
|
ce66215764 | ||
|
|
d76d024d62 | ||
|
|
56548a731e | ||
|
|
252624708d | ||
|
|
7ae628f9f1 | ||
|
|
f8b8959347 | ||
|
|
409e35e232 | ||
|
|
462fee6a46 | ||
|
|
ee033a1e08 | ||
|
|
12de551328 | ||
|
|
96f0373e79 | ||
|
|
ecd11db3af | ||
|
|
b45b584871 | ||
|
|
b237bbed7c | ||
|
|
b8093f899e | ||
|
|
f7811468f7 | ||
|
|
92cd5f2903 | ||
|
|
e612936125 | ||
|
|
c9c62f4b22 | ||
|
|
1dbcc0d84c | ||
|
|
1de64d05f6 | ||
|
|
553c237268 | ||
|
|
5ca82d1033 | ||
|
|
00dd1b2f89 | ||
|
|
75dd248046 | ||
|
|
81cffc020a | ||
|
|
34e8426bb6 | ||
|
|
a7554bebad | ||
|
|
520706df0d | ||
|
|
6a94f1b215 | ||
|
|
9ca2f2d382 | ||
|
|
c4f3063e88 | ||
|
|
cb91d7a42f | ||
|
|
6edf13e974 | ||
|
|
4885e8fc75 | ||
|
|
ffa4290c4a | ||
|
|
7b9fd60f6e | ||
|
|
0ca07f8c5e | ||
|
|
393f149c05 | ||
|
|
50062a4c3d | ||
|
|
9c2f5f6085 | ||
|
|
abcffc424a | ||
|
|
9b7ab9e532 | ||
|
|
729eaf492d | ||
|
|
253c803be6 | ||
|
|
91eeb6402a | ||
|
|
f2480700ee | ||
|
|
158995b71b | ||
|
|
26ebec4c40 | ||
|
|
8270ae9b38 | ||
|
|
33555c7f6b | ||
|
|
737926de7e | ||
|
|
de7518bb16 | ||
|
|
cd8d5f2799 | ||
|
|
a944d5eecd | ||
|
|
ce3658a33d | ||
|
|
3679c247bf | ||
|
|
7e1ca79dc0 | ||
|
|
f665a6d271 | ||
|
|
1a53aa31a3 | ||
|
|
eca96b3067 | ||
|
|
1779d8a697 | ||
|
|
e95748dee9 | ||
|
|
530a35711f | ||
|
|
810f3a8a07 | ||
|
|
c4c370df2c | ||
|
|
1187a7ec56 | ||
|
|
e4fa4a2880 | ||
|
|
9b7d691ca9 | ||
|
|
8d08176ab2 | ||
|
|
18eaaa4c5f | ||
|
|
fb1d25b373 | ||
|
|
b2ca7bf7f1 | ||
|
|
e2254c50f5 | ||
|
|
93293258d1 | ||
|
|
f86d27cae1 | ||
|
|
d727421dca | ||
|
|
f94c86bcc8 | ||
|
|
5fdeea9a51 | ||
|
|
5a00ad317d | ||
|
|
cf5f3b2d49 | ||
|
|
28e972f663 | ||
|
|
ca08d89946 | ||
|
|
bffa3ec1a6 | ||
|
|
3fd4c0fac0 | ||
|
|
ee95abd0b0 | ||
|
|
6989354a42 | ||
|
|
8a4e0ac797 | ||
|
|
afe9b806d5 | ||
|
|
6d3891fc44 | ||
|
|
4cd07bdda5 | ||
|
|
8617493f8a | ||
|
|
c3b20aace6 | ||
|
|
5e842eeeb8 | ||
|
|
b5dc4a1b1b | ||
|
|
139aec0e70 | ||
|
|
1ba16cbc6b | ||
|
|
eaf11faea1 | ||
|
|
13b81e5822 | ||
|
|
bb777d4880 | ||
|
|
1bea953315 | ||
|
|
03741083fa | ||
|
|
c23f616003 | ||
|
|
a3e236e986 | ||
|
|
9baee61769 | ||
|
|
64a820a998 | ||
|
|
c81b696a62 | ||
|
|
f13ed3d3fb | ||
|
|
ad0b094af1 | ||
|
|
2925818a75 | ||
|
|
b7dd8ac490 | ||
|
|
b295e128eb | ||
|
|
8a3f32c5a2 | ||
|
|
da1872c59d | ||
|
|
4e594a4eee | ||
|
|
ba1209deb3 | ||
|
|
8e38894a2b | ||
|
|
605fb1347b | ||
|
|
bcfadc1e3f | ||
|
|
867b5bf897 | ||
|
|
cde4d59afb | ||
|
|
7979671a3d | ||
|
|
3a10aa3d9f | ||
|
|
9996ee83b2 | ||
|
|
af378a9d00 | ||
|
|
e6241481aa | ||
|
|
12feabd17f | ||
|
|
24e403b183 | ||
|
|
2342c53704 | ||
|
|
246313c45a | ||
|
|
838dcce410 | ||
|
|
2528229c08 | ||
|
|
91af9c5284 | ||
|
|
f3f3bfa08b | ||
|
|
685f3cc756 | ||
|
|
17474f0d85 | ||
|
|
174b1eec25 | ||
|
|
6363ed87b7 | ||
|
|
173c594c24 | ||
|
|
62f693223f | ||
|
|
bc79cd6321 | ||
|
|
48a8de5d77 | ||
|
|
bd0a5c81f3 | ||
|
|
5394319205 | ||
|
|
5779dcf5be | ||
|
|
97fffc4f3d | ||
|
|
5b8e1945af | ||
|
|
45ba5c1005 | ||
|
|
70fb47f529 | ||
|
|
d5762b3e12 | ||
|
|
e54976df03 | ||
|
|
f14d796056 | ||
|
|
0a0b925d99 | ||
|
|
1f8e8bab74 | ||
|
|
1b3498e785 | ||
|
|
ed865a3c2d | ||
|
|
1aef696ae6 | ||
|
|
b9a9d4b426 | ||
|
|
4fc857f1bd | ||
|
|
c1dbfaa145 | ||
|
|
debdfb454b | ||
|
|
255130b31f | ||
|
|
5367bc4d27 | ||
|
|
dfdfe019f2 | ||
|
|
5f31eca7ac | ||
|
|
cb3e973bf6 | ||
|
|
855cce7034 | ||
|
|
ed893bd598 | ||
|
|
3d64e3e332 | ||
|
|
60ea2f923b | ||
|
|
faa60655f7 | ||
|
|
0498a8f2e6 | ||
|
|
47f446bf62 | ||
|
|
14cb9939ad | ||
|
|
9d8b75ca4d | ||
|
|
1b49125ffa | ||
|
|
787fcb67bb | ||
|
|
83d10ec9e7 | ||
|
|
9a2bff07ab | ||
|
|
ff722ab12e | ||
|
|
7620e4bcb6 | ||
|
|
bc10af7b62 | ||
|
|
9f63e51702 | ||
|
|
e57604ac81 | ||
|
|
c981ff3bb0 | ||
|
|
8370ef2ddc | ||
|
|
a308eb6c81 | ||
|
|
dbb2d4965a | ||
|
|
eac3344e2d | ||
|
|
1da917bb01 | ||
|
|
08bb5a96a4 | ||
|
|
b0a6cdd1e7 | ||
|
|
69618965c7 | ||
|
|
f6e4d7f2e1 | ||
|
|
614e3dabe2 | ||
|
|
ce2a6ebbaf | ||
|
|
b1f55faebb | ||
|
|
c5cae01ecd | ||
|
|
7e6cc35199 | ||
|
|
57b2b405cf | ||
|
|
e1b8989621 | ||
|
|
f5f20a8b18 | ||
|
|
a110714305 | ||
|
|
f430b3e18d | ||
|
|
5cd69fb9db | ||
|
|
a4e61b44a1 | ||
|
|
2be45f7a74 | ||
|
|
14a5e1c5f7 | ||
|
|
6d5a8a65d3 | ||
|
|
e95a2365f5 | ||
|
|
294d6d3dfa | ||
|
|
cda264dd30 | ||
|
|
cfe8cf950c | ||
|
|
3ef36c7648 | ||
|
|
83c10719ca | ||
|
|
ac572e2c4d | ||
|
|
77eff1744a | ||
|
|
05007d3ae2 | ||
|
|
a771a9a5dc | ||
|
|
3f1de4e431 | ||
|
|
155c93a0c5 | ||
|
|
471b5b373e | ||
|
|
df206dbfe3 | ||
|
|
dc0a9e740a | ||
|
|
398bc73450 | ||
|
|
4369e2f050 | ||
|
|
e61772c2ce | ||
|
|
5bc45f4e93 | ||
|
|
4c4807c72e | ||
|
|
baa2017106 | ||
|
|
38120d3f73 | ||
|
|
9d2633420d | ||
|
|
de6d9014b3 | ||
|
|
0a2a9c2f89 | ||
|
|
6a36cae707 | ||
|
|
d47d550409 | ||
|
|
303694784c | ||
|
|
da46e84526 | ||
|
|
6eebb56ba0 | ||
|
|
ad08dccf1c | ||
|
|
6da6b0df76 | ||
|
|
b9dc0bddb1 | ||
|
|
4008bcf061 | ||
|
|
39f603c091 | ||
|
|
34abc63edc | ||
|
|
0a8231d93c | ||
|
|
7838b82655 | ||
|
|
9f89ccbb7e | ||
|
|
1dcaa48baf | ||
|
|
51ff817dde | ||
|
|
791fcf083b | ||
|
|
4cb5f9192a | ||
|
|
0f9bade034 | ||
|
|
eedda35a29 | ||
|
|
55d77fd989 | ||
|
|
c45ec60635 | ||
|
|
efc437f0de | ||
|
|
3b1a629854 | ||
|
|
a38aaef5d2 | ||
|
|
7caa307042 | ||
|
|
e0bad60b31 | ||
|
|
8e0fc1d347 | ||
|
|
574f42c4ed | ||
|
|
5102516b10 | ||
|
|
189344626e | ||
|
|
fe6389a2a0 | ||
|
|
9d53362466 | ||
|
|
005cc37a65 | ||
|
|
2f081dd1f5 | ||
|
|
b0701d8ae4 | ||
|
|
f0af29c62b | ||
|
|
2a61ed2b34 | ||
|
|
143dfe019d | ||
|
|
f5f4e2ec90 | ||
|
|
fefb1319c0 | ||
|
|
cde8d617ad | ||
|
|
85c69041fe | ||
|
|
1762ba8387 | ||
|
|
84515e895c | ||
|
|
852d42bf3a | ||
|
|
8cd0f71ff7 | ||
|
|
8c1c4bd938 | ||
|
|
b1c3ba3bda | ||
|
|
ea7c5a953c | ||
|
|
117a5b2947 | ||
|
|
2dbc455b86 | ||
|
|
c59647ac9f | ||
|
|
7b3a7acb12 | ||
|
|
95f5915c6a | ||
|
|
ff1ea6ce24 | ||
|
|
5eb7d4b1aa | ||
|
|
d40cad8eed | ||
|
|
110de56e7f | ||
|
|
d7e348b236 | ||
|
|
b2f4242526 | ||
|
|
4c771e0a82 | ||
|
|
f6b8065580 | ||
|
|
db01d9e445 | ||
|
|
eb6fa9d672 | ||
|
|
4c518b054e | ||
|
|
9ae9423742 | ||
|
|
b976f075ff | ||
|
|
648b1eee8d | ||
|
|
749c28452c | ||
|
|
f2dfcf1419 | ||
|
|
0e85656279 | ||
|
|
fe99b46eef | ||
|
|
afbb7b3aad | ||
|
|
7384163a2c | ||
|
|
c473825b3d | ||
|
|
5664958dfb | ||
|
|
199be5d7cb | ||
|
|
51c6c9f20b | ||
|
|
8b76cd57db | ||
|
|
3f36d2b51e | ||
|
|
0fde0bfe4b | ||
|
|
61a4d81658 | ||
|
|
2c4949f1cc | ||
|
|
c89e4248f0 | ||
|
|
2b14e8863e | ||
|
|
97d618a25d | ||
|
|
5f4170919d | ||
|
|
b01a5bb574 | ||
|
|
da3a49f03f | ||
|
|
a2dab2cd49 | ||
|
|
a4e577e376 | ||
|
|
4f12fdfcac | ||
|
|
4fb7802eb4 | ||
|
|
d1aa93e17f | ||
|
|
6a52c0aa4b | ||
|
|
daa0abbf03 | ||
|
|
48fcd34608 | ||
|
|
80f82d8ff0 | ||
|
|
9d7023df4a | ||
|
|
d2f965611c | ||
|
|
a87a8b50b3 | ||
|
|
13e1dcb7b2 | ||
|
|
68b310f646 | ||
|
|
f9c32930da | ||
|
|
2ba2a37232 | ||
|
|
5a59e727c0 | ||
|
|
52ab8578e5 | ||
|
|
4e52997d64 | ||
|
|
ccf8b6480f | ||
|
|
b96dfe9780 | ||
|
|
aee6dc6c8f | ||
|
|
010cd87601 | ||
|
|
5013d709e4 | ||
|
|
ca0e2d884c | ||
|
|
02843032d2 | ||
|
|
90eebbc74d | ||
|
|
f988334005 | ||
|
|
e6545b6eae | ||
|
|
0cb88569d8 | ||
|
|
f9ff4af8c5 | ||
|
|
3bd5384699 | ||
|
|
5af8c6c539 | ||
|
|
6f63f3936e | ||
|
|
7fa65e3309 | ||
|
|
997efc1084 | ||
|
|
36493b75f8 | ||
|
|
001a1e919e | ||
|
|
1422c2f5fa | ||
|
|
427448f48e | ||
|
|
218517d5af | ||
|
|
3e31bcf29e | ||
|
|
2e3a36b25d | ||
|
|
842bdd3967 | ||
|
|
0b7cb58418 | ||
|
|
e3e572657a | ||
|
|
ab817eddfa | ||
|
|
3dcdeb5cf1 | ||
|
|
0a2e0484b3 | ||
|
|
01cf89cd01 | ||
|
|
5984179dc9 | ||
|
|
f75b05bda1 | ||
|
|
9e7f8914f0 | ||
|
|
a45c691428 | ||
|
|
fff6675c7a | ||
|
|
9e91f18dfd | ||
|
|
2af84fd9ce | ||
|
|
9808224718 | ||
|
|
cb3efaa50c | ||
|
|
d52fd19c49 | ||
|
|
eb6a2e6571 | ||
|
|
ebf9e6a6c4 | ||
|
|
d42d500071 | ||
|
|
e8a374e93c | ||
|
|
fddf877d9e | ||
|
|
cc7219a24e | ||
|
|
c066c60494 | ||
|
|
9875a28716 | ||
|
|
97a173eb9c | ||
|
|
a8b6d31c5e | ||
|
|
46d0521105 | ||
|
|
44be8f58b8 | ||
|
|
02a25b1e75 | ||
|
|
2d4965dfc7 | ||
|
|
272219a4eb | ||
|
|
da4be64bfe | ||
|
|
c9431b7e26 | ||
|
|
a93159dddc | ||
|
|
1b293985d6 | ||
|
|
291485dbda | ||
|
|
aff07c4375 | ||
|
|
84ac3ed6a4 | ||
|
|
791f6bfa40 | ||
|
|
eabc26a044 | ||
|
|
4b800d0b3b | ||
|
|
3d22d627fb | ||
|
|
fa17ffa9a8 | ||
|
|
159035f98a | ||
|
|
8a95dbcbf0 | ||
|
|
71a7f0a09a | ||
|
|
9bfc3e826f | ||
|
|
89ed5ee355 | ||
|
|
24c668e1c8 | ||
|
|
870632bd5e | ||
|
|
152df22a1a | ||
|
|
6e35bb42ee | ||
|
|
6f55f490f6 | ||
|
|
0f52aca923 | ||
|
|
c0cc39a565 | ||
|
|
41030ec7f2 | ||
|
|
687e80b6cd | ||
|
|
5512be5371 | ||
|
|
7b5f66dfcf | ||
|
|
271c7b24b4 | ||
|
|
f94286464b | ||
|
|
e3dcc2463a | ||
|
|
4b54f3e531 | ||
|
|
669fbd63fd | ||
|
|
191e050374 | ||
|
|
9e0081724e | ||
|
|
fd3b451af9 | ||
|
|
616f985dee | ||
|
|
16587f62af | ||
|
|
f57fd6d354 | ||
|
|
407946d5d2 | ||
|
|
37d09f0ca1 | ||
|
|
3269f4409f | ||
|
|
65b32ce405 | ||
|
|
c4943e6b04 | ||
|
|
475478b273 | ||
|
|
ec807b3217 | ||
|
|
e9cb61e18c | ||
|
|
1cf72e3c81 | ||
|
|
4df5e800fe | ||
|
|
ce48ab7944 | ||
|
|
bb730dd533 | ||
|
|
073437c8bf | ||
|
|
b92d56c090 | ||
|
|
d4778d29e9 | ||
|
|
ba5eeaea33 | ||
|
|
4450ac5ce6 | ||
|
|
87a28cc5f7 | ||
|
|
7d8f6d3848 | ||
|
|
76420b8ecd | ||
|
|
5a6f09b0df | ||
|
|
009685cdd7 | ||
|
|
9244b9d570 | ||
|
|
a22657e28e | ||
|
|
446c550320 | ||
|
|
8ea81f886a | ||
|
|
f3f59adcbc | ||
|
|
b0bfd3d65a | ||
|
|
fc12269838 | ||
|
|
9647a1f49f | ||
|
|
dc07310cda | ||
|
|
e8f8087f78 | ||
|
|
be9f844c5a | ||
|
|
ca6e22ac66 | ||
|
|
8d38f07626 | ||
|
|
a310362839 | ||
|
|
14974d20ef | ||
|
|
26754ae266 | ||
|
|
98482bccec | ||
|
|
ba930d1df2 | ||
|
|
3cd14c4f0d | ||
|
|
57e422308b | ||
|
|
cffa8b64f8 | ||
|
|
b0036bcc48 | ||
|
|
f30866530a | ||
|
|
48871454e9 | ||
|
|
a3b9fa0fbb | ||
|
|
6f023e3f0d | ||
|
|
c5e1c50418 | ||
|
|
990a4bfeb1 | ||
|
|
9533c2e4ec | ||
|
|
6492c2e400 | ||
|
|
20755dfdd1 | ||
|
|
f8d74a5543 | ||
|
|
d73508bae6 | ||
|
|
2d93a22721 | ||
|
|
9983ab3a1a | ||
|
|
9c9a6a9a08 | ||
|
|
570dcacdc9 | ||
|
|
adac0dfc3a | ||
|
|
374a39b6dc | ||
|
|
60034dfff0 | ||
|
|
44c69bef3e | ||
|
|
2a7d2b04a2 | ||
|
|
f9d29ba92f | ||
|
|
495ff9f30b | ||
|
|
3ae72e800f | ||
|
|
a4bff5dd59 | ||
|
|
d3dc8821c2 | ||
|
|
31dfacab23 | ||
|
|
9e50804b2e | ||
|
|
b0fd9a8800 | ||
|
|
55ee1b421b | ||
|
|
8a7d6cae5a | ||
|
|
260fbc8428 | ||
|
|
36616821cf | ||
|
|
53febe8eb7 | ||
|
|
170d776aa6 | ||
|
|
64e3378125 | ||
|
|
f6dd04eeee | ||
|
|
1b301af15e | ||
|
|
499cbf66d1 | ||
|
|
78dcdae880 | ||
|
|
79f34971d6 | ||
|
|
dac31e736a | ||
|
|
b63b2fe974 | ||
|
|
200790b6fc | ||
|
|
ed15789a81 | ||
|
|
73be6c9019 | ||
|
|
027d17dd4a | ||
|
|
01fd83d28b | ||
|
|
2aa856197b | ||
|
|
bc2104b169 | ||
|
|
e271d1b941 | ||
|
|
dfdc3c3626 | ||
|
|
5156ead4c1 | ||
|
|
090c6b39c5 | ||
|
|
38259cc40c | ||
|
|
d20d7e4dd6 | ||
|
|
687ef50bb5 | ||
|
|
f6dc344b71 | ||
|
|
2b192afd86 | ||
|
|
6aba6e077c | ||
|
|
9892fead34 | ||
|
|
9e9d92306e | ||
|
|
0ea1c3ecdd | ||
|
|
43f8540c9a | ||
|
|
002aa9dc34 | ||
|
|
d9d1b39d1c | ||
|
|
4f2ea0bdf0 | ||
|
|
904b80d4c9 | ||
|
|
aaa9c74eec | ||
|
|
dd693dfbd5 | ||
|
|
1b3f210e0f | ||
|
|
04b24f7972 | ||
|
|
222f3e6a97 | ||
|
|
02ad38908e | ||
|
|
6f49206964 | ||
|
|
0c045ff906 | ||
|
|
0c7d147129 | ||
|
|
e075a81eb7 | ||
|
|
9e5a0d20b5 | ||
|
|
56560cd6d2 | ||
|
|
e2f211c7ac | ||
|
|
7f9adebf88 | ||
|
|
46a732d892 | ||
|
|
2cfd57d678 | ||
|
|
aa1ca0afcf | ||
|
|
b66e6b1f11 | ||
|
|
0875c87058 | ||
|
|
a20b6cb79f | ||
|
|
b9c85357e6 | ||
|
|
5c7ebbe914 | ||
|
|
2947ddebcf | ||
|
|
69d19076bd | ||
|
|
17d2e1d7bb | ||
|
|
351b7d7d2a | ||
|
|
b8e80204a0 | ||
|
|
1042f35090 | ||
|
|
eedea9f791 | ||
|
|
0271da1b69 | ||
|
|
b647cc2943 | ||
|
|
3360ccb801 | ||
|
|
1c0beffcb6 | ||
|
|
254cd92c5b | ||
|
|
3976f96fcb | ||
|
|
6161bb3e59 | ||
|
|
3cd064a467 | ||
|
|
db05c1ca0a | ||
|
|
1bb87a454d | ||
|
|
cc1e15582e | ||
|
|
560579b7f9 | ||
|
|
f0cc716dd0 | ||
|
|
66746bdc7e | ||
|
|
d563513875 | ||
|
|
1a0d3a7429 | ||
|
|
c20bf5314a | ||
| f615161d8c | |||
|
|
ed339ceb3f | ||
|
|
149cf46b63 | ||
| 17daaee224 | |||
| 2224112c29 | |||
| 4fd134f636 | |||
| caa960d70a | |||
|
|
9965c93c8d | ||
|
|
811926e67b | ||
|
|
c9d2f37ead | ||
|
|
4deea6518a | ||
|
|
0b4311a882 | ||
|
|
ecfd596ad0 | ||
|
|
df9c0ad67f | ||
|
|
d2f71b3723 | ||
|
|
5754172787 | ||
|
|
ee8172c724 | ||
|
|
7139f40c8d | ||
|
|
54bf9107fb | ||
|
|
41e962da37 | ||
|
|
22cede9ed0 | ||
| c1dd4e666e | |||
|
|
494a1cf60f | ||
|
|
f5a8ec1c05 | ||
|
|
e9f83b16ee | ||
|
|
416e22754a | ||
|
|
d4d26c43bf | ||
|
|
8fb872a3f9 | ||
|
|
f8ba4f1e25 | ||
|
|
39d7fc02f6 | ||
|
|
8ea6a54eaa | ||
|
|
667438447a | ||
|
|
092036786c | ||
|
|
4a574041a9 | ||
|
|
2dbb214006 | ||
|
|
e926a5a990 | ||
|
|
4f6510271f | ||
|
|
fccf9485b7 | ||
|
|
f4712cd1a2 | ||
|
|
e27e4fe67e | ||
|
|
4843e16124 | ||
|
|
f41e87373b | ||
|
|
d32988dde6 | ||
|
|
b30e5e5850 | ||
|
|
bbb35a44f3 | ||
|
|
56f3a7fe3f | ||
|
|
d3dee58b2b | ||
|
|
3df9bf08b1 | ||
|
|
d08a244fcb | ||
|
|
b488e6084c | ||
|
|
99f004a593 | ||
|
|
a252c00ac2 | ||
|
|
13dd73069e | ||
|
|
592b53f0ac | ||
|
|
16d6069123 | ||
|
|
a5cca36d69 | ||
|
|
966a8340e4 | ||
|
|
6b3760c6e7 | ||
|
|
92bd812a17 | ||
|
|
3456d1f465 | ||
|
|
ffcf242738 | ||
|
|
24b5d5872f | ||
|
|
b335ef7719 | ||
|
|
394927d843 | ||
|
|
f4adeb3808 | ||
|
|
452c41bc4d | ||
|
|
cac0452672 | ||
|
|
0c65f7af2b | ||
|
|
4a9e5d99be | ||
|
|
04d6219417 | ||
|
|
30ab1b4b35 | ||
|
|
b5d05f2779 | ||
|
|
4ce03262c0 | ||
|
|
4014f53b9e | ||
|
|
3e92e7242a | ||
|
|
c10e787fcf | ||
|
|
17a0491a4a | ||
|
|
1e1f667530 | ||
|
|
33a13a8abb | ||
|
|
4ceedf50ee | ||
|
|
41a6bbff48 | ||
|
|
856522552f | ||
|
|
c30aed3987 | ||
|
|
205db8234c | ||
|
|
1edbc110d4 | ||
|
|
455025f546 | ||
|
|
58d5e0ffdd | ||
|
|
583e3240ce | ||
|
|
88ddd67736 | ||
|
|
259f4b8e7e | ||
|
|
bb561d2482 | ||
|
|
8f5fde8d4e | ||
|
|
925868fb65 | ||
|
|
3556b48c4a | ||
|
|
74ac36f34d | ||
|
|
b129acdc48 | ||
|
|
ee9a7cb88e | ||
|
|
970cd3f234 | ||
|
|
7f15c5b756 | ||
|
|
25f5796dd0 | ||
|
|
ce34f56c39 | ||
|
|
7ed6c061c4 | ||
|
|
15f3331d58 | ||
|
|
cc0f5e65f4 | ||
|
|
607b030034 | ||
|
|
021d7eab2b | ||
|
|
adfecb4c67 | ||
|
|
5aedfea85c | ||
|
|
58966bba20 | ||
|
|
7740433ae7 | ||
|
|
5c43428a47 | ||
|
|
7f078abb7b | ||
|
|
940e94882e | ||
|
|
7126984caa | ||
|
|
43b867ef55 | ||
|
|
9c459e7195 | ||
|
|
e4df1b2576 | ||
|
|
b2fc4dde6d | ||
|
|
138dc0d644 | ||
|
|
f91554c7dc | ||
|
|
6ec0258001 | ||
|
|
9c66706b67 | ||
|
|
f89bc61af4 | ||
|
|
3c69088433 | ||
|
|
e60b3b1e60 | ||
|
|
d06e45b0c8 | ||
|
|
03eca07f9f | ||
|
|
242ed201ba | ||
|
|
6b8661dcfd | ||
|
|
b3f85d5f5a | ||
|
|
dc6c45848a | ||
|
|
eead468d4d | ||
|
|
fb51614a86 | ||
|
|
e7996abc6a | ||
|
|
474f245ba7 | ||
|
|
03f308261e | ||
|
|
934b073eea | ||
|
|
9a3c5f9157 | ||
|
|
6e140e1445 | ||
|
|
0b12ec28b9 | ||
|
|
2b5f49fc51 | ||
|
|
7b7e7d7e28 | ||
|
|
b01579ca99 | ||
|
|
d5f2b68d88 | ||
|
|
a2afbc40bb | ||
|
|
facc1f0955 | ||
|
|
e38f8e813b | ||
|
|
70e268034e | ||
|
|
508e0a40d5 | ||
|
|
c0562fdddc | ||
|
|
22227e7cad | ||
|
|
7089ada0a0 | ||
|
|
9bb36756d1 | ||
|
|
f2a554a330 | ||
|
|
0aaacb4bff | ||
|
|
bf7b71ab00 | ||
|
|
4ecad009b9 | ||
|
|
e43bf78c73 | ||
|
|
fecfd9053e | ||
|
|
7c6be91a91 | ||
|
|
7d246e1d5b | ||
|
|
b974d83986 | ||
|
|
232eaa8768 | ||
|
|
c2289ca074 | ||
|
|
68916e2791 | ||
|
|
72262acfa0 | ||
|
|
4950135cfc | ||
|
|
d40b6ad197 | ||
|
|
315ede4f3f | ||
|
|
f3b0124d4c | ||
|
|
fb53234811 | ||
|
|
9e51faa192 | ||
|
|
916930a714 | ||
|
|
7c17626339 | ||
|
|
368107c17b | ||
|
|
865be95b6e | ||
|
|
3efe408a27 | ||
|
|
b1505b4d63 | ||
|
|
e1b7683bfd | ||
|
|
a69bf1997b | ||
|
|
622fc9d527 | ||
|
|
3409a9db50 | ||
|
|
0f55175d9b | ||
|
|
0811bea57c | ||
|
|
0ff6f87757 | ||
|
|
089845e7ee | ||
|
|
ab5ea15e80 | ||
|
|
1347a34d14 | ||
|
|
6a32957bf5 | ||
|
|
9bf54d95d3 | ||
|
|
dc16d2b75c | ||
|
|
cd73be673d | ||
|
|
85c31677c5 | ||
|
|
59634f8516 | ||
|
|
d8495e98ef | ||
|
|
ea363984a7 | ||
|
|
47df269785 | ||
|
|
19e39e0b8c | ||
|
|
68c48086df | ||
|
|
e34c80cdd2 | ||
|
|
73b3049e01 | ||
|
|
978b609325 | ||
|
|
6e9d0e474f | ||
|
|
240ea4b01a | ||
|
|
422c23d3e5 | ||
|
|
f974e7d4b4 | ||
|
|
b27399d908 | ||
|
|
cc7e45a225 | ||
|
|
0a466dcb2b | ||
|
|
4492c0eb8f | ||
|
|
0f336e9d1e | ||
|
|
4d34a387a9 | ||
|
|
571b840911 | ||
|
|
78cfa85232 | ||
|
|
d8141a63a9 | ||
|
|
bf85258d3c | ||
|
|
e48229ab22 | ||
|
|
48e496b01b | ||
|
|
01a9d1e2c8 | ||
|
|
13268ee5b5 | ||
|
|
55c99cbc4d | ||
|
|
023c4c07d2 | ||
|
|
37074cf25b | ||
|
|
1619213039 | ||
|
|
5da6c34758 | ||
|
|
879a73e480 | ||
|
|
629ad42104 | ||
|
|
a34dee6825 | ||
|
|
8b8a41f2d7 | ||
|
|
3f5cf79cc2 | ||
|
|
c093c32e1d | ||
|
|
07d6d0d03f | ||
|
|
71e746b5f5 | ||
|
|
9761465d45 | ||
|
|
aa5d983b7d | ||
|
|
d0ddbb6ccf | ||
|
|
77b3b3c800 | ||
|
|
2cd254fee5 | ||
|
|
ffc9c1a727 | ||
|
|
b23e4122f2 | ||
|
|
42e4503ce6 | ||
|
|
336b47a5fc | ||
|
|
ecf4d4f743 | ||
|
|
5984f82c75 | ||
|
|
2e33b9b1c0 | ||
|
|
73ffcb17bb | ||
|
|
8365286651 | ||
|
|
b497c2d061 | ||
|
|
10c32b2c97 | ||
|
|
bfbe044005 | ||
|
|
0fa4481719 | ||
|
|
b0a9aa0c4b | ||
|
|
fcdf25ed38 | ||
|
|
e8531fde1d | ||
|
|
64f1b2732b | ||
|
|
9a68b886ac | ||
|
|
9bb14e391d | ||
|
|
d252924a21 | ||
|
|
19f9affe5c | ||
|
|
b15c292f12 | ||
|
|
03c97d234b | ||
|
|
b1cebf0707 | ||
|
|
58fa0c7133 | ||
|
|
2631849409 | ||
|
|
e5985cf4cd | ||
|
|
bd651338dc | ||
|
|
022298db92 | ||
|
|
d0c11dd979 | ||
|
|
a26f53aaba | ||
|
|
1075eea0aa | ||
|
|
b15329bec8 | ||
|
|
9faa52d91d | ||
|
|
c8c06c88e9 | ||
|
|
4fe426ce56 | ||
|
|
e8990d7e63 | ||
|
|
075f37f31f | ||
|
|
095bae1be8 | ||
|
|
1e035f10a4 | ||
|
|
f5d7cfb2df | ||
|
|
26ce017921 | ||
|
|
10ae34fc4a | ||
|
|
482858405c | ||
|
|
aae05326ab | ||
|
|
b20ffaa30f | ||
|
|
07516303d4 | ||
|
|
61c66362bb | ||
|
|
633b7a37dd | ||
|
|
fa32c6bec4 | ||
|
|
22744b28c8 | ||
|
|
2897f3b0c6 | ||
|
|
718c6a8886 | ||
|
|
32fedbe898 | ||
|
|
0bf579a6f0 | ||
|
|
1dc2c04b20 | ||
|
|
e645a71283 | ||
|
|
9a8c25fe24 | ||
|
|
8ee0dd21fe | ||
|
|
c0aab147bf | ||
|
|
52ac60cf29 | ||
|
|
6178d09e31 | ||
|
|
7e772dcfa8 | ||
|
|
1f9293cc42 | ||
|
|
e2b711c0df | ||
|
|
94f754c80b | ||
|
|
2d1516e621 | ||
|
|
68051dd0bf | ||
|
|
a8184ae024 | ||
|
|
efbf1c1a41 | ||
|
|
66dced1c3f | ||
|
|
6024abe7aa | ||
|
|
b16320c352 | ||
|
|
8cfd3f8292 | ||
|
|
053a603308 | ||
|
|
90eeba280d | ||
|
|
f8555a4946 | ||
|
|
b840de8d55 | ||
|
|
641a162675 | ||
|
|
d507aa08f7 | ||
|
|
90e58ecaf7 | ||
|
|
f0730ec03d | ||
|
|
a51936ed98 | ||
|
|
91026fccd7 | ||
|
|
45f8aaf449 | ||
|
|
6b6564e59a | ||
|
|
58aa36720b | ||
|
|
5e20f8178f | ||
|
|
239cae7b5a | ||
|
|
f2599a5a37 | ||
|
|
2db8e39666 | ||
|
|
e267426f5e | ||
|
|
cfe9ce4e7b | ||
|
|
e9cd4e312a | ||
|
|
8d09d19206 | ||
|
|
d3b4d56934 | ||
|
|
f5cc1764cc | ||
|
|
6f10780bc5 | ||
|
|
d264efc2e7 | ||
|
|
63b37b564a | ||
|
|
3ba90b83e0 | ||
|
|
1ffee0d6cc | ||
|
|
ec5875367a | ||
|
|
d6e78edaf2 | ||
|
|
75f7b3ac7f | ||
|
|
df3a8a7894 | ||
|
|
d8d254598b | ||
|
|
b83a95df06 | ||
|
|
20c6433273 | ||
|
|
033c1058e9 | ||
|
|
86b60103c1 | ||
|
|
51f4c97489 | ||
|
|
4a8cbcdcb3 | ||
|
|
c30b094194 | ||
|
|
2be5f59220 | ||
|
|
c80c06d2f7 | ||
|
|
4c03c35e8d | ||
|
|
c7da99562d | ||
|
|
f08d4ff5c4 | ||
|
|
fece43291b | ||
|
|
e674843455 | ||
|
|
c038bcedbe | ||
|
|
f64fac7a68 | ||
|
|
6f0fdf3c71 | ||
|
|
7f67bd858c | ||
|
|
d3a1049b55 | ||
|
|
62e7ea28e4 | ||
|
|
e5e7c3bd07 | ||
|
|
9a17715a21 | ||
|
|
41b33c731c | ||
|
|
156b431dda | ||
|
|
fa48bcc48d | ||
|
|
4e892573df | ||
|
|
5ac98a6b82 | ||
|
|
5441d1786d | ||
|
|
5cfbcaa5fe | ||
|
|
330cf30ac9 | ||
|
|
5af369ef3d | ||
|
|
0c782238af | ||
|
|
5d60abd57b | ||
|
|
ab66b20893 | ||
|
|
dded0237d0 | ||
|
|
fb4fa3efca | ||
|
|
6a3bd0fa22 | ||
|
|
9929f6a3c6 | ||
|
|
04379751f6 | ||
|
|
8c2776ec1d | ||
|
|
54548071ad | ||
|
|
e24adfa492 | ||
|
|
54af0add08 | ||
|
|
d8dd85d380 | ||
|
|
867de78fe7 | ||
|
|
cd4b6f7023 | ||
|
|
ec687f4788 | ||
|
|
e0fb0f22a4 | ||
|
|
816d2ff544 | ||
|
|
59a1449d73 | ||
|
|
7b5fea54c5 | ||
|
|
bd33cfc936 | ||
|
|
b69f1c8eb6 | ||
|
|
016280c1a3 | ||
|
|
f78f8c1cf4 | ||
|
|
7a1d05b7db | ||
|
|
ceda595797 | ||
|
|
5082213a11 | ||
|
|
5b7435f218 | ||
|
|
57b502e1c3 | ||
|
|
65c1cd6d93 | ||
|
|
465a92612f | ||
|
|
8ffbea63ce | ||
|
|
8b1cb357ea | ||
|
|
d711742300 | ||
|
|
9142d24612 | ||
|
|
72f2519e3a | ||
|
|
d0eb2d4bd4 | ||
|
|
2972a8fb7a | ||
|
|
d2c6f0749a | ||
|
|
a0029c7dec | ||
|
|
07008eeeb7 | ||
|
|
16b43cba11 | ||
|
|
c3566c4b83 | ||
|
|
026ea66c62 | ||
|
|
4511c0ad51 | ||
|
|
c0f9c147fa | ||
|
|
ecce8a1a28 | ||
|
|
7c53b42199 | ||
|
|
8016482127 | ||
|
|
413debe66b | ||
|
|
f8d877c88c | ||
|
|
c6c2790354 | ||
|
|
b2e672139e | ||
|
|
0d6d080687 | ||
|
|
ec38c6e64a | ||
|
|
d786515831 | ||
|
|
032b609f1b | ||
|
|
502d85decb | ||
|
|
88fbdbacd2 | ||
|
|
42ad3299a3 | ||
|
|
bdb21cd824 | ||
|
|
a5c0a4f83d | ||
|
|
7371676f6f | ||
|
|
6acbc75a09 | ||
|
|
7b46654cb7 | ||
|
|
2ab42d68e1 | ||
|
|
2199376f11 | ||
|
|
4192f27b17 | ||
|
|
ea834d8dea | ||
|
|
f34d938e2e | ||
|
|
f6b28ecd63 | ||
|
|
ade0751701 | ||
|
|
2ba8ab0f05 | ||
|
|
62017377ad | ||
|
|
2905c42455 | ||
|
|
ce808f8c07 | ||
|
|
915450a748 | ||
|
|
adae1ac0f1 | ||
|
|
db844e6126 | ||
|
|
cdb807dcd5 | ||
|
|
97ade48cf8 | ||
|
|
af97ea7074 | ||
|
|
138155b04a | ||
|
|
609e675659 | ||
|
|
218a7d91a6 | ||
|
|
5318a30346 | ||
|
|
c21b384050 | ||
|
|
acd22bbffa | ||
|
|
1433428cf7 | ||
|
|
5c05df5855 | ||
|
|
35384031c6 | ||
|
|
fee8bdb0a8 | ||
|
|
e2d739d1d4 | ||
|
|
f12b68ff56 | ||
|
|
869f0e6922 | ||
|
|
76421163c7 | ||
|
|
439b427259 | ||
|
|
6f8fc02b2f | ||
|
|
b12d1eeeb3 | ||
|
|
0c9491a370 | ||
|
|
37430946d8 | ||
|
|
eb0a6ad499 | ||
|
|
de048075ef | ||
|
|
c87266c2c7 | ||
|
|
a51ec8a162 | ||
|
|
14915ef1f2 | ||
|
|
7a25001d6b | ||
|
|
fd0354f1d0 | ||
|
|
ae9ae0fcae | ||
|
|
a36b5ede46 | ||
|
|
e2a725ed58 | ||
|
|
0b4f903325 | ||
|
|
8d7ab94cac | ||
|
|
27b16a9ce6 | ||
|
|
09dd4a1730 | ||
|
|
fc4763a8c9 | ||
|
|
0b2bc3ce5a | ||
|
|
73005fdd9b | ||
|
|
c2e75dda3e | ||
|
|
ac146f8879 | ||
|
|
4f1372668f | ||
|
|
344e1b108a | ||
|
|
a17844604c | ||
|
|
082d5cfcee | ||
|
|
bdc8f393a8 | ||
|
|
76230f9f34 | ||
|
|
17dd830976 | ||
|
|
70d0e4e5d9 | ||
|
|
432cd9fc4f | ||
|
|
944b5b0a58 | ||
|
|
7040fd2d03 | ||
|
|
a040b015b5 | ||
|
|
ad9afabc49 | ||
|
|
2c201674a4 | ||
|
|
ba9adb0db4 | ||
|
|
7558939482 | ||
|
|
31ef235c80 | ||
|
|
37851f0f04 | ||
|
|
20a450c4bd | ||
|
|
0ece1c38dc | ||
|
|
179040b1ab | ||
|
|
6388b8f37a | ||
|
|
ac9deefe85 | ||
|
|
1fa15cef79 | ||
|
|
23c20714dd | ||
|
|
c031c36f8f | ||
|
|
aa646a795a | ||
|
|
6a19e30527 | ||
|
|
43d8d43e16 | ||
|
|
b2fb4165b5 | ||
|
|
bdf20c2064 | ||
|
|
c86367c6b0 | ||
|
|
4dcbed4c10 | ||
|
|
199228241a | ||
|
|
63ca46c030 | ||
|
|
10ce1696dc | ||
|
|
728495143e | ||
|
|
20fee3b7ba | ||
|
|
5c547dd25f | ||
|
|
981df46e3f | ||
|
|
b24328f4f8 | ||
|
|
83a4d456b5 | ||
|
|
be247f5a4b | ||
|
|
b47f745b83 | ||
|
|
35b9c5e080 | ||
|
|
759cba00d7 | ||
|
|
6858161f3f | ||
|
|
7bfb69405d | ||
|
|
814a7ed4be | ||
|
|
9383233130 | ||
|
|
27ed15f55a | ||
|
|
7b95e5af33 | ||
|
|
5bd554f762 | ||
|
|
c1985a2396 | ||
|
|
c538e175bc | ||
|
|
24a5043d15 | ||
|
|
0ba596a7c7 | ||
|
|
bcf04b7438 | ||
|
|
d5e1f2af99 | ||
|
|
42095c6dab | ||
|
|
67ce313c9a | ||
|
|
4f1571d508 | ||
|
|
871fd38bb5 | ||
|
|
3c8d7f0801 | ||
|
|
7518041829 | ||
|
|
164192bae7 | ||
|
|
b9fd690e5d | ||
|
|
efbb08b7a5 | ||
|
|
ebf0eca3b7 | ||
|
|
5416bed96e | ||
|
|
e945eff091 | ||
|
|
aefff98cad | ||
|
|
be26f6d795 | ||
|
|
f1a7b9c941 | ||
|
|
67a68067a0 | ||
|
|
707feb213a | ||
|
|
2afdbb9ca7 | ||
|
|
f2df3e53dc | ||
|
|
075fb38096 | ||
|
|
dbf48330bd | ||
|
|
7b220f824e | ||
|
|
e4554d86cf | ||
|
|
7a84b8fde0 | ||
|
|
fe48bbb181 | ||
|
|
50317a3c0f | ||
|
|
127730c5cf | ||
|
|
cbbf2fbb21 | ||
|
|
b9ec3511ac | ||
|
|
f55bddb017 | ||
|
|
9a1e588af3 | ||
|
|
c1ba901f7a | ||
|
|
cf74985e56 | ||
|
|
80c4dcc91b | ||
|
|
c632402f2d | ||
|
|
2e3168bb99 | ||
|
|
d630a73433 | ||
|
|
d90e6754b5 | ||
|
|
cdf95640ae | ||
|
|
e7799331e0 | ||
|
|
dab6002b5c | ||
|
|
b3d947fc16 | ||
|
|
b0a9cd029f | ||
|
|
a783e4c94b | ||
|
|
2ca9d51339 | ||
|
|
218941d97b | ||
|
|
ff771b7bba | ||
|
|
b010bd1355 | ||
|
|
2dbe903c64 | ||
|
|
2f0c9a37e6 | ||
|
|
22403f5a45 | ||
|
|
d8a9be3d00 | ||
|
|
80179df9fc | ||
|
|
c353757d6b | ||
|
|
9f368104b5 | ||
|
|
d0b9b22879 | ||
|
|
81b1453db5 | ||
|
|
d511fd9b9e | ||
|
|
3ea61a2f07 | ||
|
|
e869d32594 | ||
|
|
3c5c38e72b | ||
|
|
8dda70ecdd | ||
|
|
b9a398830f | ||
|
|
772c6813ed | ||
|
|
ead86d523e | ||
|
|
a77c7a3296 | ||
|
|
b87bb1ccb4 | ||
|
|
c3f9ecbafd | ||
|
|
8b53afa468 | ||
|
|
28901d34b4 | ||
|
|
3c1983352a | ||
|
|
a69da95217 | ||
|
|
eaf7b9ecca | ||
|
|
e3047c8e6e | ||
|
|
e7f07a451c | ||
|
|
b1566874e1 | ||
|
|
2e87027672 | ||
|
|
2a483ea415 | ||
|
|
a1972a0103 | ||
|
|
4b5affacd0 | ||
|
|
2bd31cc06b | ||
|
|
1910c2581e | ||
|
|
501f561eae | ||
|
|
16a70d2920 | ||
|
|
ddb153f0d7 | ||
|
|
41251e2223 | ||
|
|
f7885fa6ec | ||
|
|
8a999c29dc | ||
|
|
f047d6b418 | ||
|
|
7914b07996 | ||
|
|
5b0f98264c | ||
|
|
929b0eacd3 | ||
|
|
4d24976ab9 | ||
|
|
a62ed2b6b7 | ||
|
|
2ccb52e75b | ||
|
|
dc236236be | ||
|
|
3ba508e8bb | ||
|
|
100d2d05fa | ||
|
|
21572d3377 | ||
|
|
09d0e169bc | ||
|
|
373aa0a753 | ||
|
|
ef1ad7999c | ||
|
|
f60dbfa159 | ||
|
|
4efb7f4083 | ||
|
|
d3280f8fc5 | ||
|
|
c8b8fccc5b | ||
|
|
1f001701a6 | ||
|
|
db262e18db | ||
|
|
2239d35564 | ||
|
|
5ea48578ec | ||
|
|
5a2438595e | ||
|
|
87a7e75d21 | ||
|
|
d41ee6a61a | ||
|
|
25f4354823 | ||
|
|
317eaa8655 | ||
|
|
d531a62361 | ||
|
|
2a01b4929b | ||
|
|
cc01261d96 | ||
|
|
6fbc44f0d1 | ||
|
|
495a639c8d | ||
|
|
8e5c6f91ba | ||
|
|
c08f5661b4 | ||
|
|
656a1d1d9e | ||
|
|
8f14eebe15 | ||
|
|
82b478a1fe | ||
|
|
d6214a2c40 | ||
|
|
2c13dd0312 | ||
|
|
06665326b2 | ||
|
|
bc7be2e2ad | ||
|
|
52e8d8baef | ||
|
|
d12ecc74f2 | ||
|
|
5e0f34ecb7 | ||
|
|
e2c22b260f | ||
|
|
fc74d265cd | ||
|
|
8350e9f5fc | ||
|
|
024be4307a | ||
|
|
a863754ae6 | ||
|
|
cd4ec9a23a | ||
|
|
e5b97d7750 | ||
|
|
f6460b1eda | ||
|
|
c5ddcb428c | ||
|
|
68914fceec | ||
|
|
3d18a89e55 | ||
|
|
f1dc44376a | ||
|
|
02c869cf70 | ||
|
|
8b9474262c | ||
|
|
f456c56d57 | ||
|
|
9a713e9a98 | ||
|
|
1a90b2e7b3 | ||
|
|
bc573566eb | ||
|
|
3cbbe8eb3d | ||
|
|
3136604a53 | ||
|
|
d17d308be1 | ||
|
|
115a8b8042 | ||
|
|
8cd5764428 | ||
|
|
5b537b8f9c | ||
|
|
af596560b6 | ||
|
|
adaa1ed8ac | ||
|
|
9636cc9733 | ||
|
|
86290edf6a | ||
|
|
26063e1d16 | ||
|
|
461e10a3ab | ||
|
|
2cb7a35c26 | ||
|
|
368fc1953a | ||
|
|
3d382146f4 | ||
|
|
03cc085514 | ||
|
|
1e41401ec3 | ||
|
|
78fe9d6ded | ||
|
|
0d82963ffa | ||
|
|
8c56a3a164 | ||
|
|
5972bf4db4 | ||
|
|
eaef521816 | ||
|
|
faf313b940 | ||
|
|
dccc745d51 | ||
|
|
97b9359235 | ||
|
|
00b8070699 | ||
|
|
76e60f2465 | ||
|
|
d5da0a0390 | ||
|
|
131e83ce77 | ||
|
|
71a8d06c97 | ||
|
|
c6feb67947 | ||
|
|
96a3abda1d | ||
|
|
300753f56c | ||
|
|
09743ffc3f | ||
|
|
0e20ab41c5 | ||
|
|
4ea787faf1 | ||
|
|
74ec257d96 | ||
|
|
e5e5f62504 | ||
|
|
29b6992314 | ||
|
|
0a3bd1969c | ||
|
|
ad43342687 | ||
|
|
22b02adda9 | ||
|
|
68d59842c5 | ||
|
|
c1e14b23a8 | ||
|
|
86f0e56739 | ||
|
|
4dae247b96 | ||
|
|
f881fdda30 | ||
|
|
86702e95ff | ||
|
|
6e296c6c61 | ||
|
|
9c9c57ba5c | ||
|
|
2420cec963 | ||
|
|
0f9caa09ca | ||
|
|
56988d753d | ||
|
|
146b3992ab | ||
|
|
6ac92f6274 | ||
|
|
e0ca7775a4 | ||
|
|
634a2dad9d | ||
|
|
3b16f69efe | ||
|
|
a61a10ffb9 | ||
|
|
b32fc52427 | ||
|
|
acf3dc8fc2 | ||
|
|
89db71a377 | ||
|
|
71558003a7 | ||
|
|
7211a2fc78 | ||
|
|
eccbc1bc68 | ||
|
|
f6a54980df | ||
|
|
b23d0b44f6 | ||
|
|
99c6833b0c | ||
|
|
c12194f333 | ||
|
|
179ec64a8c | ||
|
|
07cdfbb20c | ||
|
|
b6b4edeaf5 | ||
|
|
0b0e014d5e | ||
|
|
10a23cb5b8 | ||
|
|
d5b73444e5 | ||
|
|
2e51bad245 | ||
|
|
041b60a563 | ||
|
|
143aa7fa3f | ||
|
|
1b6e743dd0 | ||
|
|
4841ca549b | ||
|
|
f0687f2d5d | ||
|
|
6ba383076a | ||
|
|
d2b123066b | ||
|
|
47f19da2e2 | ||
|
|
b192e0359a | ||
|
|
a11aa15017 | ||
|
|
45a501406b | ||
|
|
26906c0585 | ||
|
|
899ec8b9f4 | ||
|
|
ab2b64c0d4 | ||
|
|
6371d9f248 | ||
|
|
a78900ce64 | ||
|
|
71cd22a12a | ||
|
|
c4628fb8c4 | ||
|
|
a7c371073b | ||
|
|
2899a1bcd4 | ||
|
|
37cb0fd876 | ||
|
|
a115a4a73c | ||
|
|
8f8e03734c | ||
|
|
747a201196 | ||
|
|
b3fd61dadb | ||
|
|
608da97538 | ||
|
|
a36d7ab1a8 | ||
|
|
6187aecab2 | ||
|
|
02d3345763 | ||
|
|
532e4724da | ||
|
|
81f3c10ca3 | ||
|
|
e5a5662303 | ||
|
|
16b22dcd40 | ||
|
|
e6580c9b26 | ||
|
|
425e5e444d | ||
|
|
3553211fc5 | ||
|
|
5848dcd71f | ||
|
|
5c370b3f57 | ||
|
|
699fb65e10 | ||
|
|
b80792d753 | ||
|
|
6d92aa52ae | ||
|
|
59e44086b1 | ||
|
|
a2040410e8 | ||
|
|
5d01a3b2fd | ||
|
|
9882dede2c | ||
|
|
59dfb2a1b1 | ||
|
|
e5f603ca8e | ||
|
|
29c70537fb | ||
|
|
c58f642574 | ||
|
|
076075a3a8 | ||
|
|
7acd668918 | ||
|
|
a50f81eaed | ||
|
|
19c3409b09 | ||
|
|
5a40b422f9 | ||
|
|
546cea38d9 | ||
|
|
e3da1628ce | ||
|
|
fee09e5116 | ||
|
|
0bf9003485 | ||
|
|
97c73a4e0c | ||
|
|
51e44d1eaf | ||
|
|
5904e96494 | ||
|
|
62d9578e1b | ||
|
|
4f7acbdafa | ||
|
|
668fa3c219 | ||
|
|
a0f701f24d | ||
|
|
ecb6549341 | ||
|
|
dc46e09888 | ||
|
|
2261b3f663 | ||
|
|
45b5cb05ba | ||
|
|
229a65b8aa | ||
|
|
9faa24fe9c | ||
|
|
62c89a81c8 | ||
|
|
2e891c9a95 | ||
|
|
70a2d8eb55 | ||
|
|
d9ebf7d9cb | ||
|
|
665151aa22 | ||
|
|
3289809a95 | ||
|
|
d9672a9231 | ||
|
|
c88e63978c | ||
|
|
939810a702 | ||
|
|
d9052de583 | ||
|
|
19ce18fa0b | ||
|
|
adc97204ff | ||
|
|
d3915c4cd3 | ||
|
|
781d92ea71 | ||
|
|
f4150e8e2c | ||
|
|
9233a9013d | ||
|
|
4cc6a63267 | ||
|
|
c598da5ef8 | ||
|
|
e7de41ce8d | ||
|
|
2d3d5f2ef0 | ||
|
|
46a7f25619 | ||
|
|
64738ea4d6 | ||
|
|
1d4f176337 | ||
|
|
8d04698637 | ||
|
|
59b960d5ce | ||
|
|
783e7038b0 | ||
|
|
0264b4c63c | ||
|
|
d9a7ac208a | ||
|
|
ed424431a7 | ||
|
|
e0cad57074 | ||
|
|
d68f49620d | ||
|
|
6af723ac40 | ||
|
|
400bdaf1ef | ||
|
|
3a0f7079fc | ||
|
|
0a2f6b971b | ||
|
|
fc61e25d73 | ||
|
|
10409ae69d | ||
|
|
91a5ce30ce | ||
|
|
a0bf3a21c7 | ||
|
|
2c178b5b49 | ||
|
|
03585c7872 | ||
|
|
f8fe78e2cb | ||
|
|
449eef2a19 | ||
|
|
e9d60543b0 | ||
|
|
e53d5555f6 | ||
|
|
c3036152f2 | ||
|
|
2697fb9f1d | ||
|
|
d435d1f535 | ||
|
|
49fde0f5c0 | ||
|
|
c1151b6af5 | ||
|
|
0259d685d2 | ||
|
|
679e726a14 | ||
|
|
03430313b5 | ||
|
|
ec98f2b33c | ||
|
|
0c9c2fccc8 | ||
|
|
60f252571a | ||
|
|
eafa63f962 | ||
|
|
c1f1c319c6 | ||
|
|
f496d8aac9 | ||
|
|
27789d37e8 | ||
|
|
9ed6399c32 | ||
|
|
a85c41383d | ||
|
|
654df39b1e | ||
|
|
8f1cfa68ce | ||
|
|
b7227f3ddc | ||
|
|
2d866aa5e4 | ||
|
|
04aa9b3004 | ||
|
|
97f7018c03 | ||
|
|
b18418ff40 | ||
|
|
bdc8cdab8c | ||
|
|
8d92e137cb | ||
|
|
c437177609 | ||
|
|
e6a4fdf21a | ||
|
|
5dedc6ed72 | ||
|
|
e41966d1fa | ||
|
|
bfdefd20d3 | ||
|
|
7aa0e67063 | ||
|
|
a8dff278e0 | ||
|
|
11ad175bae | ||
|
|
30989452a9 | ||
|
|
b541acca8e | ||
|
|
5f5a8f2036 | ||
|
|
7c632aba6f | ||
|
|
80b65bc7f3 | ||
|
|
2093f809c7 | ||
|
|
155a6579aa | ||
|
|
3a7fbe81fb | ||
|
|
cab376d91c | ||
|
|
999969a78d | ||
|
|
156bff395d | ||
|
|
7f201fe08e | ||
|
|
7f0561df35 | ||
|
|
10be42a9c8 | ||
|
|
63562013af | ||
|
|
6e6e0f5e4b | ||
|
|
5311fc2d51 | ||
|
|
61a0c9e301 | ||
|
|
175b5b7bb5 | ||
|
|
c5000a5950 | ||
|
|
1b6ea9dcbe | ||
|
|
78f5e8c570 | ||
|
|
a423bddc63 | ||
|
|
c927c831c5 | ||
|
|
b2cf27ef68 | ||
|
|
be9cdb8ed8 | ||
|
|
a36521f07c | ||
|
|
7695bacfe2 | ||
|
|
84510b2f57 | ||
|
|
e3d37c4391 | ||
|
|
edab790d0d | ||
|
|
fd76a06942 | ||
|
|
89acc2cd4b | ||
|
|
494bd0b04b | ||
|
|
b4e6bb865c | ||
|
|
22d54be408 | ||
|
|
117abf7df0 | ||
|
|
0c73cdddc2 | ||
|
|
2773368df1 | ||
|
|
f30c1aa17f | ||
|
|
fa9c4738e0 | ||
|
|
4a9e8460c0 | ||
|
|
cea36979d9 | ||
|
|
eb69c3473e | ||
|
|
d6e3be000b | ||
|
|
951f62832a | ||
|
|
7e2f20bba9 | ||
|
|
675237e16d | ||
|
|
f6f6ea4493 | ||
|
|
d48a905077 | ||
|
|
e909cbdc94 | ||
|
|
4e59043761 | ||
|
|
8752d7c5ef | ||
|
|
43ed761fae | ||
|
|
fc22c24fa0 | ||
|
|
ddd0556030 | ||
|
|
de2c1e1011 | ||
|
|
2e43240f51 | ||
|
|
bc6b5249c4 | ||
|
|
1973664e0a | ||
|
|
4025cf6538 | ||
|
|
a33b249e28 | ||
|
|
a8e48929ea | ||
|
|
adc7fbd7b8 | ||
|
|
2869c9a27c | ||
|
|
9ed61a8a14 | ||
|
|
67a0efc0a5 | ||
|
|
63e3ab1504 | ||
|
|
cb0f69d6c0 | ||
|
|
32df0f8c84 | ||
|
|
baea745394 | ||
|
|
026fab35c9 | ||
|
|
fabc7ebdb4 | ||
|
|
b24fcf26ca | ||
|
|
018feadda3 | ||
|
|
b1c8ef2e48 | ||
|
|
b5506df5d1 | ||
|
|
3150fea488 | ||
|
|
735a0edd1c | ||
|
|
615b0013f4 | ||
|
|
0a85348fbd | ||
|
|
79618eb594 | ||
|
|
36f3f6e910 | ||
|
|
058272ecbf | ||
|
|
2c15734c11 | ||
|
|
b870272eb5 | ||
|
|
e18773d3aa | ||
|
|
ab954f3505 | ||
|
|
53ded89075 | ||
|
|
28ea27f119 | ||
|
|
5e722abde4 | ||
|
|
f4edd3e780 | ||
|
|
c18b54a92c | ||
|
|
7bd796b04a | ||
|
|
fe2c7f8249 | ||
|
|
edb1f30451 | ||
|
|
7a9ec16300 | ||
|
|
1c38d05b1d | ||
|
|
df55db0bc7 | ||
|
|
e1781983aa | ||
|
|
a1228df9ea | ||
|
|
3b18d3beab | ||
|
|
e8d2eb7509 | ||
|
|
a35ad4776a | ||
|
|
b0a7ec9591 | ||
|
|
bb3445fdd1 | ||
|
|
83b12fc11f | ||
|
|
2cfa2eede0 | ||
|
|
1edfaec702 | ||
|
|
03f87e2dcb | ||
|
|
12b454505e | ||
|
|
1c9bd11d20 | ||
|
|
ebc261a730 | ||
|
|
ea82b2c66f | ||
|
|
a0e9913311 | ||
|
|
1cfe54ca17 | ||
|
|
9115c519e1 | ||
|
|
fdcfbae436 | ||
|
|
c673fa18e3 | ||
|
|
f6b045d5ae | ||
|
|
081e1f6cc4 | ||
|
|
cf0fc7c6ec | ||
|
|
8c9c662aeb | ||
|
|
204ad40e76 | ||
|
|
34077ca512 | ||
|
|
329d793705 | ||
|
|
ad80419fae | ||
|
|
393893c295 | ||
|
|
0eece49dc0 | ||
|
|
cb2466c243 | ||
|
|
e0fbc8c988 | ||
|
|
f8e9d6855e | ||
|
|
5e6fc49bb7 | ||
|
|
f3e7d93aa3 | ||
|
|
1b4ea170aa | ||
|
|
ff4950b69e | ||
|
|
c1215a4617 | ||
|
|
9ae3dad2d0 | ||
|
|
299d737da8 | ||
|
|
694230e30c | ||
|
|
5825c52ad3 | ||
|
|
bb7f4260cf | ||
|
|
5d13d2550b | ||
|
|
ccfe925b5d | ||
|
|
e1c4d5dc33 | ||
|
|
5426a3782c | ||
|
|
386efdbeb2 | ||
|
|
5bf8f64d15 | ||
|
|
0c72819a12 | ||
|
|
23cf041ee7 | ||
|
|
36763ca629 | ||
|
|
f2af9582af | ||
|
|
5ab182a61e | ||
|
|
f3eadddcb3 | ||
|
|
89da950c07 | ||
|
|
bae1563a98 | ||
|
|
6172ead37c | ||
|
|
c1bbdee0b9 | ||
|
|
df257bde69 | ||
|
|
975ecee94a | ||
|
|
35e08a6151 | ||
|
|
bfcc1bdc39 | ||
|
|
5d8e136f12 | ||
|
|
11ea3e168b | ||
|
|
a789124f10 | ||
|
|
c29e25b880 | ||
|
|
6c407f208b | ||
|
|
9c755282ce | ||
|
|
a866e0dffe | ||
|
|
d0af5d85dc | ||
|
|
be49070962 | ||
|
|
3346804952 | ||
|
|
58471baaf6 | ||
|
|
72b4581f77 | ||
|
|
11be446db2 | ||
|
|
f035863350 | ||
|
|
3e67badaf9 | ||
|
|
c9405db975 | ||
|
|
3c97d2b774 | ||
|
|
f178e9cceb | ||
|
|
5734fca012 | ||
|
|
d37bfcdde1 | ||
|
|
8cb9252115 | ||
|
|
fcc94ee37e | ||
|
|
a98caca397 | ||
|
|
3c78dbf994 | ||
|
|
05fe0cc783 | ||
|
|
149870943d | ||
|
|
cf44f1c371 | ||
|
|
29fe5aaeb2 | ||
|
|
5f5dc2ead7 | ||
|
|
071021fe54 | ||
|
|
4982b37864 | ||
|
|
30b1c5740d | ||
|
|
37b2e89b49 | ||
|
|
cec4b449ad | ||
|
|
50b8f61a69 | ||
|
|
cf54f94ba5 | ||
|
|
5332202f9f | ||
|
|
883d896406 | ||
|
|
122df58236 | ||
|
|
6c5ee5e512 | ||
|
|
0300858fdf | ||
|
|
611b7842c0 | ||
|
|
1cbfeba4a6 | ||
|
|
d598f30a0c | ||
|
|
8a28af209d | ||
|
|
f455dc47de | ||
|
|
f4dec4e1a9 | ||
|
|
b1f10cd37c | ||
|
|
553329ac80 | ||
|
|
921e0a1503 | ||
|
|
a6fb2d8380 | ||
|
|
0af59ed31b | ||
|
|
d02f27b28f | ||
|
|
b59e6a241f | ||
|
|
135cab0990 | ||
|
|
f6f717eae8 | ||
|
|
708e0679c2 | ||
|
|
397767604d | ||
|
|
0df9f12c2c | ||
|
|
26b1d33eca | ||
|
|
ad344c27eb | ||
|
|
8510049293 | ||
|
|
9b0bf2f31a | ||
|
|
354a154fee | ||
|
|
dd4dc279e1 | ||
|
|
91a9585b68 | ||
|
|
9b799dd838 | ||
|
|
c26113a6ec | ||
|
|
960f867744 | ||
|
|
6d89e1c2c3 | ||
|
|
70594a287d | ||
|
|
4629eea670 | ||
|
|
2b0f3758bb | ||
|
|
614e568763 | ||
|
|
e0bc1db6bf | ||
|
|
9c04e39aaf | ||
|
|
a9ec724127 | ||
|
|
2f59b46444 | ||
|
|
906fd5b8e6 | ||
|
|
98e89f1573 | ||
|
|
66928576ac | ||
|
|
5492ba316b | ||
|
|
772079750d | ||
|
|
cec5e2fcb2 | ||
|
|
b3d4a6f2ed | ||
|
|
391a085c2c | ||
|
|
b234134334 | ||
|
|
a0d597e14e | ||
|
|
3926fa4f26 | ||
|
|
443f56b099 | ||
|
|
9c696e1947 | ||
|
|
8cc0dc5218 | ||
|
|
c172ce87bc | ||
|
|
35ad68bf5e | ||
|
|
caa5ca3c34 | ||
|
|
e699dbb4b6 | ||
|
|
d42f4311e6 | ||
|
|
d7d9317977 | ||
|
|
13f8d94224 | ||
|
|
720644869c | ||
|
|
47a83a9f16 | ||
|
|
350936d3c3 | ||
|
|
864b07c061 | ||
|
|
14ded3cab8 | ||
|
|
0eaa0ee023 | ||
|
|
708d28cd94 | ||
|
|
1188165fa1 | ||
|
|
2ba5062846 | ||
|
|
c9dd228c4b | ||
|
|
a62c1ee3b9 | ||
|
|
6702fd2995 | ||
|
|
adba625280 | ||
|
|
0d24fb3cef | ||
|
|
1093772945 | ||
|
|
3a17165562 | ||
|
|
534edddd11 | ||
|
|
102a68ceb4 | ||
|
|
7eef92219f | ||
|
|
9578c86201 | ||
|
|
ff4f2bc962 | ||
|
|
6d721d7b9a | ||
|
|
ac03ec27de | ||
|
|
c5733b8377 | ||
|
|
a77c87df35 | ||
|
|
80e343b2d3 | ||
|
|
c613a98b61 | ||
|
|
2a185e6e79 | ||
|
|
714036c751 | ||
|
|
5613f349a7 | ||
|
|
e88bb2c8a5 | ||
|
|
7eb044900c | ||
|
|
531a84f5ab | ||
|
|
c144d836e9 | ||
|
|
17740ac822 | ||
|
|
2bd861527f | ||
|
|
1fad12d718 | ||
|
|
1cff836933 | ||
|
|
9b5f2d9ee8 | ||
|
|
710a208ba5 | ||
|
|
3cb228fc16 | ||
|
|
3e860e0472 | ||
|
|
251930b81d | ||
|
|
09c4e75f0e | ||
|
|
30698a9480 | ||
|
|
875e8f3fc0 | ||
|
|
7bc7008799 | ||
|
|
1af2c401d3 | ||
|
|
dd4661ff6e | ||
|
|
394d2c2ce6 | ||
|
|
381999d69b | ||
|
|
4139f712cc | ||
|
|
013256dfbd | ||
|
|
e3217c6bc1 | ||
|
|
853d22d044 | ||
|
|
7b62b0e291 | ||
|
|
13b94e4263 | ||
|
|
62eaf50800 | ||
|
|
d5a1427fff | ||
|
|
acd7756b2f | ||
|
|
31391ee27e | ||
|
|
0fdd34ec24 | ||
|
|
d727459273 | ||
|
|
0d2c648be4 | ||
|
|
af601ddb01 | ||
|
|
0a813f2557 | ||
|
|
f3033b0886 | ||
|
|
05524721f3 | ||
|
|
286c4c743c | ||
|
|
26b7e9e8d1 | ||
|
|
97986fd530 | ||
|
|
2c2baf4b64 | ||
|
|
b2aaf2acc0 | ||
|
|
9bdc6ff0bd | ||
|
|
ac24f0546c | ||
|
|
882e112a2a | ||
|
|
88fd9bc231 | ||
|
|
509c5cf730 | ||
|
|
1b90df2b20 | ||
|
|
7de12951b7 | ||
|
|
2cb01feafd | ||
|
|
f4f956bc8c | ||
|
|
c6b53f4cb8 | ||
|
|
cdd64ade24 | ||
|
|
815ea75745 | ||
|
|
fe139898e4 | ||
|
|
c4caf7fc8e | ||
|
|
15207d11fa | ||
|
|
03bdc6860f | ||
|
|
5ec794e13e | ||
|
|
874e67a185 | ||
|
|
d6f9a861fc | ||
|
|
f4d93ddaa2 | ||
|
|
2e9f163c3a | ||
|
|
c2f0898784 | ||
|
|
35ef013916 | ||
|
|
fce3f46a04 | ||
|
|
1954226df7 | ||
|
|
7b94f30108 | ||
|
|
18dc1aa8ed | ||
|
|
e2a97af404 | ||
|
|
1c055d97c5 | ||
|
|
ca2eb8c4a6 | ||
|
|
e3fa838b39 | ||
|
|
d374470151 | ||
|
|
f36c020902 | ||
|
|
6eb624f8e4 | ||
|
|
351a14d3ad | ||
|
|
a5fe03b721 | ||
|
|
0bfa19ae9c | ||
|
|
5ae0b39192 | ||
|
|
f12832ee69 | ||
|
|
f49b0c6036 | ||
|
|
d6b72c7060 | ||
|
|
88182d671b | ||
|
|
1f767cc76f | ||
|
|
3740bec0d2 | ||
|
|
72b49620d8 | ||
|
|
4d137e8734 | ||
|
|
74ccbb3a1e | ||
|
|
6796944dc8 | ||
|
|
6ac6c99811 | ||
|
|
d936895438 | ||
|
|
a5ea1c1889 | ||
|
|
ce6c403637 | ||
|
|
83f02257d1 | ||
|
|
b2fea563ca | ||
|
|
fec144e6a9 | ||
|
|
eda6600a6f | ||
|
|
b8cf682edc | ||
|
|
285b1fb2d9 | ||
|
|
2704d33b6e | ||
|
|
57279aa91f | ||
|
|
5e1efa7a64 | ||
|
|
562cf5bba3 | ||
|
|
5fb0028b31 | ||
|
|
33c2e2899f | ||
|
|
5a0593e952 | ||
|
|
e5175cabb0 | ||
|
|
94432e04d3 | ||
|
|
2a84f66951 | ||
|
|
6b4dcbdd31 | ||
|
|
2e42fb220f | ||
|
|
ecc294e79f | ||
|
|
9dbb61cdd1 | ||
|
|
2d789c596c | ||
|
|
c36d77fba8 | ||
|
|
d4c20518b3 | ||
|
|
8f1751c917 | ||
|
|
f0b0c699ef | ||
|
|
cb3d8434e0 | ||
|
|
14c632d063 | ||
|
|
169a8df718 | ||
|
|
3b891b5460 | ||
|
|
c180cf2b18 | ||
|
|
e7e2a9368d | ||
|
|
b26a30a8ae | ||
|
|
24c65f4da8 | ||
|
|
0eda70edd8 | ||
|
|
3ee7122380 | ||
|
|
829f828bbe | ||
|
|
455686e1e3 | ||
|
|
68a17b1dff | ||
|
|
319d61e41a | ||
|
|
dcfa96206a | ||
|
|
5f371c7a70 | ||
|
|
35740a7bc5 | ||
|
|
5fc719ce88 | ||
|
|
3d9aca430d | ||
|
|
f802d65490 | ||
|
|
77a78f4938 | ||
|
|
17c915f0a6 | ||
|
|
4f8d18f454 | ||
|
|
5fbec8a7c9 | ||
|
|
12a90477f7 | ||
|
|
a611640927 | ||
|
|
c58c7aaa22 | ||
|
|
55d92d4e75 | ||
|
|
62da3f6993 | ||
|
|
79cc190a0f | ||
|
|
7ba1e86410 | ||
|
|
713b8986c9 | ||
|
|
6a4b8775de | ||
|
|
f3d38be04b | ||
|
|
5ea46cdc68 | ||
|
|
7f69e8271a | ||
|
|
6897c38f97 | ||
|
|
e973b1a0a1 | ||
|
|
a7ad991c59 | ||
|
|
984ae53af3 | ||
|
|
df05999c0f | ||
|
|
b9476733fc | ||
|
|
0b5ae56dd5 | ||
|
|
3972ca2edd | ||
|
|
100e90a3be | ||
|
|
e1b78d602e | ||
|
|
e7d2c8626c | ||
|
|
034817f119 | ||
|
|
5e5687dcc1 | ||
|
|
05d90fe57e | ||
|
|
a2e9335fc5 | ||
|
|
f27aa44ec5 | ||
|
|
03d4ee83e1 | ||
|
|
1b992cb89a | ||
|
|
b1deae4c73 | ||
|
|
e333f68cc3 | ||
|
|
1a8367a9da | ||
|
|
582d2ee8cb | ||
|
|
307d92b7f5 | ||
|
|
311cd74358 | ||
|
|
61a2834710 | ||
|
|
cb3b38a952 | ||
|
|
3768e5d601 | ||
|
|
3724764079 | ||
|
|
655a882cf0 | ||
|
|
2ff31032c6 | ||
|
|
90ff111283 | ||
|
|
af27eff911 | ||
|
|
5745abf0ae | ||
|
|
d5aba614fe | ||
|
|
16679896cf | ||
|
|
d1ddf2de7a | ||
|
|
1608b8f7ad | ||
|
|
5cc335acde | ||
|
|
bbdb63a9be | ||
|
|
1d4e8cffc2 | ||
|
|
71849d4aab | ||
|
|
2f02d72565 | ||
|
|
28121b750d | ||
|
|
b9698b1fbb | ||
|
|
f0fef3f906 | ||
|
|
9f4e46d06d | ||
|
|
aa2ce7910b | ||
|
|
64cd9d67a5 | ||
|
|
bc945b3328 | ||
|
|
b02bb244ec | ||
|
|
df580ae8bd | ||
|
|
bd777856e6 | ||
|
|
ac79549aca | ||
|
|
4b2e1c435c | ||
|
|
0e923eadff | ||
|
|
d604ee0474 | ||
|
|
1dca970937 | ||
|
|
3799702d22 | ||
|
|
8fc529d2eb | ||
|
|
9961f8a1b9 | ||
|
|
921c55c97f | ||
|
|
b60a0a45ab | ||
|
|
4994957194 | ||
|
|
d7278d5fff | ||
|
|
d6b0960272 | ||
|
|
ffd53ec713 | ||
|
|
778d03c903 | ||
|
|
a61d1a5dd7 | ||
|
|
3fa86786e8 | ||
|
|
5eba6797ab | ||
|
|
2551018e22 | ||
|
|
46f43eebdf | ||
|
|
ba226085a2 | ||
|
|
c4ffbcba0b | ||
|
|
f5a3a6940a | ||
|
|
90856bbe3a | ||
|
|
d5cafaa699 | ||
|
|
4bdb82b7ed | ||
|
|
9eff0cb944 | ||
|
|
4589acfed2 | ||
|
|
d43feb85d7 | ||
|
|
9ce35b00a2 | ||
|
|
46a143b4dc | ||
|
|
441c14a97f | ||
|
|
52fb2ff638 | ||
|
|
4a2731d56e | ||
|
|
2c46f40a38 | ||
|
|
993675ef3f | ||
|
|
863e4a470d | ||
|
|
cfe38da760 | ||
|
|
40e992acf6 | ||
|
|
645130a042 | ||
|
|
a30151a24a | ||
|
|
8f8ea08155 | ||
|
|
8f9dffa3c6 | ||
|
|
59689010ed | ||
|
|
7b8142e1f6 | ||
|
|
5acf2d3d9e | ||
|
|
4f66a42cec | ||
|
|
96e7a4db8d | ||
|
|
03ac72700b | ||
|
|
14d7ddcd4e | ||
|
|
d5834de00e | ||
|
|
ea14712dcc | ||
|
|
d5b5668154 | ||
|
|
8faa646bb0 | ||
|
|
8e8c713675 | ||
|
|
c282dd5ff1 | ||
|
|
46c0cfadca | ||
|
|
b8ea7291a6 | ||
|
|
cbfad8de93 | ||
|
|
9f834babda | ||
|
|
eb24df17c9 | ||
|
|
8ccf70f6a7 | ||
|
|
7dcee03161 | ||
|
|
a5be04560e | ||
|
|
76feacb340 | ||
|
|
4124f5bac8 | ||
|
|
da1b05fe6c | ||
|
|
b5a400b5e9 | ||
|
|
8db587a4b3 | ||
|
|
6cfd123a70 | ||
|
|
3afc247c67 | ||
|
|
64b715f416 | ||
|
|
49c2fe97df | ||
|
|
c413220fd0 | ||
|
|
bb28d31c4b | ||
|
|
be00ce9179 | ||
|
|
337f3cfa09 | ||
|
|
d45f394e3b | ||
|
|
97dd9b5bfe | ||
|
|
faf11a3c15 | ||
|
|
86c27c772c | ||
|
|
2ad2994f22 | ||
|
|
dac76aabbe | ||
|
|
a962c8d1c7 | ||
|
|
27d43c3e11 | ||
|
|
63926a667f | ||
|
|
b829efbaab | ||
|
|
6e71151104 | ||
|
|
007dd0a986 | ||
|
|
169d1a6d8b | ||
|
|
99b56d308f | ||
|
|
59a1106035 | ||
|
|
7276abf589 | ||
|
|
3ba9c451a5 | ||
|
|
78a693621e | ||
|
|
b430a2cdcb | ||
|
|
6c2c90989b | ||
|
|
cac624007c | ||
|
|
53742ad094 | ||
|
|
0419679c5d | ||
|
|
9cd10c4b1e | ||
|
|
d8b85d071a | ||
|
|
300130983d | ||
|
|
0c43017c45 | ||
|
|
fa6c0d20aa | ||
|
|
3d46e8d65a | ||
|
|
ad0081f692 | ||
|
|
3a8de95bf1 | ||
|
|
7a84f14f8d | ||
|
|
2d29a50e2b | ||
|
|
0c1b6c1dc2 | ||
|
|
5732a1ab39 | ||
|
|
af03f16690 | ||
|
|
f33659a893 | ||
|
|
6cd4ab5a60 | ||
|
|
c37e80fb02 | ||
|
|
af2b5640c8 | ||
|
|
71598ec0f3 | ||
|
|
0443b33705 | ||
|
|
c2f8cee9f0 | ||
|
|
ab9cd0fad5 | ||
|
|
922dbfa0bf | ||
|
|
a8598a1d80 | ||
|
|
651342e49d | ||
|
|
1ca244a116 | ||
|
|
fdf5d19480 | ||
|
|
e52ad139f9 | ||
|
|
ff2d99ae70 | ||
|
|
d7b47c21f5 | ||
|
|
5adaec1376 | ||
|
|
f3140692ff | ||
|
|
2239b7b186 | ||
|
|
7a5dce6988 | ||
|
|
7835d69c9f | ||
|
|
f5a6005b3e | ||
|
|
27ba97800f | ||
|
|
768b988843 | ||
|
|
e8933ab8ac | ||
|
|
7a3fd5c42f | ||
|
|
05f1b0af3a | ||
|
|
5d31269736 | ||
|
|
aeed70f90f | ||
|
|
a0eb15b980 | ||
|
|
8ca0691e02 | ||
|
|
b75de86db1 | ||
|
|
d09a201a6a | ||
|
|
77ac8f06c0 | ||
|
|
840236610c | ||
|
|
575d5aa039 | ||
|
|
467e4182f2 | ||
|
|
4b8e1d754a | ||
|
|
d3093aead7 | ||
|
|
338bfb83e7 | ||
|
|
a5f2fd01dd | ||
|
|
59a52c8b95 | ||
|
|
f3c2b9bbee | ||
|
|
369cf8ecf4 | ||
|
|
1892a5d579 | ||
|
|
805f8db179 | ||
|
|
b2837c6426 | ||
|
|
0765c0596b | ||
|
|
f620762a4b | ||
|
|
86df630894 | ||
|
|
743d480b7c | ||
|
|
2194824e6c | ||
|
|
d650e2f562 | ||
|
|
cfc3e5cc0e | ||
|
|
0c2b05ce74 | ||
|
|
704a0b91ac | ||
|
|
61da20e9b6 | ||
|
|
4f2414ef5c | ||
|
|
855ab98ce5 | ||
|
|
54ef4c79f3 | ||
|
|
839269ab8e | ||
|
|
d38a373297 | ||
|
|
fc6241ff46 | ||
|
|
26e40b0e71 | ||
|
|
6cc339cdb5 | ||
|
|
cc7f3c4b8e | ||
|
|
4737184f59 | ||
|
|
8e0addb76c | ||
|
|
74cf5b0005 | ||
|
|
3da51e926b | ||
|
|
4974869f6a | ||
|
|
16d7f42506 | ||
|
|
4ca58ffaa2 | ||
|
|
e26f958de0 | ||
|
|
b4f088d70a | ||
|
|
3cd1102cd2 | ||
|
|
d6ac21db0e | ||
|
|
7459f2aef1 | ||
|
|
67bdc09e1e | ||
|
|
90bfa4b968 | ||
|
|
92af7ea29a | ||
|
|
40e5f238a4 | ||
|
|
5bed29f6e9 | ||
|
|
6f09b28196 | ||
|
|
7f41d0bca5 | ||
|
|
5de6de67e6 | ||
|
|
e4809ef843 | ||
|
|
d829ea6c05 | ||
|
|
8f6f61858e | ||
|
|
cbe545f3e3 | ||
|
|
d9db45ea3a | ||
|
|
6416408031 | ||
|
|
f5d8f450b3 | ||
|
|
849e74118d | ||
|
|
01eb1647bc | ||
|
|
5cb25981ef | ||
|
|
2ccaab0e72 | ||
|
|
8d0930a5b2 | ||
|
|
2764ac1e31 | ||
|
|
aeb6919da6 | ||
|
|
79106b69da | ||
|
|
d16a2a201a | ||
|
|
14db8a7816 | ||
|
|
252c744ddd | ||
|
|
b6c2a8ec62 | ||
|
|
b22ff2084c | ||
|
|
91340f5d5f | ||
|
|
6a362bcdde | ||
|
|
67ff6fba96 | ||
|
|
12a0f26bb7 | ||
|
|
2ca459e027 | ||
|
|
de045f4a56 | ||
|
|
e2a6dd1487 | ||
|
|
b086e02dcd | ||
|
|
4e5939071a | ||
|
|
5f2033ea90 | ||
|
|
02ce03aeaa | ||
|
|
8fd40e0846 | ||
|
|
e41e93ede8 | ||
|
|
73655af744 | ||
|
|
b77fcb3351 | ||
|
|
6fee15d4aa |
217
AGENTS.md
217
AGENTS.md
@ -1,102 +1,167 @@
|
||||
# AGENTS.md
|
||||
|
||||
适用范围:`D:\AI Code\3danzhuang-wuyongjie-mainline` 全仓库。
|
||||
未单独说明时,先遵循全局规则:`C:\Users\54972\.codex\AGENTS.md`。
|
||||
这份文件只补充本仓库特有规则,不重复全局通用要求。
|
||||
未单独说明时,先遵循当前 agent 自己的全局规则(ZCode:`C:\Users\54972\.zcode\v2\AGENTS.md`;Codex:`C:\Users\54972\.codex\AGENTS.md`)。
|
||||
这份文件只记录本仓库当前有效的补充规则和核查入口,不保存密码、密钥、数据库连接串或对象存储密钥。
|
||||
|
||||
重要边界:`D:\AI Code\3danzhuang` 是历史旧现场;当前正式工作目录以 `D:\AI Code\3danzhuang-wuyongjie-mainline` 为准,除非用户明确要求,不要混用旧目录。
|
||||
重要边界:`D:\AI Code\3danzhuang` 是历史旧现场;当前正式工作目录以 `D:\AI Code\3danzhuang-wuyongjie-mainline` 为准。除非用户明确要求,不要混用旧目录、旧服务器别名、旧部署脚本或旧截图结论。
|
||||
|
||||
## 1. 默认执行口径
|
||||
## 1. 当前项目真相
|
||||
|
||||
- 用户未指定目录时,默认在 `furniture-3d-system/` 执行。
|
||||
- 当任务发生在 `furniture-3d-system/` 时,默认按当前会话直接开发。
|
||||
- 常规任务分支在完成必要验证、确认范围和目标后,可以按团队协作标准流程主动 `commit / push`。
|
||||
- `main`、生产发布、破坏性操作、删除数据、改远端历史等高风险动作仍必须由用户明确授权。
|
||||
- 禁止破坏性操作,除非用户明确授权。
|
||||
- 没有“规范审查结论”,任务不算完成。
|
||||
- 外层目录 `D:\AI Code\3danzhuang-wuyongjie-mainline` 是 Git 仓库根目录。
|
||||
- 主应用目录是 `furniture-3d-system/`;用户未指定目录时,默认在这里执行代码、测试、构建和脚本。
|
||||
- 技术栈以当前 `furniture-3d-system/package.json` 为准(核心:Next.js App Router + React + TypeScript + Prisma/PostgreSQL + Jest + Playwright + Tailwind CSS + Three.js/R3F;另有 ioredis、puppeteer、exceljs/xlsx、zod、zustand 等)。package.json 变动后文档不自动同步,以实时核查为准。
|
||||
- `CLAUDE.md`、旧 README、`docs/历史归档/` 和早期部署文档里可能仍有旧苹果风格、MinIO、旧 Docker 部署、旧模型数量、旧 Product/SKU 口径等信息;只能当历史参考,不能覆盖当前代码、workflow 文档和实时命令结果。
|
||||
- 工作树可能有用户或其他协作者留下的未跟踪文件、日志、贴图和本地产物。只处理本次任务明确相关的文件,不要顺手清理或回退无关现场。
|
||||
|
||||
## 2. 三条硬规则
|
||||
## 2. 开工前固定入口
|
||||
|
||||
- 必须符合企业级架构标准。
|
||||
- 必须符合统一视觉标准。
|
||||
- 必须优先做根因修复,不能拿补丁冒充完成。
|
||||
|
||||
## 3. 固定协作入口
|
||||
|
||||
当任务发生在 `furniture-3d-system/` 时,人和 AI 都必须先读取下面 3 份协作文档,再开始改代码:
|
||||
当任务发生在 `furniture-3d-system/` 时,改代码前必须先读这 3 份当前协作入口:
|
||||
|
||||
- `furniture-3d-system/docs/workflow/团队协作模式.md`
|
||||
- `furniture-3d-system/docs/workflow/模块边界说明.md`
|
||||
- `furniture-3d-system/docs/workflow/主线同步操作手册.md`
|
||||
|
||||
如果任务和包装协作有关,再额外读取:
|
||||
按任务类型额外读取:
|
||||
|
||||
- `furniture-3d-system/docs/workflow/同事交接模板.md`
|
||||
- `furniture-3d-system/docs/workflow/同事本地AGENTS模板.md`
|
||||
- `furniture-3d-system/docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md`
|
||||
- 生产、部署、线上排障:`furniture-3d-system/docs/workflow/生产部署入口说明.md`,并核对 `scripts/deploy-yydd-prod-from-git.ps1` 与其调用的 `scripts/create-yydd-prod-deploy.ps1` 当前默认参数。
|
||||
- 前端、视觉、交互:`furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` 和 `furniture-3d-system/docs/workflow/规范审查清单.md`。
|
||||
- 包装、仓库、轻仓库协作:再读包装/仓库相关交接文档和 `src/app/warehouse`、`src/app/api/warehouse`、`src/app/api/pre-skus/[id]/packaging/route.ts` 的当前实现。
|
||||
- 数据库、主数据、迁移:优先读 `docs/架构设计/内部主数据巡检基线-20260529.md`、`docs/架构设计/数据库混乱点治理台账.md` 和相关 `db:foundation:*` 脚本说明。
|
||||
|
||||
## 4. 协作与分支口径
|
||||
如果文档和代码冲突,按这个顺序判断:当前代码和脚本、当前线上/命令返回、当前 workflow 文档、历史文档或旧记忆。
|
||||
|
||||
- `main` 是最终发布主线,不能随便直接推;生产发布必须另行确认。
|
||||
- `origin/codex/xuxiake-mainline` 是当前共享开发主线和默认同步基线。
|
||||
## 3. Git 与主线协作
|
||||
|
||||
- `main` 是最终发布主线,由 `WuYongJie` 收口;不要直接把半成品推到 `main`。
|
||||
- `origin/codex/xuxiake-mainline` 是日常共享开发主线和默认同步基线。
|
||||
- 新功能、优化和修复默认从最新 `origin/codex/xuxiake-mainline` 开任务分支,再按协作流程回流。
|
||||
- 新分支统一使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
|
||||
- 不再使用 `feature/packaging`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
|
||||
- 长期共享分支同步当前共享开发主线时固定使用 `merge`。
|
||||
- 临时个人任务分支只有在确认无人共用时才使用 `rebase`。
|
||||
- 新分支统一使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`,不要再新建 `feature/...`、`packaging/...`、`dev`、`codex/xuxiake-dev` 这类泛分支名。
|
||||
- 长期共享分支同步共享开发主线时固定使用 `merge origin/codex/xuxiake-mainline`;只有确认无人共用的临时个人任务分支才使用 `rebase origin/codex/xuxiake-mainline`。
|
||||
- 每次开工或接续旧任务时,先看 `git status --short --branch`;涉及主线同步时再看 `git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline`,不要把“当前所在分支”误当成“已同步共享主线”。
|
||||
- 用户问“xuxiake 主线有没有新提交”时,必须先 `git fetch origin`,再基于最新远端状态回答。
|
||||
- 不要在脏工作树上直接 merge/rebase。先看 `git status --short`,分清本次改动、用户改动、本地产物和无关未跟踪文件。
|
||||
- 常规任务完成必要验证后,可以按协作流程推进 `commit / push`;`main`、生产发布、破坏性操作、删除数据、改远端历史等高风险动作必须有用户明确授权。
|
||||
|
||||
## 5. 白曜石工坊视觉口径
|
||||
### 主线部署标准流程(每次"主线部署"都走这一套)
|
||||
|
||||
- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md` 是当前新平台第一视觉标准。
|
||||
- 新平台主链页面,以及 `/admin`、`/users` 等后台管理页面,默认都要向白曜石工坊口径统一。
|
||||
- 白曜石工坊默认是浅色工作台;不再提供深浅色主题切换入口,也不要把后台页面继续做成默认黑底。
|
||||
- 页面应保持工作台感、低噪音、克制、层级清楚,不能退回普通 SaaS 卡片墙或营销式大卡片布局。
|
||||
- `Inspector(检视器)`、数据列表、表格、长文本和表单密集区必须优先保证可读性,不能用毛玻璃承载正文。
|
||||
- 毛玻璃只允许用于顶栏、HUD、浮动工具条和轻量状态浮层。
|
||||
- 板材、五金、组件、包装等资产应按 `Meta Component(元组件)` 表达,而不是普通图片卡片。
|
||||
- 苹果风格视觉标准已降级为历史参考,不再作为新平台主链、后台管理页面、弹窗或验收的视觉依据。
|
||||
本仓库有两根主线,都不直接提交:`main` 是生产部署唯一来源(由 `WuYongJie` 收口),`origin/codex/xuxiake-mainline` 是共享开发次主线(日常回流基线)。
|
||||
|
||||
## 6. 前端责任口径
|
||||
每次用户要求"主线部署",严格按以下顺序,不跳步、不直接支线 → main:
|
||||
|
||||
- 未明确指定其他协作者时,前端、视觉、交互、`3D(立体)` 表现层和系统联调默认由当前 Codex 全权负责。
|
||||
- 前端实现必须遵守白曜石工坊视觉标准、模块边界和最小验证要求。
|
||||
- 处理前端页面时,优先用当前代码、实际页面表现、截图结论和设计规范判断,不再套用旧模型时期的保守分工。
|
||||
- 复杂任务可以主动拆计划、并行排查和验证,但不能因为模型更强就扩大改动范围或跳过规范审查。
|
||||
1. **在支线开发** → 提交到任务分支(如 `codex/wuyongjie-xxx`),不碰两根主线。
|
||||
2. **fetch xuxiake** → 必须先 `git fetch origin`,看 `origin/codex/xuxiake-mainline` 有没有新提交。
|
||||
3. **有新提交** → `merge origin/codex/xuxiake-mainline` 拉下来处理冲突(长期共享分支用 merge,不 rebase);只处理冲突文件,不扩范围。
|
||||
4. **合并到 xuxiake** → 把支线合进 `origin/codex/xuxiake-mainline`,并 `push` 更新次主线。
|
||||
5. **合并到 main** → 把 xuxiake 合进 `main`,并 `push origin main`(本步需用户授权)。
|
||||
6. **用 main 部署** → 走 `scripts/deploy-yydd-prod-from-git.ps1`(默认源 `origin/main`),部署后核对 commit / 进程 / 端口 / 日志 / 关键页面(本步需用户授权)。
|
||||
|
||||
## 7. 截图和上下文续接
|
||||
发布前必须确认 `git ls-remote origin refs/heads/main`、本地 `origin/main`、目标部署 commit 三者一致。
|
||||
|
||||
- 工程对话可以正常使用截图,但截图只用于当前判断。
|
||||
- 压缩上下文、写会话恢复摘要或给新对话续接时,只保留文字结论、关键决策、当前状态和下一步。
|
||||
- 不继承、不复述 `data:image base64(图片编码文本)`,也不要把图片原始内容写进摘要。
|
||||
- 如果同一问题需要连续大量截图,优先开短图像对话处理,最后只把文字结论合回主工程对话。
|
||||
## 4. 生产与部署
|
||||
|
||||
## 8. 敏感信息和交接材料
|
||||
- 当前正式网址:`https://cp.jiansun.vip/`。
|
||||
- 当前权威生产 SSH 入口:`root@ltai5tdb2ogyj59fquyedh.yydd.ai`,端口 `60022`。
|
||||
- 当前生产部署目录:`/opt/furniture-3d-system-prod`。
|
||||
- 当前 PM2 进程名:`furniture-3d-system-prod`。
|
||||
- 当前应用本机端口:`3001`。
|
||||
- 默认生产部署来源:`origin/main`。
|
||||
- 当前主部署入口是外层 `scripts/deploy-yydd-prod-from-git.ps1`;它默认部署 `origin/main`,并调用 `scripts/create-yydd-prod-deploy.ps1` 生成发布包/远端发布脚本。不要把下层打包脚本单独当成唯一部署入口。
|
||||
- 发布前必须确认 `git ls-remote origin refs/heads/main`、本地 `origin/main`、目标部署 commit 三者一致;本地 `main` 是否领先或落后远端不能替代 `origin/main` 的真实发布来源。
|
||||
- 旧 `192.168.58.20`、本机泛别名 `prod`、旧 `gongxiangp2 / youyidian-3d-material` 生产信息都不能套用到本项目。
|
||||
- 生产动作前至少实时确认:目标主机、远端目录、PM2 进程、`current` 软链接、监听端口和 `https://cp.jiansun.vip/api/health`。
|
||||
- 生产数据库、迁移、主数据地基相关动作先只读核查;需要写入时必须有备份、目标库身份、确认口令和可回滚路径。
|
||||
- 对象存储当前要按 `src/lib/storage.ts`、`storage:*` 脚本、七牛迁移/GC 文档和生产环境变量实时核对;不要把历史 MinIO/S3 备份脚本当成当前生产存储真相。
|
||||
|
||||
- 账号、密码、服务器地址、访问密钥、数据库连接、S3/MinIO 密钥等敏感信息,只能放在本地续接材料里。
|
||||
- 含敏感信息的摘要、日志或交接文件不能提交 Git,不能发给同事,不能写进共享文档。
|
||||
## 5. 当前主链与模块边界
|
||||
|
||||
当前新平台数据主链按这条线理解:
|
||||
|
||||
`资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库`
|
||||
|
||||
当前全局工作台导航还包含首页、OA 新品流程看板、仓库质检、AI 出图工作台等运营入口。导航入口不等于数据主链,不要把页面入口和主数据归属混成一件事。
|
||||
|
||||
主链默认入口和目录:
|
||||
|
||||
- 首页与全局壳:`src/app/page.tsx`、`src/components/layout`
|
||||
- 资产中心:`src/app/asset-center`、`src/app/api/asset-center`
|
||||
- 辅助装配:`src/app/assisted-assembly`、`src/app/api/assisted-assembly`
|
||||
- 草稿组:`src/app/draft-groups`、`src/app/api/draft-groups`
|
||||
- 产品资料/正式入库:`src/app/intake`、`src/app/api/intake`
|
||||
- 仓库管理/轻仓库:`src/app/warehouse`、`src/app/api/warehouse`
|
||||
- 仓库质检:`src/app/warehouse-quality`、`src/app/api/warehouse-quality`
|
||||
- AI 出图工作台:`src/app/ai-render-workbench`
|
||||
- 审批工作流:`src/app/approval-workflows`
|
||||
- 素材库:`src/app/materials`
|
||||
- 移动端:`src/app/mobile`
|
||||
- 平台组件库:`src/components/platform`
|
||||
- 主链规则与类型:`src/lib/open-product`、`src/lib/planning`
|
||||
|
||||
业务真相:
|
||||
|
||||
- 资产族:`AssetFamily`
|
||||
- 规格款:`AssetVariant`
|
||||
- 草稿组:`DraftGroup`
|
||||
- 准 SKU:`PreSku`
|
||||
- 入库结果:正式 `Product / ProductSKU`(`SKU` 是业务概念,Prisma 模型名是 `ProductSKU`)
|
||||
- 产品资料侧非集成开品:`IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`
|
||||
|
||||
旧 CAD 兼容链继续存在,但不是新平台主数据源:
|
||||
|
||||
- 页面:`src/app/products`、`src/app/products/[id]`、`src/app/products/[id]/assembly`、`src/app/products/[id]/skus/[skuId]`
|
||||
- 旧链模型和服务包括 `Product / ProductSKU / Panel / Hole / ProductHardware`、`assembly.service`、`panel.service`、`hole.service` 等。
|
||||
- 旧链可以继续服务历史产品、CAD 导入、装配步骤和兼容发布后能力,但不要反向主导资产中心、草稿组或产品资料主链。
|
||||
|
||||
共享接缝默认谨慎处理:
|
||||
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations`
|
||||
- `src/app/layout.tsx`
|
||||
- `src/app/intake`
|
||||
- `src/app/draft-groups/[id]`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
|
||||
改这些地方前先说明原因、影响范围和验证计划;不要为了局部 UI 或临时兼容绕过模块边界。
|
||||
|
||||
## 6. 架构硬规则
|
||||
|
||||
- API Route 负责收参、鉴权和返回,不直接堆业务规则或 Prisma 查询。
|
||||
- Service 负责业务规则、权限校验、事务编排、缓存和跨模块协调。
|
||||
- Repository 负责数据访问。
|
||||
- 任何新接口必须有明确输入输出 DTO/校验口径。
|
||||
- 不是必须的话,不要使用兜底逻辑;原生主逻辑能做好,就先把主逻辑、状态契约和数据来源修正确。兜底只能用于明确的兼容、降级或应急场景,不能用来掩盖本应修好的业务规则、接口契约或前端状态问题。
|
||||
- 任何 Prisma 模型调整必须带迁移,并补齐主数据/迁移核查;生产/准生产禁止用 `prisma db push` 替代迁移。
|
||||
- 影响命名、BOM、包装、安装步骤、SKU、barcode、批次、入库承接的数据问题,优先从数据源和业务规则修,不在页面上压报错或绕校验。
|
||||
- SPU、SKU、barcode、批次不能混用:SPU 是父级产品款,SKU 是 SPU 下的具体子项(Prisma 模型名 `ProductSKU`),barcode 是绑定到最终 SKU 的业务定位值;基于某 SKU 派生的新 SKU 必须仍属于同一 SPU,并按业务要求继承或明确指定批次。
|
||||
|
||||
## 7. 前端与视觉硬规则
|
||||
|
||||
- 白曜石工坊统一视觉标准是本项目新平台第一视觉标准;默认是浅色工作台,不再提供深浅色主题切换入口。
|
||||
- 本项目有自己的组件库与 `Meta Component(元组件)` 体系;前端实现必须优先复用 `src/components/platform`、资产中心已沉淀的工作台表达和现有元组件,不允许绕开组件库另起一套孤立 UI。
|
||||
- 当用户指定 Stitch 设计稿或已有 Stitch 设计稿可参考时,Stitch 是当前功能的视觉规范与验收标准;实现方式是在本项目组件库/元组件体系内对齐 Stitch 的密度、层级、控件形态、留白、字号、圆角、阴影和低噪音表达,而不是无视组件库硬抄静态稿。
|
||||
- 资产中心页面是当前项目内的优先参考样板;涉及资产、图片、SKU、AI 白底图、3D 预览、工作区和检视器时,默认参考资产中心的工作台感、元组件表达、低噪音操作面板和重点图片区。
|
||||
- `Inspector(检视器)`、数据列表、表格、长文本和表单密集区必须优先保证可读性;毛玻璃只允许用于顶栏、HUD、浮动工具条和轻量状态浮层,不承载正文。
|
||||
- 板材、五金、组件、包装等资产必须按元组件表达,不能退回普通图片卡片或营销式大卡片布局。
|
||||
- 现有 `apple-*` token、深色 `ObsidianControlShell`、历史主题 CSS 只能作为兼容遗留;新功能不得以它们作为视觉方向。
|
||||
- 前端改造不得把现有后台习惯、普通 SaaS 审美或共享组件默认皮肤当成最终依据;如果组件库默认视觉和 Stitch/资产中心标准冲突,应在组件库体系内做专用变体、重写皮肤或局部重构。
|
||||
- 没有出现在设计稿、组件规范或业务必要路径里的说明文案、辅助卡片、重复统计块、解释区,不要为了“更易懂”自行添加;工作区必须抓重点,图片类功能要让图片和关键动作成为第一层级。
|
||||
- 涉及前端动画、入退场、滚动联动、时间线、拖拽、SVG 或动效性能时,先判断是否有助于理解和操作;需要时优先按技术栈调用对应 GSAP Skill,并继续遵守 Impeccable。React 必须使用 `useGSAP` 或带 `gsap.context().revert()` 的完整清理,滚动相关实例必须销毁并在布局变化时正确刷新;优先动画 `transform/opacity`,尊重 `prefers-reduced-motion`,不得为装饰滥用动效。
|
||||
- 前端修改后优先用真实页面、截图、1920×1080 视口和必要的移动端视口验证,不只看代码。
|
||||
|
||||
## 8. 验证、收口与规范审查
|
||||
|
||||
- 修改后必须做最小但有效的验证;不能验证就明确说明未验证原因和风险。
|
||||
- 常用验证命令以当前 `package.json` 为准,优先选和改动范围匹配的最小集合:`npm run type-check`、`npm run build`、`npm test -- <相关测试>`、`npm run lint`;对象存储用 `npm run storage:gc:audit` / `npm run storage:gc:apply`,主数据用 `npm run db:foundation:*` 相关只读或授权脚本。
|
||||
- 前端体验类改动要结合浏览器实际操作和截图验证;涉及 3D/canvas 时要确认画面非空、尺寸正确、交互可用。
|
||||
- 生产验证必须包含真实健康检查和日志/PM2/端口等现场证据,不能只说"本地构建通过"。
|
||||
- 规范审查固定只查三件事:架构有没有被破坏、视觉有没有跑偏、这次是不是根因修复。结论只能是 `passed` / `passed with warnings` / `rejected`;只要架构违规、视觉违规或明显补丁式修复,就必须 `rejected`。非独立复审时必须写明"这是自审,不是独立复审"。统一清单:`furniture-3d-system/docs/workflow/规范审查清单.md`。没有规范审查结论,任务不算完成。
|
||||
|
||||
## 9. 敏感信息和交接
|
||||
|
||||
- 账号密码、访问密钥、数据库连接串、对象存储密钥等敏感信息只能放在本地安全材料里,不写入 Git、共享文档、普通交接文本或长期记忆;生产域名、PM2 名称、部署目录这类非密钥运行入口可以写进仓库文档,但必须保持最新。
|
||||
- 仓库内协作文档只写非敏感入口、流程和职责;凭据类信息必须用 `[REDACTED_SECRET]` 或本地路径说明替代。
|
||||
- 发现摘要文件里混有明文凭据时,默认先提示风险,再决定是否拆成本地敏感版和仓库协作版。
|
||||
|
||||
## 9. 规范审查
|
||||
|
||||
规范审查固定只查三件事:
|
||||
|
||||
- 架构有没有被破坏
|
||||
- 视觉有没有跑偏
|
||||
- 这次是不是根因修复
|
||||
|
||||
结论只能是:
|
||||
|
||||
- `passed`
|
||||
- `passed with warnings`
|
||||
- `rejected`
|
||||
|
||||
只要架构违规、视觉违规,或明显是补丁式修复,就必须 `rejected`。
|
||||
如果这次不是独立复审,必须明确写出“这是自审,不是独立复审”。
|
||||
|
||||
统一清单:`furniture-3d-system/docs/workflow/规范审查清单.md`
|
||||
|
||||
## 10. 标准来源
|
||||
|
||||
- `furniture-3d-system/docs/设计规范/白曜石工坊统一视觉标准.md`
|
||||
- `furniture-3d-system/docs/workflow/规范审查清单.md`
|
||||
- 发现摘要、日志或交接文件混有明文凭据时,先提示风险,再决定是否拆成本地敏感版和仓库协作版。
|
||||
- 给下一个 agent 或同事交接时,写清:本次只改了什么、没改什么、验证跑了什么、风险是什么、下一步要看哪些文件。
|
||||
|
||||
187
furniture-3d-system/.env.example
Normal file
187
furniture-3d-system/.env.example
Normal file
@ -0,0 +1,187 @@
|
||||
# 本地运行
|
||||
NODE_ENV=development
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DB_NAME?schema=public
|
||||
|
||||
# 员工工时花名只读人员配置来源(可选;开发环境配置后会合并正式人员配置)
|
||||
SUPPLY_CHAIN_PRODUCT_ARCHIVE_READ_DATABASE_URL=postgresql://READ_USER:READ_PASSWORD@READ_HOST:5432/READ_DB
|
||||
SUPPLY_CHAIN_PRODUCT_ARCHIVE_READ_DATABASE_SSL_REJECT_UNAUTHORIZED=true
|
||||
|
||||
# 产品全量档案的聚水潭 SQL Server 只读来源
|
||||
JST_REPORT_DB_HOST=
|
||||
JST_REPORT_DB_PORT=1433
|
||||
JST_REPORT_DB_NAME=
|
||||
JST_REPORT_DB_SCHEMA=rpt
|
||||
JST_REPORT_DB_USER=
|
||||
JST_REPORT_DB_PASSWORD=
|
||||
JST_REPORT_DB_ENCRYPT=true
|
||||
JST_REPORT_DB_TRUST_SERVER_CERTIFICATE=true
|
||||
|
||||
# OA 新品流程看板数据源。默认 mysql;切换到 JSReportReplica 时设为 mssql。
|
||||
OA_BOARD_SOURCE_DRIVER=mysql
|
||||
# MySQL 来源兼容配置。
|
||||
OA_BOARD_SOURCE_DB_HOST=
|
||||
OA_BOARD_SOURCE_DB_PORT=3306
|
||||
OA_BOARD_SOURCE_DB_NAME=
|
||||
OA_BOARD_SOURCE_DB_USER=
|
||||
OA_BOARD_SOURCE_DB_PASSWORD=
|
||||
OA_BOARD_SOURCE_DB_CHARSET=utf8mb4
|
||||
OA_BOARD_SOURCE_DB_SSL=false
|
||||
|
||||
# SQL Server 来源:JSReportReplica.rpt 的 ding_Workflow、ding_WorkflowScheme、ding_WorkflowTask 只读视图。
|
||||
# 使用 mssql 来源时,快照仍写入下方配置的企划 MySQL 库。
|
||||
OA_BOARD_MSSQL_HOST=
|
||||
OA_BOARD_MSSQL_PORT=1433
|
||||
OA_BOARD_MSSQL_DATABASE=JSReportReplica
|
||||
OA_BOARD_MSSQL_SCHEMA=rpt
|
||||
OA_BOARD_MSSQL_USERNAME=
|
||||
OA_BOARD_MSSQL_PASSWORD=
|
||||
OA_BOARD_MSSQL_ENCRYPT=false
|
||||
OA_BOARD_MSSQL_TRUST_SERVER_CERTIFICATE=true
|
||||
OA_BOARD_MSSQL_CONNECTION_TIMEOUT_MS=15000
|
||||
OA_BOARD_MSSQL_REQUEST_TIMEOUT_MS=120000
|
||||
|
||||
# OA 看板快照目标(MySQL)。留空时兼容复用 PLANNING_DATABASE_URL / PLANNING_DB_*。
|
||||
# 配置此组变量后,可保证切换 SQL Server 来源时所有快照、超时跟进和日报数据仍保存在企划库。
|
||||
OA_BOARD_SNAPSHOT_DATABASE_URL=
|
||||
OA_BOARD_SNAPSHOT_DB_HOST=
|
||||
OA_BOARD_SNAPSHOT_DB_PORT=3306
|
||||
OA_BOARD_SNAPSHOT_DB_NAME=
|
||||
OA_BOARD_SNAPSHOT_DB_USER=
|
||||
OA_BOARD_SNAPSHOT_DB_PASSWORD=
|
||||
OA_BOARD_SNAPSHOT_DB_CHARSET=utf8mb4
|
||||
OA_BOARD_SNAPSHOT_DB_SSL=false
|
||||
|
||||
# HR 员工目录(SQL Server)。OA 快照仅在本地员工目录无法匹配时使用,
|
||||
# 用于将 ding_WorkflowTask.userId 解析成当前负责人的姓名。
|
||||
HR_SQLSERVER_HOST=
|
||||
HR_SQLSERVER_PORT=1433
|
||||
HR_SQLSERVER_DATABASE=
|
||||
HR_SQLSERVER_USERNAME=
|
||||
HR_SQLSERVER_PASSWORD=
|
||||
HR_SQLSERVER_ENCRYPT=false
|
||||
HR_SQLSERVER_TRUST_SERVER_CERTIFICATE=true
|
||||
|
||||
# 产品资料供应商与仓库目录(SQL Server 报表库,只读)
|
||||
JST_PLANNING_SQLSERVER_HOST=
|
||||
JST_PLANNING_SQLSERVER_PORT=1433
|
||||
JST_PLANNING_SQLSERVER_DATABASE=
|
||||
JST_PLANNING_SQLSERVER_USERNAME=
|
||||
JST_PLANNING_SQLSERVER_PASSWORD=
|
||||
JST_PLANNING_SQLSERVER_SCHEMA=rpt
|
||||
JST_PLANNING_SQLSERVER_ENCRYPT=false
|
||||
JST_PLANNING_SQLSERVER_TRUST_SERVER_CERTIFICATE=true
|
||||
|
||||
# 登录和令牌
|
||||
JWT_SECRET=replace-with-random-secret
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# 钉钉登录(OAuth 应用;只用于登录授权和用户信息获取)
|
||||
DINGTALK_LOGIN_CLIENT_ID=
|
||||
DINGTALK_LOGIN_CLIENT_SECRET=
|
||||
DINGTALK_CALLBACK_ORIGIN=
|
||||
|
||||
# 钉钉开放平台 API(通讯录、待办、审批等服务端调用)
|
||||
DINGTALK_API_CLIENT_ID=
|
||||
DINGTALK_API_CLIENT_SECRET=
|
||||
|
||||
# 钉钉待办:OA 阶段超时预警,以及预计出货日前 15 天的品牌策划主管提醒。
|
||||
DINGTALK_TODO_ENABLED=false
|
||||
DINGTALK_TODO_DETAIL_BASE_URL=
|
||||
DINGTALK_TODO_SOURCE_APP=oa-board
|
||||
DINGTALK_TODO_ESCALATION_ENABLED=false
|
||||
DINGTALK_TODO_ESCALATION_DAYS=3
|
||||
# DINGTALK_AGENT_ID=
|
||||
|
||||
# 由壹点通知中心(仅服务端使用,项目密钥禁止提交或暴露给浏览器)
|
||||
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
|
||||
YYD_NOTIFICATION_PROJECT_CODE=furniture_platform
|
||||
YYD_NOTIFICATION_PROJECT_SECRET=
|
||||
YYD_NOTIFICATION_TIMEOUT_MS=10000
|
||||
|
||||
# 店铺账号新增时,梦汐/扬灵群使用的钉钉“由壹点”群机器人。
|
||||
# webhook 地址和签名密钥只配置在本地/正式站运行环境,不要提交真实值。
|
||||
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_URL=
|
||||
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_SECRET=
|
||||
DINGTALK_STORE_ACCOUNT_GROUP_WEBHOOK_TIMEOUT_MS=10000
|
||||
STORE_ACCOUNT_NOTIFICATION_RETRY_SECRET=
|
||||
|
||||
# 产品开发待办定时任务(仅服务端/部署 cron 使用,须与其他任务密钥分开)
|
||||
PRODUCT_DEVELOPMENT_TODO_CRON_SECRET=
|
||||
|
||||
# 对象存储
|
||||
# 必填,不能留空。当前生产桶使用 qiniu;只有明确切换适配器时才改为 s3/minio。
|
||||
STORAGE_PROVIDER=qiniu
|
||||
OBJECT_STORAGE_FILE_PATH=dev
|
||||
STORAGE_FILE_PATH=dev
|
||||
|
||||
# 七牛云 Kodo
|
||||
QINIU_ACCESS_KEY=replace-with-qiniu-access-key
|
||||
QINIU_SECRET_KEY=replace-with-qiniu-secret-key
|
||||
QINIU_BUCKET=sucaiku-js
|
||||
QINIU_PUBLIC_DOMAIN=https://qny.jiansun.vip
|
||||
QINIU_FILE_PATH=dev
|
||||
QINIU_USE_HTTPS=true
|
||||
QINIU_PRIVATE_BUCKET=true
|
||||
QINIU_PRIVATE_URL_EXPIRES_SECONDS=7200
|
||||
|
||||
# S3/MinIO 兼容适配器
|
||||
# STORAGE_PROVIDER=s3
|
||||
S3_ENDPOINT=http://127.0.0.1:9000
|
||||
S3_PUBLIC_ENDPOINT=http://127.0.0.1:9000
|
||||
S3_ACCESS_KEY=replace-with-s3-access-key
|
||||
S3_SECRET_KEY=replace-with-s3-secret-key
|
||||
S3_BUCKET=replace-with-s3-bucket
|
||||
S3_FILE_PATH=dev
|
||||
S3_REGION=us-east-1
|
||||
S3_ENABLE_PATH_STYLE=true
|
||||
|
||||
# 外部 AI 生图
|
||||
EXTERNAL_IMAGE_GENERATE_TIMEOUT_MS=180000
|
||||
|
||||
# BOM 核算竞品分析
|
||||
# 留空表示分析服务尚未接入;本地开发可显式填 fixture 验证完整工作台。
|
||||
# 生产环境禁止使用 fixture。
|
||||
BOM_COSTING_ANALYSIS_PROVIDER=
|
||||
# 真实竞品分析使用 market-detail-multimodal;Key 只放本地/生产环境变量。
|
||||
BOM_COSTING_MARKET_API_BASE_URL=https://qh.yydd.ai
|
||||
BOM_COSTING_MARKET_API_KEY=replace-with-market-open-api-key
|
||||
# 复用 AI 服务配置面板中已激活的多模态模型连接。
|
||||
BOM_COSTING_AI_SERVICE_TYPE=prompt-optimizer
|
||||
BOM_COSTING_MARKET_TIMEOUT_MS=60000
|
||||
# 最终结构与 BOM 汇总默认最多等待 110 秒;GPT-5 兼容网关会强制收紧到该上限,避免上游两分钟截断造成任务假死。
|
||||
BOM_COSTING_AI_TIMEOUT_MS=110000
|
||||
BOM_COSTING_AI_MAX_IMAGES=20
|
||||
# GPT-5 兼容网关的最终 BOM 汇总最多使用 12000 个输出额度,避免冗长回复拖垮整项任务。
|
||||
BOM_COSTING_AI_MAX_TOKENS=12000
|
||||
BOM_COSTING_AI_REASONING_EFFORT=low
|
||||
BOM_COSTING_MODELING_PROVIDER=
|
||||
|
||||
# BOM 核算实时同款采购预估(1688 官方图搜)。
|
||||
# 网关固定为 https://skills-gateway.1688.com/api/find_product/1.0.0。
|
||||
# AK 只放服务器私密环境变量,服务端会按 x-csk-* 协议签名;实时同款价固定 × 90%
|
||||
# 是整件采购预估,历史 BOM 仅保留为结构参考。不要把 AK 写入 Git 或 NEXT_PUBLIC_* 变量。
|
||||
ALIBABA_1688_IMAGE_SEARCH_AK=replace-with-1688-image-search-ak
|
||||
# 仅在官方网关地址调整时设置;系统会拒绝非官方 HTTPS 地址。
|
||||
ALIBABA_1688_IMAGE_SEARCH_ENDPOINT_URL=https://skills-gateway.1688.com/api/find_product/1.0.0
|
||||
ALIBABA_1688_IMAGE_SEARCH_TIMEOUT_MS=45000
|
||||
ALIBABA_1688_IMAGE_SEARCH_MIN_SIMILARITY=0.78
|
||||
ALIBABA_1688_IMAGE_SEARCH_PAGE_SIZE=10
|
||||
ALIBABA_1688_IMAGE_SEARCH_PURCHASE_AMOUNT=1
|
||||
ALIBABA_1688_IMAGE_SEARCH_TAGS=4306497
|
||||
|
||||
# 素材库白底图回传
|
||||
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_ENABLED=false
|
||||
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_BASE_URL=https://example.com
|
||||
MATERIAL_LIBRARY_WHITE_BACKGROUND_API_KEY=replace-with-material-library-api-key
|
||||
# 检测报告查询(不填则复用上面素材平台的 BaseUrl / API Key;都没有时「是否有检测报告」显示为空)
|
||||
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL=
|
||||
MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_KEY=
|
||||
|
||||
# 产品全量档案以图搜图(模型文件放在仓库外,不提交到 Git)
|
||||
IMAGE_SEARCH_MODEL_PATH=
|
||||
IMAGE_SEARCH_MODEL_SOURCE_BASE_URL=https://www.modelscope.cn/models/Xenova/dinov2-small/resolve/master
|
||||
IMAGE_SEARCH_MIN_SHAPE_COSINE=0.78
|
||||
IMAGE_SEARCH_MIN_SHAPE_MARGIN=0.08
|
||||
1
furniture-3d-system/.gitattributes
vendored
Normal file
1
furniture-3d-system/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
13
furniture-3d-system/.gitignore
vendored
13
furniture-3d-system/.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/node_modules.pnpm-mixed
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@ -15,6 +16,9 @@
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-*/
|
||||
/.next-oa-preview*/
|
||||
/.next-oa-webpack/
|
||||
/out/
|
||||
/.data/
|
||||
|
||||
@ -33,6 +37,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@ -44,6 +49,8 @@ next-env.d.ts
|
||||
/src/generated/prisma
|
||||
.database-backups/
|
||||
.master-data-audits/
|
||||
.storage-migration-audits/
|
||||
.storage-gc-audits/
|
||||
.master-data-cleanup/
|
||||
|
||||
# local workspace artifacts
|
||||
@ -66,3 +73,9 @@ typecheck-latest.log
|
||||
.local-test-*.log
|
||||
tsconfig.typecheck.generated.json
|
||||
local-packages/npm-cache/cache/_logs/
|
||||
|
||||
# 脚本监控 Agent 本机凭据
|
||||
agents/script-monitor/config.json
|
||||
|
||||
# 下载中心本地运行时产物
|
||||
.download-center/
|
||||
|
||||
30
furniture-3d-system/agents/script-monitor/README.md
Normal file
30
furniture-3d-system/agents/script-monitor/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 供应链脚本监控 Agent
|
||||
|
||||
每台运行脚本的机器部署一份 Agent。Agent 只主动向产品平台发起 HTTP 请求,不需要为被监控机器开放入站端口。
|
||||
|
||||
## 接入
|
||||
|
||||
1. 在供应链中心的“脚本监控”页面点击“接入机器”,生成独立的 `agentId` 和 `agentToken`。
|
||||
2. 复制本目录到目标机器,把 `config.example.json` 复制为 `config.json`,填入凭据、内外网地址和脚本任务说明。
|
||||
3. 先运行 `node agent.mjs` 确认上报,再使用 `pm2 start ecosystem.config.cjs` 常驻。
|
||||
|
||||
需要在平台点击“立即运行”的脚本,必须在 `config.json` 对应任务中显式配置
|
||||
`"allowManualRun": true`。平台只会下发脚本 `key`,Agent 始终执行本机配置里预存的
|
||||
`command`,不会接收平台传来的任意命令。Agent 默认每 5 秒主动领取一次执行请求,
|
||||
因此内网机器不需要开放入站端口。
|
||||
|
||||
`connectionMode` 支持:
|
||||
|
||||
- `intranet`:只走内网地址。
|
||||
- `internet`:只走公网地址,生产必须使用 HTTPS。
|
||||
- `auto`:优先内网,失败后自动尝试公网。
|
||||
|
||||
## 记录真实运行结果
|
||||
|
||||
对于需要记录开始时间、耗时、退出码和输出的任务,用包装器代替原命令:
|
||||
|
||||
```bash
|
||||
node /opt/furniture-script-monitor/run-script.mjs supply-chain-archive-sync
|
||||
```
|
||||
|
||||
包装器执行 `config.json` 中同一 `key` 的 `command`,并在开始与结束时上报。Agent Token 只保存在目标机器的 `config.json`,该文件已被仓库忽略。
|
||||
410
furniture-3d-system/agents/script-monitor/agent.mjs
Normal file
410
furniture-3d-system/agents/script-monitor/agent.mjs
Normal file
@ -0,0 +1,410 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { execFileSync, spawn } from 'node:child_process'
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs'
|
||||
import { hostname, platform, release } from 'node:os'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
export const AGENT_VERSION = '1.0.0'
|
||||
const AGENT_DIR = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function requiredText(value, label) {
|
||||
if (typeof value !== 'string' || !value.trim()) throw new Error(`缺少 ${label}`)
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
export function loadConfig(configPath = process.env.SCRIPT_MONITOR_CONFIG || resolve(AGENT_DIR, 'config.json')) {
|
||||
if (!existsSync(configPath)) {
|
||||
throw new Error(`配置文件不存在:${configPath},请复制 config.example.json 为 config.json`)
|
||||
}
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
||||
config.agentId = requiredText(config.agentId, 'agentId')
|
||||
config.agentToken = requiredText(config.agentToken, 'agentToken')
|
||||
config.connectionMode = String(config.connectionMode || 'auto').toLowerCase()
|
||||
if (!['intranet', 'internet', 'auto'].includes(config.connectionMode)) {
|
||||
throw new Error('connectionMode 必须是 intranet、internet 或 auto')
|
||||
}
|
||||
config.reportIntervalSec = Math.max(15, Number(config.reportIntervalSec) || 60)
|
||||
config.requestTimeoutMs = Math.max(1000, Number(config.requestTimeoutMs) || 10000)
|
||||
config.commandPollIntervalSec = Math.min(60, Math.max(2, Number(config.commandPollIntervalSec) || 5))
|
||||
config.scripts = Array.isArray(config.scripts) ? config.scripts : []
|
||||
return config
|
||||
}
|
||||
|
||||
function normalizeCenterUrl(value) {
|
||||
if (typeof value !== 'string' || !value.trim()) return null
|
||||
const url = new URL(value.trim())
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error(`不支持的中心地址协议:${url.protocol}`)
|
||||
return url.toString().replace(/\/$/, '')
|
||||
}
|
||||
|
||||
export function resolveCenterEndpoints(config) {
|
||||
const intranet = normalizeCenterUrl(config.centerUrls?.intranet)
|
||||
const internet = normalizeCenterUrl(config.centerUrls?.internet)
|
||||
const endpoints = []
|
||||
if (config.connectionMode !== 'internet' && intranet) endpoints.push({ kind: 'INTRANET', url: intranet })
|
||||
if (config.connectionMode !== 'intranet' && internet) endpoints.push({ kind: 'INTERNET', url: internet })
|
||||
if (endpoints.length === 0) throw new Error('当前连接模式没有可用的 centerUrls 地址')
|
||||
return endpoints
|
||||
}
|
||||
|
||||
export async function postToCenter(config, route, payload) {
|
||||
let lastError = null
|
||||
for (const endpoint of resolveCenterEndpoints(config)) {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs)
|
||||
try {
|
||||
const response = await fetch(`${endpoint.url}${route}`, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
authorization: `Bearer ${config.agentToken}`,
|
||||
'x-script-monitor-agent-id': config.agentId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
...(route.endsWith('/report')
|
||||
? { connectionKind: payload.connectionKind ?? endpoint.kind }
|
||||
: {}),
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const message = await response.text().catch(() => '')
|
||||
throw new Error(`HTTP ${response.status}${message ? `:${message.slice(0, 240)}` : ''}`)
|
||||
}
|
||||
return { endpoint, body: await response.json().catch(() => null) }
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error('没有可用的中心地址')
|
||||
}
|
||||
|
||||
function hashKey(value) {
|
||||
return createHash('sha256').update(value).digest('hex').slice(0, 20)
|
||||
}
|
||||
|
||||
function redactCommand(command) {
|
||||
return String(command || '')
|
||||
.replace(/([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*=)([^\s]+)/gi, '$1[REDACTED]')
|
||||
.replace(/(https?:\/\/[^:/\s]+:)[^@/\s]+@/gi, '$1[REDACTED]@')
|
||||
.slice(0, 2000)
|
||||
}
|
||||
|
||||
function readPm2() {
|
||||
try {
|
||||
const raw = execFileSync('pm2', ['jlist'], { encoding: 'utf8', timeout: 5000 })
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function scanPm2() {
|
||||
return readPm2().map((processInfo) => {
|
||||
const name = String(processInfo.name || '未命名 PM2 进程')
|
||||
const status = processInfo.pm2_env?.status === 'online'
|
||||
? 'RUNNING'
|
||||
: processInfo.pm2_env?.status === 'stopped'
|
||||
? 'DISABLED'
|
||||
: processInfo.pm2_env?.status === 'errored'
|
||||
? 'FAILED'
|
||||
: 'UNKNOWN'
|
||||
return {
|
||||
key: `pm2:${name}`,
|
||||
name,
|
||||
taskDescription: '',
|
||||
jobType: 'PM2',
|
||||
schedule: null,
|
||||
commandPreview: redactCommand(processInfo.pm2_env?.pm_exec_path || ''),
|
||||
expectedIntervalSec: null,
|
||||
status,
|
||||
isEnabled: status !== 'DISABLED',
|
||||
canManualRun: false,
|
||||
lastStartedAt: processInfo.pm2_env?.pm_uptime
|
||||
? new Date(processInfo.pm2_env.pm_uptime).toISOString()
|
||||
: null,
|
||||
lastResultSummary: status === 'RUNNING'
|
||||
? `PM2 在线,PID ${processInfo.pid || '-'}`
|
||||
: `PM2 状态:${processInfo.pm2_env?.status || 'unknown'}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function parseCronLine(line) {
|
||||
const value = line.trim()
|
||||
if (!value || value.startsWith('#')) return null
|
||||
const special = value.match(/^@(reboot|yearly|annually|monthly|weekly|daily|hourly)\s+(.+)$/)
|
||||
const regular = value.match(/^(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$/)
|
||||
const schedule = special ? `@${special[1]}` : regular?.[1]
|
||||
const command = special?.[2] ?? regular?.[2]
|
||||
if (!schedule || !command) return null
|
||||
const tag = command.match(/#\s*([\w.-]{3,})\s*$/)?.[1]
|
||||
const name = tag || command.split(/\s+/).filter(Boolean).slice(-1)[0] || 'cron task'
|
||||
return {
|
||||
key: `cron:${hashKey(`${schedule}\n${command}`)}`,
|
||||
name,
|
||||
taskDescription: '',
|
||||
jobType: 'CRON',
|
||||
schedule,
|
||||
commandPreview: redactCommand(command.replace(/\s+#.*$/, '')),
|
||||
expectedIntervalSec: null,
|
||||
status: 'UNKNOWN',
|
||||
isEnabled: true,
|
||||
canManualRun: false,
|
||||
}
|
||||
}
|
||||
|
||||
function scanCron() {
|
||||
if (platform() === 'win32') return []
|
||||
try {
|
||||
const raw = execFileSync('crontab', ['-l'], { encoding: 'utf8', timeout: 5000 })
|
||||
return raw.split(/\r?\n/).map(parseCronLine).filter(Boolean)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function checkProcess(name) {
|
||||
try {
|
||||
if (platform() === 'win32') {
|
||||
const output = execFileSync('tasklist', ['/FI', `IMAGENAME eq ${name}`], { encoding: 'utf8', timeout: 4000 })
|
||||
return output.toLowerCase().includes(String(name).toLowerCase())
|
||||
}
|
||||
execFileSync('pgrep', ['-f', '--', name], { stdio: 'ignore', timeout: 4000 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function checkConfiguredScript(script) {
|
||||
if (script.enabled === false) return { status: 'DISABLED', summary: '监控已停用' }
|
||||
const check = script.resultCheck
|
||||
if (!check) return { status: 'UNKNOWN', summary: null }
|
||||
|
||||
if (check.type === 'file') {
|
||||
try {
|
||||
const stat = statSync(requiredText(check.path, 'resultCheck.path'))
|
||||
const ageSec = Math.max(0, Math.round((Date.now() - stat.mtimeMs) / 1000))
|
||||
const fresh = !check.maxAgeSec || ageSec <= Number(check.maxAgeSec)
|
||||
const largeEnough = check.minBytes == null || stat.size >= Number(check.minBytes)
|
||||
return {
|
||||
status: fresh && largeEnough ? 'SUCCESS' : 'FAILED',
|
||||
summary: `文件 ${stat.size} B,${ageSec} 秒前更新`,
|
||||
}
|
||||
} catch (error) {
|
||||
return { status: 'FAILED', summary: `文件检查失败:${error.message}` }
|
||||
}
|
||||
}
|
||||
|
||||
if (check.type === 'systemd') {
|
||||
try {
|
||||
const service = requiredText(check.service, 'resultCheck.service')
|
||||
const state = execFileSync('systemctl', ['is-active', service], { encoding: 'utf8', timeout: 4000 }).trim()
|
||||
return { status: state === 'active' ? 'RUNNING' : 'FAILED', summary: `systemd:${state}` }
|
||||
} catch {
|
||||
return { status: 'FAILED', summary: 'systemd 服务未运行' }
|
||||
}
|
||||
}
|
||||
|
||||
if (check.type === 'process') {
|
||||
const running = checkProcess(requiredText(check.name, 'resultCheck.name'))
|
||||
return { status: running ? 'RUNNING' : 'FAILED', summary: running ? '进程正在运行' : '未找到目标进程' }
|
||||
}
|
||||
|
||||
return { status: 'UNKNOWN', summary: '未识别的结果检查类型' }
|
||||
}
|
||||
|
||||
function normalizeConfiguredScript(script) {
|
||||
const key = requiredText(script.key, 'scripts[].key')
|
||||
const result = checkConfiguredScript(script)
|
||||
return {
|
||||
key,
|
||||
name: requiredText(script.name, `scripts[${key}].name`),
|
||||
taskDescription: String(script.taskDescription || '').slice(0, 2000),
|
||||
jobType: String(script.jobType || 'OTHER').toUpperCase(),
|
||||
schedule: script.schedule ? String(script.schedule).slice(0, 240) : null,
|
||||
commandPreview: redactCommand(script.commandPreview || script.command || ''),
|
||||
expectedIntervalSec: script.expectedIntervalSec ? Number(script.expectedIntervalSec) : null,
|
||||
status: result.status,
|
||||
isEnabled: script.enabled !== false,
|
||||
canManualRun: script.allowManualRun === true && Boolean(script.command),
|
||||
lastResultSummary: result.summary,
|
||||
}
|
||||
}
|
||||
|
||||
export function collectScripts(config) {
|
||||
const byKey = new Map()
|
||||
if (config.autoDiscover?.cron !== false) {
|
||||
for (const script of scanCron()) byKey.set(script.key, script)
|
||||
}
|
||||
if (config.autoDiscover?.pm2 !== false) {
|
||||
for (const script of scanPm2()) byKey.set(script.key, script)
|
||||
}
|
||||
for (const script of config.scripts) {
|
||||
const normalized = normalizeConfiguredScript(script)
|
||||
byKey.set(normalized.key, { ...(byKey.get(normalized.key) || {}), ...normalized })
|
||||
}
|
||||
return [...byKey.values()]
|
||||
}
|
||||
|
||||
export async function reportSnapshot(config) {
|
||||
const scripts = collectScripts(config)
|
||||
const result = await postToCenter(config, '/api/internal/script-monitor/report', {
|
||||
hostname: hostname(),
|
||||
os: `${platform()} ${release()}`,
|
||||
agentVersion: AGENT_VERSION,
|
||||
reportIntervalSec: config.reportIntervalSec,
|
||||
scripts,
|
||||
})
|
||||
console.log(`[script-monitor] 已通过${result.endpoint.kind === 'INTRANET' ? '内网' : '公网'}上报 ${scripts.length} 个脚本`)
|
||||
}
|
||||
|
||||
function keepTail(current, chunk, limit = 12000) {
|
||||
const next = current + chunk.toString()
|
||||
return next.length > limit ? next.slice(-limit) : next
|
||||
}
|
||||
|
||||
async function reportManualRun(config, command, input) {
|
||||
return postToCenter(config, '/api/internal/script-monitor/run', {
|
||||
commandId: command.id,
|
||||
scriptKey: command.scriptKey,
|
||||
scriptName: command.scriptName,
|
||||
taskDescription: command.taskDescription || '',
|
||||
jobType: String(command.jobType || 'OTHER').toUpperCase(),
|
||||
...input,
|
||||
})
|
||||
}
|
||||
|
||||
export async function executeManualCommand(config, command) {
|
||||
const script = config.scripts.find((item) => item.key === command.scriptKey)
|
||||
const startedAt = new Date()
|
||||
const externalRunId = `manual:${command.id}`
|
||||
if (!script?.command || script.allowManualRun !== true) {
|
||||
const message = 'Agent 配置未开放该脚本的手动运行权限'
|
||||
await reportManualRun(config, command, {
|
||||
externalRunId,
|
||||
status: 'FAILED',
|
||||
trigger: 'manual',
|
||||
startedAt: startedAt.toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
durationMs: 0,
|
||||
exitCode: 1,
|
||||
resultSummary: message,
|
||||
errorTail: message,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await reportManualRun(config, command, {
|
||||
externalRunId,
|
||||
status: 'RUNNING',
|
||||
trigger: 'manual',
|
||||
startedAt: startedAt.toISOString(),
|
||||
})
|
||||
let outputTail = ''
|
||||
let errorTail = ''
|
||||
try {
|
||||
const child = spawn(script.command, {
|
||||
cwd: script.cwd ? resolve(script.cwd) : process.cwd(),
|
||||
env: process.env,
|
||||
shell: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
child.stdout.on('data', (chunk) => {
|
||||
outputTail = keepTail(outputTail, chunk)
|
||||
process.stdout.write(chunk)
|
||||
})
|
||||
child.stderr.on('data', (chunk) => {
|
||||
errorTail = keepTail(errorTail, chunk)
|
||||
process.stderr.write(chunk)
|
||||
})
|
||||
const exitCode = await new Promise((resolveExit, reject) => {
|
||||
child.once('error', reject)
|
||||
child.once('close', (code) => resolveExit(code ?? 1))
|
||||
})
|
||||
const finishedAt = new Date()
|
||||
await reportManualRun(config, command, {
|
||||
externalRunId,
|
||||
status: exitCode === 0 ? 'SUCCESS' : 'FAILED',
|
||||
trigger: 'manual',
|
||||
startedAt: startedAt.toISOString(),
|
||||
finishedAt: finishedAt.toISOString(),
|
||||
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
||||
exitCode,
|
||||
resultSummary: exitCode === 0 ? '手动运行成功' : `手动运行失败,退出码 ${exitCode}`,
|
||||
outputTail,
|
||||
errorTail,
|
||||
})
|
||||
} catch (error) {
|
||||
const finishedAt = new Date()
|
||||
await reportManualRun(config, command, {
|
||||
externalRunId,
|
||||
status: 'FAILED',
|
||||
trigger: 'manual',
|
||||
startedAt: startedAt.toISOString(),
|
||||
finishedAt: finishedAt.toISOString(),
|
||||
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
||||
exitCode: 1,
|
||||
resultSummary: `手动运行启动失败:${error.message}`,
|
||||
errorTail: error.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function pollManualCommand(config) {
|
||||
const result = await postToCenter(
|
||||
config,
|
||||
'/api/internal/script-monitor/commands/claim',
|
||||
{},
|
||||
)
|
||||
const command = result.body?.data?.command
|
||||
if (!command) return false
|
||||
await executeManualCommand(config, command)
|
||||
return true
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const config = loadConfig()
|
||||
const run = async () => {
|
||||
try {
|
||||
await reportSnapshot(config)
|
||||
} catch (error) {
|
||||
console.error('[script-monitor] 上报失败:', error.message)
|
||||
}
|
||||
}
|
||||
let commandBusy = false
|
||||
const poll = async () => {
|
||||
if (commandBusy) return
|
||||
commandBusy = true
|
||||
try {
|
||||
await pollManualCommand(config)
|
||||
} catch (error) {
|
||||
console.error('[script-monitor] 手动任务轮询失败:', error.message)
|
||||
} finally {
|
||||
commandBusy = false
|
||||
}
|
||||
}
|
||||
await run()
|
||||
await poll()
|
||||
const timer = setInterval(run, config.reportIntervalSec * 1000)
|
||||
const commandTimer = setInterval(poll, config.commandPollIntervalSec * 1000)
|
||||
const stop = () => {
|
||||
clearInterval(timer)
|
||||
clearInterval(commandTimer)
|
||||
process.exit(0)
|
||||
}
|
||||
process.on('SIGINT', stop)
|
||||
process.on('SIGTERM', stop)
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
main().catch((error) => {
|
||||
console.error('[script-monitor] 启动失败:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
79
furniture-3d-system/agents/script-monitor/agent.test.mjs
Normal file
79
furniture-3d-system/agents/script-monitor/agent.test.mjs
Normal file
@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { executeManualCommand } from './agent.mjs'
|
||||
|
||||
function config(overrides = {}) {
|
||||
return {
|
||||
agentId: 'agent-1',
|
||||
agentToken: 'secret-token',
|
||||
connectionMode: 'internet',
|
||||
centerUrls: { internet: 'https://monitor.example.test' },
|
||||
requestTimeoutMs: 5000,
|
||||
scripts: [{
|
||||
key: 'local-check',
|
||||
name: '本机检查',
|
||||
taskDescription: '验证手动触发',
|
||||
jobType: 'MANUAL',
|
||||
allowManualRun: true,
|
||||
command: `${JSON.stringify(process.execPath)} -e "console.log('manual-ok')"`,
|
||||
}],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function command() {
|
||||
return {
|
||||
id: 'command-1',
|
||||
scriptKey: 'local-check',
|
||||
scriptName: '本机检查',
|
||||
taskDescription: '验证手动触发',
|
||||
jobType: 'MANUAL',
|
||||
}
|
||||
}
|
||||
|
||||
test('manual command executes only the local configured command and reports its result', async () => {
|
||||
const previousFetch = globalThis.fetch
|
||||
const reports = []
|
||||
globalThis.fetch = async (_url, init) => {
|
||||
reports.push(JSON.parse(init.body))
|
||||
return new Response(JSON.stringify({ success: true, data: { ok: true } }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
}
|
||||
try {
|
||||
await executeManualCommand(config(), command())
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch
|
||||
}
|
||||
|
||||
assert.equal(reports.length, 2)
|
||||
assert.equal(reports[0].status, 'RUNNING')
|
||||
assert.equal(reports[1].status, 'SUCCESS')
|
||||
assert.equal(reports[1].exitCode, 0)
|
||||
assert.match(reports[1].outputTail, /manual-ok/)
|
||||
assert.equal(reports[1].commandId, 'command-1')
|
||||
})
|
||||
|
||||
test('manual command is rejected when allowManualRun is not explicitly enabled', async () => {
|
||||
const previousFetch = globalThis.fetch
|
||||
const reports = []
|
||||
globalThis.fetch = async (_url, init) => {
|
||||
reports.push(JSON.parse(init.body))
|
||||
return new Response(JSON.stringify({ success: true, data: { ok: true } }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})
|
||||
}
|
||||
const blockedConfig = config()
|
||||
blockedConfig.scripts[0].allowManualRun = false
|
||||
try {
|
||||
await executeManualCommand(blockedConfig, command())
|
||||
} finally {
|
||||
globalThis.fetch = previousFetch
|
||||
}
|
||||
|
||||
assert.equal(reports.length, 1)
|
||||
assert.equal(reports[0].status, 'FAILED')
|
||||
assert.match(reports[0].resultSummary, /未开放/)
|
||||
})
|
||||
@ -0,0 +1,36 @@
|
||||
{
|
||||
"agentId": "PASTE_AGENT_ID_HERE",
|
||||
"agentToken": "PASTE_AGENT_TOKEN_HERE",
|
||||
"agentName": "仓库服务器 01",
|
||||
"connectionMode": "auto",
|
||||
"centerUrls": {
|
||||
"intranet": "http://10.0.0.20:3001",
|
||||
"internet": "https://cp.jiansun.vip"
|
||||
},
|
||||
"reportIntervalSec": 60,
|
||||
"commandPollIntervalSec": 5,
|
||||
"requestTimeoutMs": 10000,
|
||||
"autoDiscover": {
|
||||
"cron": true,
|
||||
"pm2": true
|
||||
},
|
||||
"scripts": [
|
||||
{
|
||||
"key": "supply-chain-archive-sync",
|
||||
"name": "供应链产品档案同步",
|
||||
"taskDescription": "同步聚水潭商品档案、仓库和供应商扩展字段",
|
||||
"jobType": "CRON",
|
||||
"schedule": "*/15 * * * *",
|
||||
"expectedIntervalSec": 900,
|
||||
"allowManualRun": true,
|
||||
"command": "npm run sync:supply-chain-product-archive",
|
||||
"cwd": "/opt/furniture-3d-system-prod/current",
|
||||
"resultCheck": {
|
||||
"type": "file",
|
||||
"path": "/opt/furniture-3d-system-prod/shared/logs/product-archive-sync.log",
|
||||
"maxAgeSec": 1800,
|
||||
"minBytes": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: 'furniture-script-monitor-agent',
|
||||
script: 'agent.mjs',
|
||||
cwd: __dirname,
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
SCRIPT_MONITOR_CONFIG: `${__dirname}/config.json`,
|
||||
},
|
||||
}],
|
||||
}
|
||||
82
furniture-3d-system/agents/script-monitor/run-script.mjs
Normal file
82
furniture-3d-system/agents/script-monitor/run-script.mjs
Normal file
@ -0,0 +1,82 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { resolve } from 'node:path'
|
||||
import { loadConfig, postToCenter } from './agent.mjs'
|
||||
|
||||
function keepTail(current, chunk, limit = 12000) {
|
||||
const next = current + chunk.toString()
|
||||
return next.length > limit ? next.slice(-limit) : next
|
||||
}
|
||||
|
||||
async function sendRun(config, script, input) {
|
||||
return postToCenter(config, '/api/internal/script-monitor/run', {
|
||||
scriptKey: script.key,
|
||||
scriptName: script.name,
|
||||
taskDescription: script.taskDescription || '',
|
||||
jobType: String(script.jobType || 'OTHER').toUpperCase(),
|
||||
...input,
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const scriptKey = process.argv[2]
|
||||
if (!scriptKey) throw new Error('用法:node run-script.mjs <script-key>')
|
||||
const config = loadConfig()
|
||||
const script = config.scripts.find((item) => item.key === scriptKey)
|
||||
if (!script) throw new Error(`配置中不存在脚本:${scriptKey}`)
|
||||
if (!script.command) throw new Error(`脚本 ${scriptKey} 未配置 command`)
|
||||
|
||||
const externalRunId = `${scriptKey}:${randomUUID()}`
|
||||
const startedAt = new Date()
|
||||
await sendRun(config, script, {
|
||||
externalRunId,
|
||||
status: 'RUNNING',
|
||||
trigger: 'wrapper',
|
||||
startedAt: startedAt.toISOString(),
|
||||
}).catch((error) => {
|
||||
console.warn('[script-monitor-run] 开始状态上报失败,不阻断原脚本:', error.message)
|
||||
})
|
||||
|
||||
let outputTail = ''
|
||||
let errorTail = ''
|
||||
const child = spawn(script.command, {
|
||||
cwd: script.cwd ? resolve(script.cwd) : process.cwd(),
|
||||
env: process.env,
|
||||
shell: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
child.stdout.on('data', (chunk) => {
|
||||
outputTail = keepTail(outputTail, chunk)
|
||||
process.stdout.write(chunk)
|
||||
})
|
||||
child.stderr.on('data', (chunk) => {
|
||||
errorTail = keepTail(errorTail, chunk)
|
||||
process.stderr.write(chunk)
|
||||
})
|
||||
|
||||
const exitCode = await new Promise((resolveExit, reject) => {
|
||||
child.once('error', reject)
|
||||
child.once('close', (code) => resolveExit(code ?? 1))
|
||||
})
|
||||
const finishedAt = new Date()
|
||||
await sendRun(config, script, {
|
||||
externalRunId,
|
||||
status: exitCode === 0 ? 'SUCCESS' : 'FAILED',
|
||||
trigger: 'wrapper',
|
||||
startedAt: startedAt.toISOString(),
|
||||
finishedAt: finishedAt.toISOString(),
|
||||
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
||||
exitCode,
|
||||
resultSummary: exitCode === 0 ? '脚本执行成功' : `脚本退出码 ${exitCode}`,
|
||||
outputTail,
|
||||
errorTail,
|
||||
}).catch((error) => {
|
||||
console.warn('[script-monitor-run] 运行结果上报失败,保留原退出码:', error.message)
|
||||
})
|
||||
process.exitCode = exitCode
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[script-monitor-run] 执行失败:', error.message)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@ -0,0 +1,153 @@
|
||||
# 财务系统店铺密码直接显示契约
|
||||
|
||||
状态:生产接口已上线;2026-09-09 按当前明文存储规则修正 `passwordConfigured`。
|
||||
|
||||
## 存储事实与授权边界
|
||||
|
||||
- 密码真相源是 `public."StoreAccount"."passwordCiphertext"`。字段名是历史遗留;当前规则将新值以明文保存,读取链路对历史 `v1:` AES-256-GCM 值保留兼容。
|
||||
- 存量 `v1:` 值已由 `scripts/migrate-store-account-plaintext-20260908.js` 迁移。`STORE_ACCOUNT_ENCRYPTION_KEY` 仅用于可能残留或恢复出的历史 `v1:` 值,不参与当前明文值读取。
|
||||
- `passwordConfigured` 只表示存储值是否非空,不校验字符集或密码强度;因此包含 `+`、`/`、`=` 等字符的密码也会正确返回 true。它不返回密码本身。
|
||||
- 普通 `store_account_reader` 可读视图、不能读取基表或密码列。此次迁移不增加其基表权限。
|
||||
- 本接口只信任专用服务身份 `finance-store-accounts`。财务系统必须在自身后端校验用户登录态、查看/复制权限及店铺范围,然后从可信会话中取 `requesterId`。不得把客户端传入的任意人员 ID 当成操作者。服务凭据仅保存在财务后端,禁止交给浏览器。
|
||||
- 不提供密码列表或批量导出。财务页面可在已通过自身页面权限校验后,自动读取当前可见页最多 50 个账号;CSV 和共享镜像仍只读取 `passwordConfigured`。复制操作继续走单账号接口。上游审计记录的用户 ID 是专用财务服务的身份断言,未授权请求中的 ID 不可信,`serviceId=null`。
|
||||
|
||||
## HTTP 契约
|
||||
|
||||
生产配置完成后的地址:
|
||||
|
||||
`POST https://cp.jiansun.vip/internal/store-accounts/{id}/password/reveal`
|
||||
|
||||
`POST https://cp.jiansun.vip/internal/store-accounts/visible-page/password/reveal`
|
||||
|
||||
`id` 为 `accounts_v1.id`,仅接受单个 1–128 位字母、数字、下划线或连字符。无查询参数;禁止在 URL、请求体、请求头中传待读取密码。
|
||||
|
||||
请求头:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <专用服务令牌>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体(严格拒绝额外字段,最大 1024 字节,1 秒内读取完毕):
|
||||
|
||||
```json
|
||||
{ "requesterId": "finance-user-123", "purpose": "view" }
|
||||
```
|
||||
|
||||
`purpose` 仅允许 `view` 或 `copy`。`requesterId` 是财务会话中的稳定人员标识,1–128 位 `[A-Za-z0-9_.:@-]`。不接受批量 ID、查询参数、普通登录 Cookie/JWT、普通数据库凭据或其他内部任务密钥。
|
||||
|
||||
当前页批量请求严格只接受以下字段,正文最大 8192 字节;`accountIds` 为 1 至 50 个互不重复且符合单账号 ID 规则的值:
|
||||
|
||||
```json
|
||||
{ "requesterId": "finance-user-123", "purpose": "view", "accountIds": ["account-1", "account-2"] }
|
||||
```
|
||||
|
||||
成功 `200`:
|
||||
|
||||
```json
|
||||
{ "requestId": "<服务端生成的 UUID>", "password": "<仅本次响应体中的密码>" }
|
||||
```
|
||||
|
||||
当前页批量成功的 HTTP 状态为 `200`,每个账号分别返回 `200/404/429/503`。每项都有独立 UUID;只有状态 `200` 的项包含密码,并按请求顺序完整返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "<批次 UUID>",
|
||||
"items": [
|
||||
{ "accountId": "account-1", "requestId": "<项目 UUID>", "status": 200, "password": "<密码>" },
|
||||
{ "accountId": "account-2", "requestId": "<项目 UUID>", "status": 404 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所有状态均返回 `Cache-Control: no-store`、`Pragma: no-cache`、`X-Content-Type-Options: nosniff`。`X-Request-Id` 与响应体、审计的追踪 ID 相同。不会在响应头中放密码。
|
||||
|
||||
错误体:
|
||||
|
||||
```json
|
||||
{ "requestId": "<UUID>", "error": { "code": "FORBIDDEN", "message": "请求被拒绝" } }
|
||||
```
|
||||
|
||||
| HTTP | code | 含义 |
|
||||
|---|---|---|
|
||||
| 403 | FORBIDDEN | 服务认证失败、非 HTTPS、非法 ID/JSON/用途、额外参数或非 POST |
|
||||
| 404 | PASSWORD_NOT_FOUND | 账号不存在或没有可用密码,两者不区分 |
|
||||
| 429 | RATE_LIMITED | 任一限流窗口超限 |
|
||||
| 503 | UNAVAILABLE | 密钥/解密、数据库、限流或审计不可用;不返回内部异常 |
|
||||
|
||||
审计或数据库故障优先返回 503,绝不在无法持久化成功审计时返回密码。数据库完全不可用时无法保存该次失败审计,由网关仅记录追踪 ID、状态与时间供运维关联。成功审计表示服务器已授权并生成响应,不等同于浏览器已经复制成功。
|
||||
|
||||
## 限流和超时
|
||||
|
||||
- 单账号数据库滚动窗口:服务全部账号 30 次/分钟、300 次/24 小时;同一人员 10 次/分钟;同一账号 5 次/分钟。公网 TLS 网关保留 1 KiB 敏感请求正文上限,财务端把 50 行当前页拆成两个最多 25 个账号的批次;批次标记因此按服务 20 次/分钟、1000 次/24 小时、同一人员 10 次/分钟限流,等价于原每页额度。批次中的每个账号仍受同一账号 5 次/分钟限制。查看和复制共享账号额度,轮换令牌不重置服务身份或额度。
|
||||
- 已认证的成功、404、参数拒绝和解密失败均占额度;429 不延长窗口。额度检查与审计在同一数据库事务中用事务级锁串行化,跨进程/重启共享状态。
|
||||
- 429 不携带剩余数量或精确窗口,避免推测其他用户行为。客户端不得自动重试密码读取;提示用户稍后再试,持续超限联系管理员。
|
||||
- 财务客户端设置总超时 10 秒、禁用自动重定向和重试。后端池上限 4、连接获取 1 秒、SQL 执行 1.5 秒/客户端查询 2 秒、锁等待 1 秒,路由运行预算 10 秒。网关必须配置请求体 1 秒和总请求截止,防止慢请求占用。
|
||||
- 入口网关还需按来源 IP 对全部请求(包括未授权请求)限制为 60 次/分钟,返回 429 JSON 和 `no-store`;这项入口防滥用配置属于上线门禁。应用层数据库限流负责已认证的敏感读取。
|
||||
|
||||
## 环境变量(仅名称及要求)
|
||||
|
||||
| 名称 | 所在系统与要求 |
|
||||
|---|---|
|
||||
| STORE_ACCOUNT_ENCRYPTION_KEY | 仅用于兼容历史 `v1:` 密文,至少 32 字符;生产不回退到 JWT_SECRET |
|
||||
| STORE_ACCOUNT_FINANCE_TOKEN | 上游专用服务令牌,密码学随机至少 32 字节,base64url 编码,43–128 字符;缺失时拒绝全部调用 |
|
||||
| STORE_ACCOUNT_FINANCE_TOKEN_PREVIOUS | 上游可选旧令牌,仅用于短时轮换;主令牌缺失时旧令牌也不生效 |
|
||||
| STORE_ACCOUNT_REVEAL_EDGE_TOKEN | 上游与可信 TLS 网关之间的独立随机凭据,base64url 43–128 字符;与财务令牌完全分离,不提供给财务系统;缺失时拒绝全部调用 |
|
||||
| DATABASE_URL | 上游现有数据库连接;仅专用不输出查询/异常的连接池读取密文、写审计 |
|
||||
| STORE_ACCOUNT_REVEAL_URL | 财务后端配置完整 HTTPS 地址模板 |
|
||||
| STORE_ACCOUNT_REVEAL_VISIBLE_PAGE_URL | 财务后端配置当前页批量 HTTPS 地址;必须使用精确 `visible-page` 路径 |
|
||||
| STORE_ACCOUNT_REVEAL_TOKEN | 财务后端保存与上游专用令牌对应的值 |
|
||||
| STORE_REVEAL_TEST_ADMIN_URL | 仅本地集成测试;必须指向回环地址、有建测试库/角色权限的 PostgreSQL |
|
||||
| STORE_REVEAL_TEST_POSTGRES_CONTAINER | 仅本地日志验收,指定本地 PostgreSQL 容器以只读扫描测试期间日志 |
|
||||
|
||||
## HTTPS、日志和缓存上线门禁
|
||||
|
||||
1. 财务后端至公开入口及跨主机的所有代理链路必须使用验证证书的 HTTPS,禁止 HTTP 跳转、跳过证书验证、明文 FRP/跨主机上游。当前 PR 不修改生产 TLS 拓扑;上线前逐跳核实,不满足即不启用令牌。
|
||||
2. 应用不以 URL 或未经认证的转发头证明 TLS。必须由受控网关在实际 HTTPS 入口覆盖 `X-Forwarded-Proto: https` 及 `X-Store-Reveal-Edge-Token`(值来自独立 `STORE_ACCOUNT_REVEAL_EDGE_TOKEN`),剥除客户端原有同名头;HTTP 入口直接拒绝,不能注入上述凭据。应用对网关凭据做定时安全比较,缺失/错误或协议不是 https 时拒绝。凭据仅上游和网关持有,不提供给财务系统;配置文件 0600,不通过 `nginx -T` 等诊断输出展示。仍需封闭应用直连并逐跳验证 TLS,独立凭据不代替网络加密。
|
||||
3. 对精确路径 `/internal/store-accounts/*/password/reveal` 禁止网关缓存、响应落盘缓冲、CDN缓存、请求/响应体及 Authorization 采集;包括 APM、WAF、异常上报、链路追踪和调试抓包。网关日志只允许时间、方法、路径(不带参数)、状态、耗时、追踪 ID。不要采集响应对象。
|
||||
4. 网关自己的 403/404/429/503 也使用脱敏 JSON、`Cache-Control: no-store` 和追踪 ID;不得返回上游诊断页。禁止将该路由接入通用 `withApi`/request logger;目前路由直接返回白名单响应,不记录异常对象。
|
||||
5. 数据库查询结果包含当前明文密码;严禁记录 SQL 结果、绑定参数或异常对象,不要启用该连接池的参数/结果追踪。密码写入及历史格式迁移期间同样关闭 PostgreSQL/pgAudit 的绑定参数和错误参数记录。
|
||||
6. 财务后端同样禁用正文采集、缓存及异常对象打印。浏览器在进入已授权页面后只读取当前可见页,避免 localStorage/sessionStorage/Service Worker 持久化;页面隐藏或离开时清除页面密码,不把密码放入通知、埋点或消息队列。
|
||||
|
||||
## 视图与审计迁移
|
||||
|
||||
初始迁移:`prisma/migrations/20260908120000_store_account_password_reveal/migration.sql`。明文状态修复:`prisma/migrations/20260909100000_fix_store_account_password_configured_plaintext/migration.sql`。
|
||||
|
||||
- 新增 `StoreAccountPasswordAccessAudit`,固定字段为请求追踪 ID、人员标识、服务身份、账号 ID、用途、HTTP状态、数据库时间;不关联用户/店铺外键,以保留未授权、不存在和删除账号的失败审计。状态 200 表示成功,其他状态表示失败。
|
||||
- 只在原视图存在时,以原视图查询作为子查询,在末尾追加 `passwordConfigured boolean`,保留原字段、筛选、每行内容、ID、所有者与 ACL;`EXISTS` 不会产生重复行。未配置外部视图的环境不擅自创建外部读入口。
|
||||
- `passwordConfigured` 的当前定义为 `passwordCiphertext <> ''`。修复迁移仅替换这个布尔表达式,并在事务内核对原字段逐行内容、视图所有者和 ACL 未变;不读取或输出密码。
|
||||
- 明文迁移已由独立脚本完成。本状态修复不改任何密码或银行账号数据,也不增加普通只读账号权限。审计数据至少保留 365 天。
|
||||
|
||||
迁移前由 DBA 只读确认目标库、视图定义/所有者/ACL、字段顺序、行数与唯一性、密码聚合、普通读取账号和其成员角色权限。若生产仍以本次基线验收,COUNT 与 DISTINCT id 必须均为 958;若业务新增/删除则暂停并重新确认基线,不改数据凑数。
|
||||
|
||||
备份与执行顺序(生产必须在明确批准后执行):
|
||||
|
||||
1. 备份当前 `main`/部署 SHA、现有视图 DDL/ACL/所有者及 `_prisma_migrations` 状态。生成目标库 `pg_dump -Fc` 备份并验证可读取;备份进入生产主机权限 0700 目录,文件 0600,不进入 Git 或共享日志。备份可能含密文,按敏感材料管理。
|
||||
2. 本次不修改任何密码、银行卡号或服务令牌;备份不得进入 Git 或普通日志。
|
||||
3. 执行明文状态修复迁移,视图短时获得 DDL 锁(最多等 5 秒,执行预算 30 秒);不回放未审查的其他迁移。
|
||||
4. 在同一只读快照比较迁移前后原字段行集、顺序/类型、行数/唯一性和权限;运行 `scripts/sql/check-store-account-password-reveal.sql`,确认最后字段 boolean,报告 true/false/null 聚合。预计当前数据为 957/1/0,但必须执行后实测。
|
||||
5. 按项目主线发布流程部署,完成逐跳 TLS、网关日志/限流/缓存检查后再配令牌。生产虚构账号的创建、调用与删除属于额外生产写入,需单独批准;不要调用真实账号作为测试。
|
||||
|
||||
回滚:先移除财务令牌阻止新调用并回退应用 SHA。新增状态列与审计表可安全保留以避免丢失审计;旧应用不依赖它们。需要完整结构回退时,经 DBA 检查依赖后,在事务中以备份 DDL 重建旧视图并恢复原所有者/ACL(PostgreSQL 不允许 `CREATE OR REPLACE VIEW` 删除尾列,禁止随意 `DROP ... CASCADE`)。审计记录保留,不直接删表;按项目规则处理已应用迁移历史,禁止直接删除 `_prisma_migrations` 记录或用 `db push`。任何完整结构回退先在隔离库演练。
|
||||
|
||||
## 密钥轮换
|
||||
|
||||
服务令牌:在受限凭据系统生成新随机令牌;旧值暂放 `STORE_ACCOUNT_FINANCE_TOKEN_PREVIOUS`,新值放主变量,重启上游;更新财务后端,使用虚构账号验收;移除旧值并重启。两值对应同一服务身份,审计/额度不变。怀疑泄露时立即撤销旧值,宁可短暂拒绝请求。
|
||||
|
||||
网关凭据:暂停此接口,在网关及上游同时更新 `STORE_ACCOUNT_REVEAL_EDGE_TOKEN` 后验收并恢复;不支持旧值并行。不得将网关凭据与服务令牌设成相同值。
|
||||
|
||||
历史兼容密钥:当前明文规则不需要为新值轮换数据密钥。若确认数据库中不存在 `v1:` 值并且备份保留策略允许,可另行评估退役历史解密密钥;退役前必须检查备份恢复场景,不能直接删除。
|
||||
|
||||
## 本次验证记录
|
||||
|
||||
- 当前页批量扩展的服务、HTTP 路由和代理边界共 3 个测试套件、41 项测试通过;`npm run type-check` 通过。测试覆盖 1–50 个账号、重复账号拒绝、部分 404/429/503、批次与每账号审计,以及失败时不返回明文。
|
||||
- 单元/HTTP/代理路由测试:34 项通过,含伪造 HTTPS URL/转发头和错误网关凭据拒绝。
|
||||
- 追加现有代理路由的 7 项回归,与 10 项集成测试合计 6 套件、51 项全部通过。
|
||||
- 隔离本地 PostgreSQL 集成测试:10 项通过,覆盖完整加解密与 HTTP handler、已提交审计、403/404/429/no-store、并发/跨实例限流、分钟/24小时配额、审计失败拒绝泄露、原字段及筛选保留、958/958 与 boolean 957/1/0、普通只读角色拒绝基表/密码/审计读取。
|
||||
- 开启本地测试连接的 PostgreSQL statement 日志,只在内存扫描 `docker logs`;应用日志、审计行、SQL请求与真实数据库日志均未检出随机测试密码。测试无需打印测试密码,断言只输出布尔结果。
|
||||
- Type-check 通过;范围内 ESLint 通过。全仓库主数据边界巡检有 6 项已有违规,均位于本次未修改模块;不得据此声称全仓库通过。
|
||||
- 2026-09-09 生产只读复核确认:958 行、958 个唯一 ID、957 条非空密码、1 条空密码;旧正则导致状态误报为全 false。本迁移用于恢复预期聚合 true/false/null = 957/1/0。
|
||||
|
||||
运行:`npx jest --runInBand --runTestsByPath src/server/services/store-account-password-reveal.service.test.ts src/proxy-store-account-reveal.test.ts "src/app/internal/store-accounts/[id]/password/reveal/route.test.ts" tests/integration/store-account-password-reveal.test.ts`。未配置本地测试库变量时,集成套件明确 skip,不能算真实数据库验收通过。
|
||||
|
||||
收口:**passed with warnings**。这是自审,不是独立复审。Route → Service → Repository 边界保持;未修改 UI;状态字段与当前明文存储规则对齐,专用认证、审计和限流边界不变。
|
||||
101
furniture-3d-system/docs/workflow/1688-dropship-integration.md
Normal file
101
furniture-3d-system/docs/workflow/1688-dropship-integration.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 1688 Dropship Integration
|
||||
|
||||
## Scope
|
||||
|
||||
The module at `/supply-chain/new-products/1688-dropship-data` includes the DF
|
||||
product table, date and multi-select column filters, real-performance ordering,
|
||||
and daily/weekly/monthly trend details with prior-month comparisons.
|
||||
|
||||
It reads the shared product archive and daily sales fact table. The shared
|
||||
`barcode + statDate` key, unmatched source barcodes, synchronization lock,
|
||||
staging, and atomic publication are preserved. No second daily table or
|
||||
archive foreign key is introduced.
|
||||
|
||||
## Field Contract
|
||||
|
||||
| Display | Source / calculation |
|
||||
| --- | --- |
|
||||
| Sales amount | `sales_amount` -> daily `salesAmount` |
|
||||
| Real performance | `real_performance` |
|
||||
| After-sale amount | `after_sale_amount` |
|
||||
| After-sale cost rate | After-sale amount / real performance; blank for zero denominator |
|
||||
| Gross profit | Real performance - clear sales cost - send freight |
|
||||
| SKU operator | DF barcode/day distinct `ider`, latest nonempty value in selected period |
|
||||
| Total cost | Latest available daily `unitCost + estimatedFreight` in selected period; missing components are displayed as zero |
|
||||
| Unit cost | Latest available daily DF cost from `goods_count = 1` observations; the existing product cost excludes freight; missing values are displayed as zero |
|
||||
| Estimated freight | Average `send_freight` for the same DF barcode/day where `goods_count = 1`; the stored value remains null when no quantity-one observations exist and the page displays zero |
|
||||
|
||||
The archive's existing `shipAmount` continues to use `clear_goods_cost`.
|
||||
It must not be substituted for the dropship module's `salesAmount`.
|
||||
Cost ties use quantity-one frequency, total frequency, then descending unit cost. The page displays total cost before unit cost and estimated freight.
|
||||
|
||||
## Deployment Prerequisites
|
||||
|
||||
Code publication is not production deployment or database/data synchronization.
|
||||
No credentials, database dumps, local accounts, or local environment files are
|
||||
included in this change.
|
||||
|
||||
1. Confirm the target PostgreSQL identity, backup, and migration readiness using
|
||||
the repository deployment/database workflow. Apply migration
|
||||
`20260912120000_add_daily_sales_dropship_fields` after the existing mainline
|
||||
daily-sales migration. Do not replay the old local prototype's daily-table
|
||||
creation migrations against the mainline database.
|
||||
2. Generate the Prisma client and build the application with the target's
|
||||
existing environment configuration.
|
||||
3. Run the existing `npm run sync:supply-chain-product-archive-daily --` command
|
||||
with `--backfill`, `--apply`,
|
||||
`--confirm=sync-supply-chain-product-archive-sales-performance-daily`,
|
||||
`--expected-database-identity=...`, and
|
||||
`--expected-source-database-identity=...` using verified identities.
|
||||
The upstream requires read access to the configured
|
||||
`JST_REPORT_DB_SCHEMA.dws_sn_sales_full_info_di`; credentials remain in the
|
||||
target environment's `JST_REPORT_DB_*` variables.
|
||||
4. Check the completed sales sync run, historic DF `salesAmount` completeness,
|
||||
module view permission `newProduct1688DropshipData`, images, filters, totals,
|
||||
and trend comparisons. The existing daily schedule then maintains the data.
|
||||
|
||||
The migrations add `skuOperator` (empty by default), `unitCost` (nullable),
|
||||
`estimatedFreight` (nullable), and `salesAmount` (nullable). Historical facts need a backfill; the ordinary
|
||||
seven-day refresh does not populate all older dates. Until backfilled, unknown
|
||||
sales totals display as `--`, not zero or shipped amount.
|
||||
|
||||
Filtering uses the existing cache service with user-scoped keys and a 24-hour
|
||||
TTL. Multi-process deployments should provide the existing shared Redis cache.
|
||||
The module uses its own authenticated DF-only image route under
|
||||
`/api/supply-chain/1688-dropship-data/source-image`. Its view permission does not
|
||||
require granting access to the full product archive; the shared image service
|
||||
and its source/URL restrictions are retained.
|
||||
Rollback can restore the prior application version while retaining the additive
|
||||
columns; do not drop the daily table or discard its history.
|
||||
|
||||
## Validation And Review
|
||||
|
||||
Independent review conclusion: `passed with warnings`. Architecture and
|
||||
root-cause handling passed, including the corrected image-permission boundary.
|
||||
The existing visual implementation was retained; target-environment screenshots
|
||||
and real-data acceptance were not performed in this integration.
|
||||
|
||||
Final integration includes shared-mainline update `5156ead4`, retaining its
|
||||
separate dropship-issues entry and permissions. After merging, 20 focused Jest
|
||||
suites (171 tests), TypeScript checking, relevant ESLint, and the production
|
||||
build passed. The build retains two unrelated broad-file-pattern warnings in
|
||||
the existing OCR route and DWG wrapper.
|
||||
|
||||
The integration was checked with focused Jest suites for the page, trends,
|
||||
filters, permissions, image route, service/repository mapping, and shared daily
|
||||
sync; TypeScript checking; changed-file ESLint; and a production build using
|
||||
isolated placeholder configuration (no live database). An in-memory PostgreSQL
|
||||
compatibility replay confirmed that the additive migration retains existing
|
||||
DF/non-DF facts and the original primary key. Image-route tests exercise the
|
||||
real request/auth wrapper, including module-only access and denied requests.
|
||||
|
||||
The repository-wide `db:verify-master-data:code` and
|
||||
`db:verify-master-data:truth` audits still fail on existing baseline issues.
|
||||
Independent comparison with clean shared-mainline commit `38259cc4` found no
|
||||
new blocking findings: code-audit finding content is unchanged apart from
|
||||
schema line offsets, and the truth-audit JSON is identical. These global audit
|
||||
failures are not presented as passing checks.
|
||||
|
||||
No target deployment, live target database migration/backfill, or live-data
|
||||
browser validation of the integrated version is included in this publication.
|
||||
The existing local test application and its prototype database are unchanged.
|
||||
@ -0,0 +1,62 @@
|
||||
# CDN 直连上线交接(2026-06-06)
|
||||
|
||||
适用范围:`furniture-3d-system` 正式环境对象读取链路。
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
- 七牛 CDN 直连方案已在正式环境落地。
|
||||
- 业务系统继续保留系统内稳定素材入口,不把七牛域名、bucket 或签名 URL 写死到业务页面和数据库字段里。
|
||||
- 正式环境的对象读取链路已经从“应用服务器代理素材响应体”切到“应用鉴权后 `307` 跳转到七牛短时签名 CDN 地址”。
|
||||
|
||||
## 2. 当前正式环境口径
|
||||
|
||||
- 正式站点:`https://cp.jiansun.vip`
|
||||
- 七牛空间:`sucaiku-js`
|
||||
- 正式 CDN 域名:`https://qnycdn.jiansun.vip`
|
||||
- 生产环境变量口径:`QINIU_PUBLIC_DOMAIN=https://qnycdn.jiansun.vip`
|
||||
- 素材真相口径:
|
||||
- 数据库继续保存系统稳定地址或对象 key
|
||||
- 登录态素材稳定入口:`/api/storage/object?key=...`
|
||||
- 分享态素材稳定入口:`/api/share/[token]/object?key=...`
|
||||
- 服务端按当前 provider 生成短时签名外链,浏览器最终直连 CDN
|
||||
|
||||
## 3. 这次已确认的内容
|
||||
|
||||
- 代码侧已具备正式直连能力:
|
||||
- `src/lib/storage.ts` 已明确区分稳定客户端入口和服务端直读地址
|
||||
- `src/lib/object-storage-read-response.ts` 已支持 `HTTPS -> 307`
|
||||
- `src/app/api/share/[token]/object/route.ts` 已存在
|
||||
- `next.config.js` 已放行正式 CDN 域名
|
||||
- 生产配置已切到正式 CDN 域名:`QINIU_PUBLIC_DOMAIN=https://qnycdn.jiansun.vip`
|
||||
- 生产侧配置只读校验已通过:`npm run storage:verify-config`
|
||||
- 真实业务侧抽验已完成:
|
||||
- 页面:`https://cp.jiansun.vip/asset-center/material-library`
|
||||
- 页面内真实纹理素材使用系统稳定入口 `/api/storage/object?...`
|
||||
- 对该真实业务素材地址做校验时,服务端返回 `HTTP 307`
|
||||
- `Location` 最终落到 `https://qnycdn.jiansun.vip/...`
|
||||
|
||||
## 4. 这次没走的路
|
||||
|
||||
- 审批访问页不是匿名链路,页面和接口都要求登录,不能拿它当公开验收入口。
|
||||
- 当时生产里没有现成可直接拿来做公开分享验收的正式素材,所以分享页没有完成页面级抽验。
|
||||
- 本次“真实业务侧”验收最终选用的是已登录正式环境中的资料页,而不是临时造生产数据。
|
||||
|
||||
## 5. 后续建议补的检查
|
||||
|
||||
1. 再抽验 `产品详情 / SKU 详情 / 3D 模型页`,确认模型和贴图都走 `307 -> qnycdn.jiansun.vip`。
|
||||
2. 条件允许时补一轮 `分享页` 页面级验收,确认公开链路也落到正式 CDN。
|
||||
3. 观察一到两天生产监控,重点看应用服务器出口流量、页面加载耗时、七牛命中率和回源量。
|
||||
4. 后续任何人如果更换 bucket、CDN 域名或对象存储 provider,只改环境变量和 `src/lib/storage.ts` 的 storage adapter,不改业务页链接逻辑。
|
||||
|
||||
## 6. 回归检查清单
|
||||
|
||||
- `GET /api/storage/object?key=...` 对浏览器可直连资源仍返回 `307`
|
||||
- `Location` 仍指向正式 CDN 域名,而不是旧测试域名或应用服务器自身
|
||||
- `next.config.js` 与 CSP 仍放行 `https://qnycdn.jiansun.vip`
|
||||
- 数据库里没有把短时签名 URL 当长期字段保存
|
||||
- 分享页和审批页没有在前端直接拼七牛域名
|
||||
|
||||
## 7. 回滚提醒
|
||||
|
||||
- 不能只改 `QINIU_PUBLIC_DOMAIN` 就当回滚完成。
|
||||
- 如果后续要回退,优先按“运行代码版本 + 共享环境变量”成组回退,避免出现“旧代码配新域名”或“新代码配旧口径”的半回滚状态。
|
||||
@ -1,64 +1,77 @@
|
||||
# 主线同步操作手册
|
||||
|
||||
适用对象:会用 Codex 协作、但不一定熟悉 Git 的同事。
|
||||
目标:当共享开发主线持续更新时,你仍然能把自己的分支保持在最新状态,不把代码做旧,不把冲突拖到最后一天。
|
||||
目标:让每个分支都能跟上共享开发主线,不把旧代码、旧冲突和旧部署口径拖到最后。
|
||||
|
||||
---
|
||||
|
||||
## 1. 先记住一句话
|
||||
|
||||
你不要直接改 `main`。
|
||||
你只在自己的分支里开发,然后经常把最新的 `origin/codex/xuxiake-mainline` 同步进来。
|
||||
不要直接改 `main`。
|
||||
日常开发从最新 `origin/codex/xuxiake-mainline` 开自己的任务分支,并且经常把共享开发主线同步进来。
|
||||
|
||||
当前推荐分工:
|
||||
当前分工:
|
||||
|
||||
- `WuYongJie` 负责最终发布主线 `main`
|
||||
- 日常开发默认以 `origin/codex/xuxiake-mainline` 为共享开发主线
|
||||
- 其他协作者在自己的长期共享分支或临时任务分支开发
|
||||
- `main` 是最终发布主线,由 `WuYongJie` 收口。
|
||||
- `origin/codex/xuxiake-mainline` 是共享开发主线和默认同步基线。
|
||||
- 其他协作者在自己的长期共享分支或临时任务分支开发。
|
||||
|
||||
分支命名固定规则:
|
||||
分支命名:
|
||||
|
||||
- 新分支统一用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`
|
||||
- `main` 只由 `WuYongJie` 收口,其他协作者只在自己的分支里同步和开发
|
||||
- 长期共享分支和临时任务分支都要把当前工作域写进分支名
|
||||
- 不再使用 `codex/xuxiake-dev` 这种看不出职责的泛分支名
|
||||
- 新分支用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
|
||||
- 不再使用 `codex/xuxiake-dev`、`feature/packaging`、`packaging/...` 这类泛分支名。
|
||||
- 分支名要能看出负责人和工作域。
|
||||
|
||||
---
|
||||
|
||||
## 2. 你每天就做这 3 件事
|
||||
## 2. 每次开工先查真实状态
|
||||
|
||||
### 开工前
|
||||
在 `furniture-3d-system/` 所在仓库根目录执行:
|
||||
|
||||
先同步一次最新主线。
|
||||
```bash
|
||||
git fetch origin
|
||||
git status --short --branch
|
||||
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
|
||||
```
|
||||
|
||||
### 主负责人刚推了重要改动后
|
||||
怎么看:
|
||||
|
||||
再同步一次,不要继续拿旧代码往下写。
|
||||
- `git status --short --branch` 会显示当前分支和未提交改动。
|
||||
- `rev-list` 左边数字是当前分支独有提交数。
|
||||
- `rev-list` 右边数字是 `origin/codex/xuxiake-mainline` 独有提交数。
|
||||
- 右边大于 0,说明你落后共享开发主线。
|
||||
- 左边大于 0,说明你当前分支有自己的提交。
|
||||
|
||||
### 你准备提交前
|
||||
如果只是想确认远端最新提交,不要靠记忆,直接查:
|
||||
|
||||
最后再同步一次,避免你推上去的分支已经落后很多。
|
||||
```bash
|
||||
git fetch origin
|
||||
git ls-remote origin refs/heads/codex/xuxiake-mainline refs/heads/main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 默认操作步骤
|
||||
## 3. 有未提交改动时不要同步
|
||||
|
||||
先加一个固定前置动作:
|
||||
先看:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
如果这里不是空的,先把当前改动处理好再同步主线:
|
||||
如果不是空的,先处理当前改动:
|
||||
|
||||
- 要保留并提交:先提交到你当前分支
|
||||
- 只是临时改动:先暂存(stash)再继续
|
||||
- 要保留:先提交到当前分支。
|
||||
- 只是临时现场:先 `stash`,并写清楚是什么现场。
|
||||
- 是别人或其他 agent 的文件:不要擅自回退,不要擅自删除。
|
||||
|
||||
不要在“有一堆未提交改动”时直接 `merge origin/codex/xuxiake-mainline` 或 `rebase origin/codex/xuxiake-mainline`。
|
||||
不要在一堆未提交改动上直接 `merge` 或 `rebase`。
|
||||
|
||||
---
|
||||
|
||||
如果你现在在长期共享分支,就把下面这 5 行命令原样交给 Codex 执行:
|
||||
## 4. 长期共享分支用 merge
|
||||
|
||||
长期共享分支是别人可能会继续接手或反复同步的分支。固定用 `merge`:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
@ -67,118 +80,153 @@ git merge origin/codex/xuxiake-mainline
|
||||
git push origin <你的长期共享分支>
|
||||
```
|
||||
|
||||
这组命令的白话意思:
|
||||
白话意思:
|
||||
|
||||
1. 先刷新远端分支信息
|
||||
2. 切回你的长期共享分支
|
||||
3. 把最新共享开发主线合进这个共享分支
|
||||
4. 再把共享分支推上去
|
||||
1. 刷新远端状态。
|
||||
2. 回到你的长期共享分支。
|
||||
3. 把最新共享开发主线合进来。
|
||||
4. 推回远端。
|
||||
|
||||
长期共享分支不要 `rebase`,不要 `push --force`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 如果你临时开了个人任务分支
|
||||
## 5. 个人任务分支可以 rebase
|
||||
|
||||
只有在你从长期共享分支再开出个人任务分支时,才用下面这套:
|
||||
只有确认这个分支没有别人共用时,才可以用:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <你的分支名>
|
||||
git checkout <你的个人任务分支>
|
||||
git rebase origin/codex/xuxiake-mainline
|
||||
git push --force-with-lease origin <你的分支名>
|
||||
git push --force-with-lease origin <你的个人任务分支>
|
||||
```
|
||||
|
||||
它的特点:
|
||||
怎么选:
|
||||
|
||||
- 好处:任务分支提交更直
|
||||
- 代价:会改写历史,不适合长期共享分支
|
||||
- 长期共享分支:用 `merge`。
|
||||
- 只有你自己使用的短期任务分支:可以用 `rebase`。
|
||||
- 以后要交给别人继续接的分支:按长期共享分支处理。
|
||||
|
||||
怎么选最简单:
|
||||
|
||||
- 长期共享分支固定用 `merge`
|
||||
- 只有你一个人在用的短期任务分支,才用 `rebase`
|
||||
- 如果这个分支会被别人继续接手或反复同步,就按长期共享分支处理,不能为了图直线历史去改写公共提交
|
||||
`--force-with-lease` 只允许用在个人任务分支,不能用在共享分支、`main` 或 `origin/codex/xuxiake-mainline`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 什么时候说明你已经落后主线了
|
||||
## 6. 什么时候必须同步
|
||||
|
||||
出现下面任意一种情况,就不要继续写了,先同步:
|
||||
出现下面任意情况,先同步再继续:
|
||||
|
||||
- 主负责人刚改了你也会碰到的模块
|
||||
- Codex 提示分支落后
|
||||
- 你打开页面后,发现和主线最新效果不一样
|
||||
- 你准备提交,但已经隔了半天或一天没同步
|
||||
- 用户问“xuxiake 主线有没有新提交”。
|
||||
- 你准备开始一个新任务。
|
||||
- 主负责人刚推了会影响你模块的改动。
|
||||
- 你准备提交或部署。
|
||||
- 你发现页面表现和主线最新效果不一致。
|
||||
- 你已经半天或一天没同步过。
|
||||
|
||||
同步前仍然要先处理未提交改动。
|
||||
|
||||
---
|
||||
|
||||
## 6. 如果同步时出现冲突
|
||||
## 7. 冲突处理
|
||||
|
||||
不要慌,按这个顺序处理:
|
||||
同步时遇到冲突,不要全选自己的版本,也不要全选主线版本。
|
||||
|
||||
1. 先停下来,不要乱点保存
|
||||
2. 看冲突文件是不是你这次本来就在改的模块
|
||||
3. 如果不是你负责的模块,先问主负责人
|
||||
4. 如果是你负责的模块,就保留你的目标改动,再把主线的新内容接进去
|
||||
5. 处理完后再继续让 Codex 帮你完成 `rebase` 或 `merge`
|
||||
处理顺序:
|
||||
|
||||
可以直接把下面这段话发给 Codex:
|
||||
1. 先停下来,看冲突文件属于哪个模块。
|
||||
2. 如果不是你负责的模块,先问主负责人。
|
||||
3. 如果是你负责的模块,优先保留主线最新结构,再把当前分支的目标改动接进去。
|
||||
4. 只处理冲突文件,不趁机改无关模块。
|
||||
5. 处理后跑最小验证,再继续 merge 或 rebase。
|
||||
|
||||
可以直接让 Codex 按下面口令处理:
|
||||
|
||||
```text
|
||||
我在同步 `origin/codex/xuxiake-mainline` 到我的分支时遇到冲突了。
|
||||
请只帮我处理这几个冲突文件,不要扩范围,不要改无关模块。
|
||||
优先保留主线最新结构,再把我这个分支的目标改动接进去。
|
||||
处理后请告诉我还需要执行什么命令。
|
||||
我在同步 `origin/codex/xuxiake-mainline` 到当前分支时遇到冲突。
|
||||
请只处理冲突文件,不扩范围,不回退无关改动。
|
||||
优先保留主线最新结构,再把当前分支的目标改动接进去。
|
||||
处理后告诉我需要继续执行什么命令和验证。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 绝对不要这样做
|
||||
## 8. 主线负责人怎么收分支
|
||||
|
||||
- 不要直接在 `main` 上开发
|
||||
- 不要长时间不同步主线
|
||||
- 不要等“全部做完了”才第一次同步
|
||||
- 不要把 `dev` 当万能长期分支名
|
||||
- 不要对长期共享分支执行 `git rebase origin/codex/xuxiake-mainline`
|
||||
- 不要看到冲突就随手全选自己的版本
|
||||
- 不要在没看懂的情况下执行 `git reset --hard`
|
||||
当同事分支已经推上来后,`WuYongJie` 按这个顺序看:
|
||||
|
||||
1. 看交接说明,确认本次做了什么、没做什么、验证跑了什么。
|
||||
2. 看分支是否同步过最新 `origin/codex/xuxiake-mainline`。
|
||||
3. 看改动是否超出任务范围。
|
||||
4. 看是否碰到共享接缝。
|
||||
5. 决定是 `merge`、`cherry-pick`,还是手工整合。
|
||||
|
||||
最简单判断:
|
||||
|
||||
- 分支干净、范围对、验证齐:`merge`。
|
||||
- 只有部分提交该进主线:`cherry-pick`。
|
||||
- 目标能收,但历史太乱或冲突太多:手工整合。
|
||||
- 没同步、没说明、没验证:先不收。
|
||||
|
||||
共享接缝要多看一层:
|
||||
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations/`
|
||||
- `src/app/layout.tsx`
|
||||
- `src/app/intake/`
|
||||
- `src/app/draft-groups/[id]/`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
|
||||
---
|
||||
|
||||
## 8. 主线负责人怎么收同事分支
|
||||
## 9. 发布前的主线核验
|
||||
|
||||
当同事分支已经推上来后,`WuYongJie` 固定按下面顺序做:
|
||||
生产发布必须由用户明确授权。
|
||||
|
||||
1. 先看交接说明,确认本次只做什么、没做什么、验证跑了什么
|
||||
2. 看分支有没有同步最新 `origin/codex/xuxiake-mainline`
|
||||
3. 看改动有没有超出任务范围,或者碰到共享接缝
|
||||
4. 再决定这次是 `merge`、`cherry-pick`,还是手工整合
|
||||
发布代码默认来自 `origin/main`,部署入口以仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1` 为准,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。
|
||||
|
||||
最简单判断法:
|
||||
部署前至少核对:
|
||||
|
||||
- 分支干净、范围对、验证齐:`merge`
|
||||
- 只有部分提交该进主线:`cherry-pick`
|
||||
- 目标能收,但提交历史太乱或冲突太多:手工整合
|
||||
- 没同步、没说明、没验证:先不收
|
||||
```bash
|
||||
git fetch origin
|
||||
git ls-remote origin refs/heads/main
|
||||
git rev-parse origin/main
|
||||
```
|
||||
|
||||
如果同事改到了 `draft-group / intake / schema.prisma / src/app/draft-groups/[id]` 这类共享接缝,再多看一步:
|
||||
如果本地 `origin/main` 和远端 `refs/heads/main` 不一致,先处理同步问题,不要部署。
|
||||
|
||||
- 为什么非改不可
|
||||
- 这次改动是不是只为当前任务服务
|
||||
- 有没有把包装逻辑带进正式入库或轻仓库主链
|
||||
部署后不能只看公网 `/api/health`。还要按生产部署手册核对:
|
||||
|
||||
- 生产当前 commit。
|
||||
- PM2 进程状态。
|
||||
- 应用端口。
|
||||
- 最近错误日志。
|
||||
- 关键页面和关键接口。
|
||||
|
||||
---
|
||||
|
||||
## 9. 给同事最短口令版
|
||||
## 10. 绝对不要这样做
|
||||
|
||||
如果你懒得记,只记这一段:
|
||||
- 不要直接在 `main` 上开发。
|
||||
- 不要长时间不同步共享开发主线。
|
||||
- 不要等全部做完才第一次同步。
|
||||
- 不要用 `dev` 这种看不出职责的长期分支名。
|
||||
- 不要对长期共享分支执行 `rebase`。
|
||||
- 不要对共享分支 `push --force`。
|
||||
- 不要看到冲突就全选自己的版本。
|
||||
- 不要在没看懂的情况下执行 `git reset --hard`。
|
||||
- 不要把生产账号密码、访问密钥、数据库连接串、对象存储密钥写进 Git 文档;非密钥运行入口必须先核对当前现场再写。
|
||||
|
||||
---
|
||||
|
||||
## 11. 最短口令版
|
||||
|
||||
```text
|
||||
每天开工前,先 fetch 远端,再把 `origin/codex/xuxiake-mainline` merge 进你的长期共享分支。
|
||||
只有你临时开的个人任务分支才用 rebase。
|
||||
如果冲突了,不要硬处理,先让 Codex 只处理冲突文件。
|
||||
每天开工前先 fetch,再看 status 和当前分支相对 `origin/codex/xuxiake-mainline` 的 ahead/behind。
|
||||
长期共享分支同步主线用 merge。
|
||||
只有个人临时任务分支才可以 rebase。
|
||||
冲突时只处理冲突文件,优先保留主线最新结构,再接回自己的目标改动。
|
||||
生产部署前必须核对 origin/main 和远端 main,部署后要核对 commit、进程、端口、日志和关键页面。
|
||||
```
|
||||
|
||||
补充说明:
|
||||
|
||||
- 历史文档里如果还出现固定包装分支名,那是旧协作阶段留下的例子,不再代表当前唯一允许的长期分支。
|
||||
- 当前哪条分支算“长期共享分支”,以最新交接、当前任务边界和实际协作方式为准。
|
||||
|
||||
54
furniture-3d-system/docs/workflow/产品全量档案图片索引操作说明.md
Normal file
54
furniture-3d-system/docs/workflow/产品全量档案图片索引操作说明.md
Normal file
@ -0,0 +1,54 @@
|
||||
# 产品全量档案图片索引操作说明
|
||||
|
||||
全量档案本地图片索引只读取 `SupplyChainProductArchive.image`。上游「我司 SKU 命中」继续使用现有独立图搜服务。
|
||||
|
||||
## 更新规则
|
||||
|
||||
- 有图片且没有索引:建立索引。
|
||||
- 图片地址变化、旧索引失效、模型版本或向量规格变化:重新索引。
|
||||
- 图片为空或地址已经变化:旧向量退出本地搜索。任务会把旧记录标为 `STALE`。
|
||||
- 下载或计算失败:保存 `FAILED` 和原因,六小时后再次尝试;图片地址再次变化则立即处理。
|
||||
- 图片内容相同、地址不同:复用向量并更新地址与状态,避免反复处理同一张图片。
|
||||
- 下载、计算期间图片改变或产品删除:提交前锁定并核对档案行,丢弃过期结果,下一轮处理最新图片。
|
||||
- 所有索引任务使用同一 PostgreSQL 会话锁,避免手动补索引、同步触发与定时任务并发计算。
|
||||
- 正常增量任务不会重新下载全部已就绪图片;同一 URL 下替换了图片内容时,使用完整内容核验命令。
|
||||
|
||||
## 本地命令
|
||||
|
||||
在主应用目录执行,模型沿用现有 `IMAGE_SEARCH_MODEL_PATH` 或 `.local-services/models/dinov2-small`。
|
||||
|
||||
```bash
|
||||
# 每次最多处理 200 条缺失、换图或到期重试记录
|
||||
npm run image-search:index
|
||||
|
||||
# 补齐当前全部待处理记录,并立即重试历史失败
|
||||
npm run image-search:index -- --limit=0 --retry-now
|
||||
|
||||
# 下载所有带图档案,核验同一 URL 下的图片内容变化
|
||||
npm run image-search:index -- --all --limit=0
|
||||
```
|
||||
|
||||
命令持续输出已处理、成功、复用、失败、跳过、过期结果丢弃和失效条数;失败行输出条码与原因。最后输出本轮结果及待处理数量。失败仍会进入后续重试,本轮失败数必须单独关注,`remaining` 只计算已到重试时间的记录。
|
||||
|
||||
## 生产启用
|
||||
|
||||
代码必须先进入被授权的 `origin/main`,再经统一部署入口发布。安装定时器与首次补索引按用户授权执行,不能把本地开发完成等同于生产已启用。
|
||||
|
||||
安装入口:`scripts/install-product-archive-image-index-timer.sh`。它要求现有生产模型文件齐全,并让任务显式使用共享模型目录。当前应用启动命令已注入模型路径,无须为本功能重写应用环境。
|
||||
|
||||
- 服务:`furniture-product-archive-image-index.service`。
|
||||
- 定时器:`furniture-product-archive-image-index.timer`,每五分钟检查,单次最多处理 200 条,失败六小时后重试。
|
||||
- 两个现有全量同步服务分别增加本功能自己的 `image-index.conf`,同步成功后异步启动索引服务。
|
||||
- 前台保存、导入及其他来源的图片变化由五分钟定时检查覆盖;索引下载与推理不阻塞保存请求。
|
||||
- 通过 `PRODUCT_ARCHIVE_IMAGE_INDEX_LIMIT` 可在安装时调整单次处理上限。
|
||||
- 脚本和日志通过发布目录与 systemd 管理,不手工修改发布目录。
|
||||
|
||||
```bash
|
||||
bash scripts/install-product-archive-image-index-timer.sh
|
||||
systemctl status furniture-product-archive-image-index.timer --no-pager
|
||||
journalctl -u furniture-product-archive-image-index.service -n 50 --no-pager
|
||||
```
|
||||
|
||||
首次补齐历史缺口可等待定时任务分批完成;需要一次补完时,手动索引命令必须指向共享模型路径。运行中的另一索引任务会返回 `LOCKED`,稍后重试。
|
||||
|
||||
验收应核对有图档案与索引的关联,而不是只看 `READY` 行数:当前地址一致、状态 `READY`、模型版本与 768 维向量均符合要求才是可用索引。历史 404 或不可读取地址不能靠索引任务生成图片,需要按条码定位源图片问题。
|
||||
@ -11,6 +11,7 @@
|
||||
- 所属模块:
|
||||
- 负责人:
|
||||
- 所在分支:
|
||||
- 同步基线:`origin/codex/xuxiake-mainline`
|
||||
- 计划开始时间:
|
||||
|
||||
---
|
||||
@ -59,6 +60,11 @@
|
||||
|
||||
-
|
||||
|
||||
### SPU / SKU / barcode / 批次
|
||||
|
||||
- 是否涉及:
|
||||
- 如果涉及,四者分别是什么:
|
||||
|
||||
### 文档
|
||||
|
||||
-
|
||||
@ -142,8 +148,15 @@
|
||||
- 至少跑 `npm run type-check`
|
||||
- 按需跑相关测试
|
||||
- 能跑就跑 `npm run build`
|
||||
- 前端改动按 1920x1080 做浏览器自查
|
||||
- 涉及部署时核对 `origin/main`、PM2、端口、日志和关键页面
|
||||
|
||||
需要同步更新的文档:
|
||||
- <文档路径 1>
|
||||
- <文档路径 2>
|
||||
|
||||
规范审查:
|
||||
- 架构有没有被破坏
|
||||
- 视觉有没有跑偏
|
||||
- 这次是不是根因修复
|
||||
```
|
||||
|
||||
@ -5,14 +5,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. 先把这 5 句话说清楚
|
||||
## 1. 先把这几句话说清楚
|
||||
|
||||
- 这次要做的模块是什么
|
||||
- 现在做到哪一步了
|
||||
- 你接下来只需要做什么
|
||||
- 哪些目录不能碰
|
||||
- 你应该在哪个分支继续做
|
||||
- 是否已经同步 `origin/codex/xuxiake-mainline`
|
||||
- 做完后怎么验证
|
||||
- 规范审查结论是什么
|
||||
|
||||
---
|
||||
|
||||
@ -21,14 +23,20 @@
|
||||
```text
|
||||
任务名称:<名称>
|
||||
目标分支:<固定写你要推到哪个分支,例如 codex/<负责人>-<工作域> 或 codex/<负责人>-<工作域>-<任务简称>>
|
||||
同步基线:origin/codex/xuxiake-mainline
|
||||
当前状态:<已完成到哪里>
|
||||
你接手的部分:<只写这次要做的内容>
|
||||
分支名称:<默认写 codex/<负责人>-<工作域>;如果是临时小任务,再写 codex/<负责人>-<工作域>-<任务简称>>
|
||||
本次只改:<一句话说清本次改动范围>
|
||||
本次明确没改:<一句话写清这次没碰什么>
|
||||
是否碰共享接缝:<没有 / 有,改了哪些,为什么非改不可>
|
||||
是否涉及 SPU / SKU / barcode / 批次:<没有 / 有,分别是什么,不能混写>
|
||||
|
||||
先看这几个文件:
|
||||
- AGENTS.md
|
||||
- docs/workflow/团队协作模式.md
|
||||
- docs/workflow/模块边界说明.md
|
||||
- docs/workflow/主线同步操作手册.md
|
||||
- <文件 1>
|
||||
- <文件 2>
|
||||
- <文件 3>
|
||||
@ -46,11 +54,16 @@
|
||||
- `npm run type-check`
|
||||
- `npm test -- --runTestsByPath <补具体文件>`
|
||||
- `npm run build`
|
||||
- 前端改动按 1920x1080 浏览器自查
|
||||
- 人工打开:<页面路径>
|
||||
|
||||
风险与待主线决定:
|
||||
- <风险 1>
|
||||
- <是否建议 merge / cherry-pick / 手工整合>
|
||||
|
||||
规范审查:
|
||||
- 这是自审,不是独立复审
|
||||
- 结论:<passed / passed with warnings / rejected>
|
||||
```
|
||||
|
||||
---
|
||||
@ -66,7 +79,16 @@
|
||||
当前背景:
|
||||
<用 3 到 5 行说清楚现在已经做到哪一步>
|
||||
|
||||
开工前请先执行:
|
||||
- `git fetch origin`
|
||||
- `git status --short --branch`
|
||||
- `git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline`
|
||||
|
||||
先看这些文件:
|
||||
- AGENTS.md
|
||||
- docs/workflow/团队协作模式.md
|
||||
- docs/workflow/模块边界说明.md
|
||||
- docs/workflow/主线同步操作手册.md
|
||||
- <文件 1>
|
||||
- <文件 2>
|
||||
- <文件 3>
|
||||
@ -88,6 +110,7 @@
|
||||
- 至少跑 `npm run type-check`
|
||||
- 按需跑相关测试
|
||||
- 能跑就跑 `npm run build`
|
||||
- 如果是前端改动,请按 1920x1080 做浏览器自查
|
||||
|
||||
如果你当前在长期共享分支上,请先同步共享开发主线再继续开发:
|
||||
- `git fetch origin`
|
||||
@ -104,6 +127,7 @@
|
||||
2. 是否完成
|
||||
3. 还有没有风险
|
||||
4. 本次建议主线怎么收:`merge` / `cherry-pick` / `手工整合`
|
||||
5. 规范审查结论:`passed` / `passed with warnings` / `rejected`
|
||||
```
|
||||
|
||||
---
|
||||
@ -119,4 +143,6 @@
|
||||
- 没写有没有碰共享接缝
|
||||
- 没写建议主线怎么收这次分支
|
||||
- 没提醒对方先同步最新 `origin/codex/xuxiake-mainline`
|
||||
- 没写清 SPU / SKU / barcode / 批次是否涉及
|
||||
- 没写规范审查结论
|
||||
- 没同步文档,导致下一个人继续靠猜
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
- 先读 `docs/workflow/团队协作模式.md`
|
||||
- 先读 `docs/workflow/模块边界说明.md`
|
||||
- 先读 `docs/workflow/主线同步操作手册.md`
|
||||
- 包装协作额外读包装 AI 对接说明
|
||||
- 前端、部署、数据库、包装等任务按任务类型额外读对应文档
|
||||
|
||||
这样就能做到:
|
||||
|
||||
@ -127,7 +127,7 @@ furniture-3d-system-codex-workspace/
|
||||
|
||||
- 默认用中文,面向非程序员表达
|
||||
- 先看哪些文档
|
||||
- 这位同事当前只负责哪些模块
|
||||
- 这位同事当前负责哪些模块
|
||||
- 哪些目录绝对不要碰
|
||||
- 开工前先同步 `origin/codex/xuxiake-mainline`
|
||||
- 结果汇报格式要简单
|
||||
@ -151,10 +151,10 @@ furniture-3d-system-codex-workspace/
|
||||
|
||||
同事本地规则,放自己工作区外层 `AGENTS.md`:
|
||||
|
||||
- 他当前只负责包装相关模块
|
||||
- 他当前负责的具体模块
|
||||
- 先同步主线再开发
|
||||
- 不扩范围
|
||||
- 不擅自改主导航、辅助装配、草稿组、仓库主链
|
||||
- 不擅自改主导航、辅助装配、草稿组、产品资料、仓库主链和共享接缝
|
||||
- 回报格式尽量简单
|
||||
|
||||
---
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 同事本地 AGENTS 模板
|
||||
|
||||
使用方式:把下面内容复制到你同事自己电脑上的“项目外层工作区” `AGENTS.md` 中。
|
||||
不要直接覆盖仓库里已经提交的那份 `AGENTS.md`。
|
||||
使用方式:把下面内容复制到同事自己电脑上的“项目外层工作区” `AGENTS.md` 中。
|
||||
不要直接覆盖仓库里已经提交的 `AGENTS.md`。
|
||||
|
||||
---
|
||||
|
||||
@ -13,107 +13,101 @@
|
||||
## 1. 默认沟通方式
|
||||
|
||||
- 默认中文回复。
|
||||
- 默认面向非程序员表达,先说结论,再说必要解释。
|
||||
- 默认只说“现在你要做什么、是否完成、还有没有风险”。
|
||||
- 默认先说结论,再说必要解释。
|
||||
- 默认面向非程序员表达。
|
||||
- 当前真实代码、脚本和线上返回优先于旧文档、旧记忆和历史摘要。
|
||||
|
||||
## 2. 先做什么
|
||||
## 2. 开工先读
|
||||
|
||||
开始任务前,先看下面这些文件:
|
||||
开始任务前,先看:
|
||||
|
||||
- `docs/README.md`
|
||||
- `AGENTS.md`
|
||||
- `docs/workflow/团队协作模式.md`
|
||||
- `docs/workflow/模块边界说明.md`
|
||||
- `docs/workflow/主线同步操作手册.md`
|
||||
|
||||
如果任务和包装有关,再看:
|
||||
按任务补读:
|
||||
|
||||
- `docs/workflow/同事交接模板.md`
|
||||
- `docs/开发指南/包装方案增强/新平台包装底座-同事AI对接说明-20260331.md`
|
||||
- 前端/视觉:`docs/设计规范/白曜石工坊统一视觉标准.md`、`docs/开发指南/组件使用指南.md`
|
||||
- 部署/线上:`docs/workflow/生产部署入口说明.md`
|
||||
- 数据库/迁移:`docs/workflow/数据库迁移操作手册.md`、`docs/开发指南/数据库基础命令速查.md`
|
||||
- 包装协作:`docs/开发指南/包装方案增强/` 下当前任务相关文档
|
||||
|
||||
## 3. 你的默认职责
|
||||
## 3. 当前项目主链
|
||||
|
||||
你当前默认只负责包装相关模块,以及与包装对接的预留能力。
|
||||
主业务链路是:
|
||||
|
||||
优先关注:
|
||||
```text
|
||||
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
|
||||
```
|
||||
|
||||
- `src/app/api/pre-skus/[id]/packaging`
|
||||
- `src/app/api/products/[id]/packaging`
|
||||
- `src/server/services/packaging.service.ts`
|
||||
- `src/server/repositories/packaging.repository.ts`
|
||||
- `docs/开发指南/包装方案增强`
|
||||
仓库质检、AI 出图工作台、OA 看板、用户和后台管理是工作台入口,不等于主数据链路本身。
|
||||
|
||||
## 4. 默认不要改的地方
|
||||
## 4. 我的负责范围
|
||||
|
||||
除非任务明确要求,否则不要主动改下面这些地方:
|
||||
把下面三项按实际任务改掉:
|
||||
|
||||
- 我当前负责:<例如 AI 白底图 / 包装 / 仓库质检 / 资产中心 / 产品资料>
|
||||
- 我默认可以改:<目录或文件>
|
||||
- 我默认不能改:<目录或文件>
|
||||
|
||||
如果不确定是否能改共享接缝,先问主负责人。
|
||||
|
||||
共享接缝包括:
|
||||
|
||||
- `src/app/asset-center`
|
||||
- `src/app/assisted-assembly`
|
||||
- `src/app/intake`
|
||||
- `src/app/warehouse`
|
||||
- `src/app/draft-groups/[id]`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/services/warehouse.service.ts`
|
||||
- `prisma/schema.prisma`
|
||||
- 旧 CAD 兼容链的大范围逻辑
|
||||
- `prisma/migrations/`
|
||||
- `src/app/layout.tsx`
|
||||
- `src/app/intake/`
|
||||
- `src/app/draft-groups/[id]/`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
|
||||
## 5. 开发前的固定动作
|
||||
## 5. 分支和同步
|
||||
|
||||
- 开工前先同步最新 `origin/codex/xuxiake-mainline`
|
||||
- 主负责人刚推了重要改动后,再同步一次
|
||||
- 自己准备提交前,再同步一次
|
||||
|
||||
高频协作固定铁律见:
|
||||
|
||||
- `docs/workflow/高频协作五条铁律.md`
|
||||
|
||||
固定分支命名:
|
||||
|
||||
- 统一用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`
|
||||
- 长期分支使用 `codex/<负责人>-<工作域>`
|
||||
- 临时小任务使用 `codex/<负责人>-<工作域>-<任务简称>`
|
||||
- 不要使用 `dev` 这种看不出职责的泛分支名
|
||||
|
||||
默认同步步骤:
|
||||
开工前、准备提交前、主负责人刚推重要改动后,都先同步状态:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <你的长期共享分支>
|
||||
git merge origin/codex/xuxiake-mainline
|
||||
git push origin <你的长期共享分支>
|
||||
git status --short --branch
|
||||
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
|
||||
```
|
||||
|
||||
如果你临时开的只是个人任务分支,再改用:
|
||||
长期共享分支用 `merge`。
|
||||
只有个人临时任务分支才可以 `rebase`。
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <你的分支名>
|
||||
git rebase origin/codex/xuxiake-mainline
|
||||
git push --force-with-lease origin <你的分支名>
|
||||
```
|
||||
分支命名:
|
||||
|
||||
## 6. 开发规则
|
||||
- `codex/<负责人>-<工作域>`
|
||||
- `codex/<负责人>-<工作域>-<任务简称>`
|
||||
|
||||
- 先修根因,不做表面补丁。
|
||||
- 不顺手扩范围,不把自己的任务做成整个项目大改。
|
||||
- Route / Page 不直接写 Prisma 业务规则。
|
||||
- 先复用现有实现,不重复造新结构。
|
||||
- 遇到冲突或边界不清,先停下来说明,不要硬改。
|
||||
- 主导航、主流程骨架、公共数据结构,不要擅自大改。
|
||||
不要使用 `dev`、`feature/...`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
|
||||
|
||||
## 7. 前端与文档
|
||||
## 6. 前端规则
|
||||
|
||||
- 涉及前端时,优先保持现有统一视觉,不要自己另起一套样式。
|
||||
- 涉及文档时,优先写短、直白、可执行的内容。
|
||||
- 不要在文档里写你自己电脑上的绝对路径。
|
||||
- 优先使用项目组件库、元组件和 `src/components/platform/`。
|
||||
- 白曜石工坊是第一视觉标准。
|
||||
- 如果有 Stitch 设计稿,按 Stitch 对齐密度、层级、控件、留白、字号和低噪音表达。
|
||||
- 资产中心是项目内参考样板。
|
||||
- 不堆说明,不堆统计,不做普通 SaaS 卡片墙。
|
||||
- 前端改动按 1920x1080 做浏览器自查;受影响时补移动端自查。
|
||||
|
||||
## 8. 完成后默认回报格式
|
||||
## 7. 数据和安全
|
||||
|
||||
完成后默认按下面格式回报:
|
||||
- SPU、SKU、barcode、批次不能混用。
|
||||
- 生产账号密码、访问密钥、数据库连接串、对象存储密钥不能写进 Git、共享文档或长期记忆。
|
||||
- 生产、数据库、对象存储写操作必须先只读核查,再取得明确授权。
|
||||
|
||||
1. 现在你做了什么
|
||||
2. 是否完成
|
||||
## 8. 完成后回报
|
||||
|
||||
默认按下面格式回报:
|
||||
|
||||
1. 做了什么
|
||||
2. 改了哪些文件或模块
|
||||
3. 跑了什么验证
|
||||
4. 还有没有风险或待确认项
|
||||
4. 还有什么风险
|
||||
5. 规范审查结论:`passed` / `passed with warnings` / `rejected`
|
||||
6. 说明:这是自审,不是独立复审
|
||||
```
|
||||
|
||||
@ -1,323 +1,220 @@
|
||||
# 团队协作模式
|
||||
|
||||
适用范围:`furniture-3d-system` 仓库。
|
||||
目标:把当前项目从“单人连续开发”切换成“多人并行协作,但主链不打架”的模式。
|
||||
适用范围:`furniture-3d-system/`。
|
||||
目标:让多人和多 AI 在同一套主线、模块边界和视觉标准下协作,避免旧分支、旧流程和旧页面口径继续污染当前项目。
|
||||
|
||||
进入这个仓库协作时,固定先读这 3 份文档,再开始改代码:
|
||||
|
||||
1. `docs/workflow/团队协作模式.md`
|
||||
2. `docs/workflow/模块边界说明.md`
|
||||
3. `docs/workflow/主线同步操作手册.md`
|
||||
|
||||
这 3 份就是当前唯一的第一层协作入口,不再额外维护第二套平级入口文档。
|
||||
本文件只写仓库内可以共享的规则。账号密码、访问密钥、数据库连接串、对象存储密钥等敏感信息不能写进 Git 文档;生产域名、PM2 名称、部署目录这类非密钥运行入口可以写,但必须保持最新。
|
||||
|
||||
---
|
||||
|
||||
## 1. 先记住一句话
|
||||
## 1. 当前一句话
|
||||
|
||||
这个仓库现在默认按下面这条主链协作:
|
||||
当前主业务链路是:
|
||||
|
||||
`资产中心 -> 辅助装配 -> 草稿组 -> 正式入库 -> 轻仓库`
|
||||
```text
|
||||
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
|
||||
```
|
||||
|
||||
- 包装相关能力当前挂在 `DraftGroup / PreSku / Warehouse` 承接口里,不作为独立顶层主线入口。
|
||||
- 旧的 `CAD -> 装配步骤` 链继续保留,但它是**历史兼容链**,不是新开品主链。
|
||||
- 历史文档里如果还出现 `auto-combinations`、`自动组合`、`自动拼装`,统一按旧称呼理解,不再代表当前页面入口。
|
||||
需要同时记住:
|
||||
|
||||
- 全局导航还包括首页、OA 新品流程看板、仓库质检、AI 出图工作台、用户和管理入口;这些是工作台入口,不等于主数据链路。
|
||||
- 包装、组件、仓库、质检、AI 出图都必须挂回明确的产品、SKU、批次或草稿组上下文,不能另起一套数据口径。
|
||||
- 旧 CAD 兼容链路仍存在,但它服务历史产品编辑,不是新平台主链。
|
||||
- 历史文档里的固定包装分支、旧服务器别名、旧 `dev` 分支说法,只能当历史参考,不能当当前事实。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前协作口径
|
||||
## 2. 开工固定入口
|
||||
|
||||
### 先固定两件事
|
||||
当任务发生在 `furniture-3d-system/`,开始改代码前先读:
|
||||
|
||||
- `main` 是最终发布主线,由 `WuYongJie` 收口;日常开发默认同步 `origin/codex/xuxiake-mainline`。
|
||||
- 这份文档现在只固定 Git 协作方式、主线收口规则和共享接缝处理原则,不再把当前模块职责长期写死在这里。
|
||||
- 生产部署入口以 `docs/workflow/生产部署入口说明.md` 和 `scripts/deploy-yydd-prod-from-git.ps1` 为准;`192.168.58.20` 不是当前项目生产部署目标。
|
||||
- `AGENTS.md`
|
||||
- `docs/workflow/团队协作模式.md`
|
||||
- `docs/workflow/模块边界说明.md`
|
||||
- `docs/workflow/主线同步操作手册.md`
|
||||
|
||||
### 当前主线入口
|
||||
如果任务涉及前端、生产或数据库,再按需补读:
|
||||
|
||||
- `src/app/asset-center`
|
||||
- `src/app/assisted-assembly`
|
||||
- `src/app/draft-groups`
|
||||
- `src/app/intake`
|
||||
- `src/app/warehouse`
|
||||
- `src/app/api/asset-center`
|
||||
- `src/app/api/assisted-assembly`
|
||||
- `src/app/api/draft-groups`
|
||||
- `src/app/api/intake`
|
||||
- `src/app/api/pre-skus`
|
||||
- `src/app/api/warehouse`
|
||||
- `src/components/platform`
|
||||
- `src/lib/open-product`
|
||||
- `docs/设计规范/白曜石工坊统一视觉标准.md`
|
||||
- `docs/workflow/规范审查清单.md`
|
||||
- `docs/workflow/生产部署入口说明.md`
|
||||
- `docs/workflow/数据库迁移操作手册.md`
|
||||
- `docs/开发指南/数据库基础命令速查.md`
|
||||
|
||||
### 具体职责怎么定
|
||||
开工时先确认真实现场:
|
||||
|
||||
- 具体谁负责哪个模块,不再以这份文档里的旧分工为准。
|
||||
- 当前任务边界以任务单、交接摘要、已确认协作信息和实际代码现状为准。
|
||||
- 如果任务碰到共享接缝,再和主线收口人对齐后再动。
|
||||
```bash
|
||||
git fetch origin
|
||||
git status --short --branch
|
||||
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
|
||||
```
|
||||
|
||||
### 哪些地方仍然算共享接缝
|
||||
`rev-list` 输出里,左边是当前分支独有提交数,右边是 `origin/codex/xuxiake-mainline` 独有提交数。右边大于 0 时,说明当前分支落后共享开发主线。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前权威分支
|
||||
|
||||
- `main` 是最终发布主线,由 `WuYongJie` 收口。
|
||||
- `origin/codex/xuxiake-mainline` 是共享开发主线和默认同步基线。
|
||||
- 新功能、优化和修复默认从最新 `origin/codex/xuxiake-mainline` 开任务分支。
|
||||
- 常规分支命名使用 `codex/<负责人>-<工作域>` 或 `codex/<负责人>-<工作域>-<任务简称>`。
|
||||
- 不再使用 `feature/packaging`、`packaging/...`、`codex/xuxiake-dev` 这类泛分支名。
|
||||
|
||||
长期共享分支同步共享开发主线时固定用 `merge`。
|
||||
临时个人任务分支只有确认无人共用时才可以用 `rebase`。
|
||||
|
||||
任何人回答“xuxiake 主线有没有新提交”之前,都必须先 `git fetch origin`,再看真实远端状态。
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前主要目录
|
||||
|
||||
主入口和全局布局:
|
||||
|
||||
- `src/app/page.tsx`
|
||||
- `src/components/layout/`
|
||||
- `src/app/layout.tsx`
|
||||
|
||||
新平台主链:
|
||||
|
||||
- `src/app/asset-center/`
|
||||
- `src/app/api/asset-center/`
|
||||
- `src/app/assisted-assembly/`
|
||||
- `src/app/api/assisted-assembly/`
|
||||
- `src/app/draft-groups/`
|
||||
- `src/app/api/draft-groups/`
|
||||
- `src/app/intake/`
|
||||
- `src/app/api/intake/`
|
||||
- `src/app/warehouse/`
|
||||
- `src/app/api/warehouse/`
|
||||
- `src/app/warehouse-quality/`
|
||||
- `src/app/api/warehouse-quality/`
|
||||
|
||||
运营、AI 和平台能力:
|
||||
|
||||
- `src/app/ai-render-workbench/`
|
||||
- `src/app/api/planning/`
|
||||
- `src/app/api/platform/`
|
||||
- `src/components/platform/`
|
||||
- `src/lib/open-product/`
|
||||
- `src/lib/planning/`
|
||||
|
||||
旧 CAD 兼容链路:
|
||||
|
||||
- `src/app/products/`
|
||||
- `src/app/api/products/`
|
||||
- `src/app/api/skus/`
|
||||
- `src/components/assembly/`
|
||||
- `src/components/3d/`
|
||||
|
||||
---
|
||||
|
||||
## 5. 共享接缝
|
||||
|
||||
下面这些位置属于高冲突区,改动前必须多看一层调用方,提交时也要讲清楚影响范围:
|
||||
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/app/intake`
|
||||
- `prisma/schema.prisma`
|
||||
- `src/app/draft-groups/[id]`
|
||||
- `prisma/migrations/`
|
||||
- `src/app/layout.tsx`
|
||||
- `src/app/intake/`
|
||||
- `src/app/draft-groups/[id]/`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
|
||||
共享接缝不是不能改,而是不能顺手改、不能带着局部假设改、不能不验证就合回主线。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分支规则
|
||||
## 6. 业务对象口径
|
||||
|
||||
### 主分支
|
||||
当前新平台不能混用这些概念:
|
||||
|
||||
- `main` 继续保持受保护状态,只作为最终发布主线收口。
|
||||
- 其他协作者默认不能直接 push `main`,只能走自己的功能分支。
|
||||
- 日常开发基线是 `origin/codex/xuxiake-mainline`,不要把半成品直接推上最终发布主线。
|
||||
- `AssetFamily`:资产中心里的产品族或产品款基础资产。
|
||||
- `AssetVariant`:资产族下的规格、材质、花色等变体。
|
||||
- `DraftGroup`:草稿组,是辅助装配到产品资料之间的承接对象。
|
||||
- `PreSku`:正式 SKU 前的预 SKU 或待确认 SKU。
|
||||
- `Product / SKU`:正式入库后的产品和 SKU。
|
||||
- `IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`:非集成产品实拍、AI 白底图、素材库回传等链路使用的对象。
|
||||
|
||||
### 开发分支
|
||||
SPU、SKU、barcode、批次必须分清:
|
||||
|
||||
统一使用:
|
||||
- SPU 是产品款。
|
||||
- SKU 是 SPU 下的具体子项。
|
||||
- barcode 是逐步收敛的唯一业务定位值,不能拿来代替 SPU 或 SKU。
|
||||
- 批次只能属于当前产品资料链路的批次上下文。
|
||||
- 基于某个 SKU 换色得到的新 SKU,仍属于同一个 SPU,并且只能归属当前批次。
|
||||
|
||||
- `codex/<负责人>-<工作域>`
|
||||
- `codex/<负责人>-<工作域>-<任务简称>`
|
||||
---
|
||||
|
||||
固定口径:
|
||||
## 7. 前端和视觉协作
|
||||
|
||||
- 负责人必须进分支名,不能只用 `feature/...`、`packaging/...`、`dev` 这类泛名字
|
||||
- `WuYongJie` 主线相关分支统一放 `codex/wuyongjie-mainline...`
|
||||
- 长期共享分支和临时任务分支都要把当前工作域写进分支名,不再把人长期绑定在单一旧分支名上
|
||||
- 其他负责人也按同一模式命名,不再额外发明新前缀
|
||||
- 历史分支名不强制重命名,但从现在开始新分支全部按这套规则建
|
||||
当前第一视觉标准是白曜石工坊,不是旧苹果风,也不是普通后台卡片墙。
|
||||
|
||||
示例:
|
||||
前端任务必须同时遵守:
|
||||
|
||||
- `codex/wuyongjie-mainline`
|
||||
- `codex/wuyongjie-mainline-asset-center`
|
||||
- `codex/xuxiake-costing`
|
||||
- `codex/xuxiake-intake-clean`
|
||||
- `codex/zhangsan-cad-compat`
|
||||
- 优先使用 `src/components/platform/` 和项目里的元组件、组件库。
|
||||
- Stitch 设计稿是视觉规格和验收标准,但落地时要通过项目组件体系实现。
|
||||
- 资产中心页面是当前项目内最重要的参考样板。
|
||||
- 新平台页面保持浅色工作台、低噪音、图片和数据优先、层级清楚。
|
||||
- 不堆说明块、不重复统计、不做营销式大卡片布局。
|
||||
- `apple-*`、暗色 `ObsidianControlShell`、旧主题 CSS 只作为兼容历史页面存在,不能作为新页面方向。
|
||||
|
||||
### 合并方式
|
||||
涉及图片、3D、AI 白底图、素材卡片时,重点是图本身和操作路径,不要让说明文字和状态块抢走主要视觉注意力。
|
||||
|
||||
- 主负责人负责把稳定内容从共享开发主线收口到 `main`。
|
||||
- 其他协作者默认先推自己的功能分支。
|
||||
- 同事的改动进入主线前,默认先由主负责人检查和收口。
|
||||
- 协作者不要自己合并到 `main`,也不要自己改远端主线历史。
|
||||
- 没有过验收清单,不合并,不并线。
|
||||
---
|
||||
|
||||
当前固定流程:
|
||||
## 8. 生产和部署协作
|
||||
|
||||
1. 协作者先从最新 `origin/codex/xuxiake-mainline` 同步到自己的长期分支或任务分支。
|
||||
2. 协作者只在自己负责的工作域内开发。
|
||||
3. 协作者推送自己的分支,并附上交接说明。
|
||||
4. `WuYongJie` 负责检查范围、决定 merge / cherry-pick / 手工收口。
|
||||
5. 只有进过主负责人收口的内容,才允许进入 `main`。
|
||||
生产发布必须由用户明确授权。
|
||||
发布代码默认以 `origin/main` 为来源,常用入口是仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1`,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。
|
||||
|
||||
### 主线收口检查单
|
||||
|
||||
`WuYongJie` 收同事分支前,固定先看这 6 项:
|
||||
|
||||
1. 这个分支是不是已经同步过最新 `origin/codex/xuxiake-mainline`
|
||||
2. 这次改动是不是只落在任务约定范围内
|
||||
3. 有没有碰共享接缝;如果碰了,交接里有没有写清原因
|
||||
4. 有没有最小验证结果,而不是只说“应该可以”
|
||||
5. 有没有交接说明,且写清楚“这次只改了什么、没改什么、风险是什么”
|
||||
6. 这次改动会不会影响正式入库、轻仓库或主链真相
|
||||
|
||||
只要这 6 项里有明显缺项,就先不要急着收口。
|
||||
|
||||
### merge / cherry-pick / 手工整合 怎么选
|
||||
|
||||
固定口径:
|
||||
|
||||
- 直接 `merge`
|
||||
- 适用:改动范围干净,只在同事负责域内,没有混入共享接缝或无关改动
|
||||
- 前提:已经同步最新 `origin/codex/xuxiake-mainline`,并且附了最小验证和交接说明
|
||||
|
||||
- 优先 `cherry-pick`
|
||||
- 适用:同一个分支里既有可收的目标提交,也混入了暂时不该进主线的提交
|
||||
- 常见场景:同一个协作分支里夹带了共享接缝改动、调试提交、试错提交
|
||||
|
||||
- 手工整合
|
||||
- 适用:目标功能要收,但提交历史太脏、冲突太多,或者改动虽然合理但不能整段直接拿
|
||||
- 常见场景:同一个文件同时碰到主线重构区和同事任务区
|
||||
|
||||
- 暂不收口
|
||||
- 适用:没同步最新 `origin/codex/xuxiake-mainline`、没交接说明、没最小验证、范围失控、直接改了敏感共享接缝但没写原因
|
||||
- 处理方式:先退回补说明、补同步、补验证,再继续看
|
||||
|
||||
### 同事和主线怎么保持同步
|
||||
|
||||
当前项目默认采用这个口径:
|
||||
|
||||
- `WuYongJie` 负责维护共享开发主线,并把稳定内容收口到 `main`
|
||||
- 协作者长期在自己的长期共享分支或当前任务分支开发
|
||||
- 同事不能等做完了再一次性同步,而是要高频同步 `origin/codex/xuxiake-mainline`
|
||||
|
||||
默认同步时机:
|
||||
|
||||
- 每天开始开发前同步一次
|
||||
- 主负责人刚推了重要改动后同步一次
|
||||
- 自己准备提交前再同步一次
|
||||
|
||||
默认推荐方式:
|
||||
|
||||
- 长期协作分支固定优先使用 `merge origin/codex/xuxiake-mainline`
|
||||
- 只有临时个人任务分支才优先使用 `rebase origin/codex/xuxiake-mainline`
|
||||
- 但不要一边长期不同步,一边继续堆改动
|
||||
|
||||
白话规则:
|
||||
|
||||
- 长期共享分支保留真实协作历史,所以固定用 `merge origin/codex/xuxiake-mainline`
|
||||
- 临时个人任务分支只服务当前任务,为了保持提交线清楚,才用 `rebase origin/codex/xuxiake-mainline`
|
||||
- 如果你拿不准自己现在是不是长期共享分支,就先按长期共享分支处理,不要擅自改写共享历史
|
||||
|
||||
长期共享分支日常同步固定步骤:
|
||||
部署前至少核对:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <你的长期共享分支>
|
||||
git merge origin/codex/xuxiake-mainline
|
||||
git push origin <你的长期共享分支>
|
||||
git ls-remote origin refs/heads/main
|
||||
git rev-parse origin/main
|
||||
```
|
||||
|
||||
如果你临时开了个人任务分支,再用:
|
||||
部署后不能只看公网 `/api/health` 就说完成,还要按生产部署手册核对发布 commit、进程、端口、日志和关键页面。
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout <你的临时任务分支>
|
||||
git rebase origin/codex/xuxiake-mainline
|
||||
git push --force-with-lease origin <你的临时任务分支>
|
||||
```
|
||||
|
||||
怎么选:
|
||||
|
||||
- 长期共享分支,用 `merge`
|
||||
- 临时个人任务分支,用 `rebase`
|
||||
|
||||
详细操作说明见:`docs/workflow/主线同步操作手册.md`
|
||||
高频协作总规则见:`docs/workflow/高频协作五条铁律.md`
|
||||
仓库文档里不要写生产账号密码、访问密钥、数据库连接串或对象存储密钥。非密钥的生产入口信息可以写,但必须先核对当前脚本和线上现场。
|
||||
|
||||
---
|
||||
|
||||
## 4. 每个任务都按这个流程走
|
||||
## 9. 本地文件和清理
|
||||
|
||||
1. 先建一张任务单。
|
||||
2. 写清楚本次只做什么、不做什么、验收标准是什么。
|
||||
3. 明确归属模块,不跨模块顺手乱改。
|
||||
4. 开发前先读对应模块边界说明。
|
||||
5. 改完后同步必要文档。
|
||||
6. 跑最小验证。
|
||||
7. 做一次规范自审,再推分支。
|
||||
本仓库经常有本地产物、排错证据和临时交接文件,例如 `.next*`、审计目录、下载素材、截图、日志、Excel、JSON 摘要等。
|
||||
|
||||
额外固定动作:
|
||||
清理前先判断它属于哪一类:
|
||||
|
||||
- 任务涉及共享接缝时,先在任务单里写清“为什么必须碰共享层”。
|
||||
- 协作者推分支后,要附一段交接说明,明确本次只改了什么、没改什么、风险是什么。
|
||||
- `WuYongJie` 收口同事改动时,优先按小任务合并,不一次性吞并长期积压改动。
|
||||
- 如果任务只是要把局部工作区做完整,默认不要直接在 `src/app/draft-groups/[id]`、`src/app/intake` 这类共享接缝里堆实现;先确认是否该在当前工作域内独立收口。
|
||||
- 如果这次改动会影响 `intake / warehouse / Product / SKU` 承接链,收口时额外确认:有没有把局部协作逻辑误带进正式入库主链,或者把旧兼容链重新当成主链真相。
|
||||
- 正式源码和协作文档:不能随手删。
|
||||
- 本地产物:确认不影响验证后可以清理。
|
||||
- 排错证据:任务没收口前不要清。
|
||||
- 敏感交接材料:不能提交 Git,必要时拆成本地版和仓库版。
|
||||
- 用户或其他 agent 的未跟踪文件:默认不要动。
|
||||
|
||||
---
|
||||
|
||||
## 5. 人和 AI 都要遵守的几条硬规则
|
||||
## 10. 每次任务的交接口径
|
||||
|
||||
### 先修根因,不打补丁
|
||||
交接时讲清楚:
|
||||
|
||||
- 影响 3D 拼装、命名、包装、BOM、安装步骤的数据,优先在数据源修。
|
||||
- 不允许只在页面上压报错、绕校验。
|
||||
- 本次解决什么问题。
|
||||
- 改了哪些模块和共享接缝。
|
||||
- 是否同步过 `origin/codex/xuxiake-mainline`。
|
||||
- 跑了哪些验证,失败了什么。
|
||||
- 是否部署,部署来源 commit 是什么。
|
||||
- 规范审查结论是什么。
|
||||
|
||||
### 不跨层
|
||||
规范审查固定只查三件事:
|
||||
|
||||
- Page / Route 不直接写 Prisma 业务逻辑。
|
||||
- Route 负责收参、鉴权、返回。
|
||||
- Service 负责业务规则。
|
||||
- Repository 负责数据访问。
|
||||
- 架构有没有被破坏。
|
||||
- 视觉有没有跑偏。
|
||||
- 这次是不是根因修复。
|
||||
|
||||
### 不混主链和兼容链
|
||||
|
||||
- 新平台主链的数据真相在 `AssetFamily / AssetVariant / DraftGroup / PreSku`。
|
||||
- 旧 `Product / SKU / Panel / Hole / ProductHardware` 继续服务兼容链。
|
||||
- 包装本期要能挂接 `PreSku`,但不直接跳过草稿组去造正式产品。
|
||||
|
||||
### 不破坏统一视觉
|
||||
|
||||
- 新平台页面优先复用 `src/components/platform`。
|
||||
- 旧装配编辑器继续走现有视觉体系,不另起一套杂样式。
|
||||
|
||||
---
|
||||
|
||||
## 6. 本地目录管理规则
|
||||
|
||||
### 需要长期保留的
|
||||
|
||||
- 源码
|
||||
- 迁移
|
||||
- 测试
|
||||
- 正式文档
|
||||
- 确认要进仓库的设计稿或说明
|
||||
|
||||
### 默认视为本地产物的
|
||||
|
||||
- `.dev-*.log`
|
||||
- `.qa-*.log`
|
||||
- `.start-*.log`
|
||||
- `.next-start-*.log`
|
||||
- `dev-server.log`
|
||||
- `.codex-logs/`
|
||||
- `_logs/`
|
||||
- `output/`
|
||||
- `temp/`
|
||||
- `public/uploads/`
|
||||
|
||||
### 推荐习惯
|
||||
|
||||
- 提交前先运行:`npm run clean:local`
|
||||
- 然后再看:`git status --short`
|
||||
|
||||
---
|
||||
|
||||
## 7. 提交前最少要交代什么
|
||||
|
||||
- 这次改的是哪个模块
|
||||
- 本次只做了什么
|
||||
- 哪些地方明确没做
|
||||
- 跑了哪些验证
|
||||
- 有没有风险或依赖别人继续接
|
||||
|
||||
详细清单见:`docs/workflow/提测与验收清单.md`
|
||||
|
||||
---
|
||||
|
||||
## 8. 给 Codex 的一句话协作口径
|
||||
|
||||
如果要把任务转给 AI,建议在任务里直接写明:
|
||||
|
||||
- 当前目标
|
||||
- 所属模块
|
||||
- 禁止改动的目录
|
||||
- 验收标准
|
||||
- 需要同步更新的文档
|
||||
|
||||
模板见:`docs/workflow/任务单模板.md` 和 `docs/workflow/同事交接模板.md`
|
||||
|
||||
---
|
||||
|
||||
## 9. 团队共享 AGENTS 和个人 AGENTS 怎么分
|
||||
|
||||
当前默认规则:
|
||||
|
||||
- 仓库里的 `AGENTS.md` 是团队共享规则
|
||||
- 个人额外规则不要直接改仓库根目录那份
|
||||
- 谁要给自己的 Codex 加私有约束,就在自己电脑上的项目外层工作区再放一份本地 `AGENTS.md`
|
||||
|
||||
推荐说明:
|
||||
|
||||
- 共享规则:继续放仓库里,大家共用
|
||||
- 个人规则:放各自机器上的外层工作区,不进仓库
|
||||
|
||||
对应说明和模板见:
|
||||
|
||||
- `docs/workflow/同事本地AGENTS使用说明.md`
|
||||
- `docs/workflow/同事本地AGENTS模板.md`
|
||||
- `docs/workflow/高频协作五条铁律.md`
|
||||
结论只能是 `passed`、`passed with warnings` 或 `rejected`。
|
||||
如果这次只是自审,必须明确写“这是自审,不是独立复审”。
|
||||
|
||||
213
furniture-3d-system/docs/workflow/外部只读数据库开放操作说明.md
Normal file
213
furniture-3d-system/docs/workflow/外部只读数据库开放操作说明.md
Normal file
@ -0,0 +1,213 @@
|
||||
# 外部只读数据库开放操作说明
|
||||
|
||||
适用范围:给指定服务器读取本项目 PostgreSQL 的外部核心数据包,覆盖产品资料、产品限价、仓库管理、仓库质检。
|
||||
|
||||
## 1. 固定结论
|
||||
|
||||
- 不开放 `public` 业务原表。
|
||||
- 不复用应用账号或管理员账号。
|
||||
- 默认只开放 `external` schema 下经过白名单整理的核心视图。
|
||||
- 外部系统优先读取 `external.product_catalog_wide_v1` 这张商品目录聚合宽表;需要仓库或质检明细时,再读取对应明细视图。
|
||||
- 外部主同步、去重和幂等更新使用 `barcode_business_key`;`product_business_key` 只用于 SPU/型号归组,`sku_business_key` 只用于 SKU 编码兼容关联。
|
||||
- 只允许指定服务器 IP 访问 PostgreSQL 端口。
|
||||
- 密码、连接串、白名单 IP 只放生产本地交接材料,不写入 Git。
|
||||
|
||||
## 2. 对外数据入口
|
||||
|
||||
推荐给外部系统读取:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM external.product_catalog_wide_v1;
|
||||
```
|
||||
|
||||
这张视图是一条码一行。正式 `Product / ProductSKU` 已存在时输出正式条码商品行;正式商品尚未落库但 non-integrated 已进入 ready 状态时,补输出可消费的开发阶段条码行。外部系统必须按 `barcode_business_key` 做主同步、去重和幂等更新;同一 SKU 下多个条码会输出多行。
|
||||
|
||||
补充口径:
|
||||
|
||||
- non-integrated ready 行会带上主图、开发阶段限价和基础商品字段。
|
||||
- non-integrated ready 行会带上产品开发明细页的负责人字段:外观设计师、结构设计师、SPU 负责人、商品企划、空间设计师、采购负责人;正式商品行没有开发阶段上下文时这些列保持空值。
|
||||
- non-integrated ready 行尚未正式化时,正式 BOM、包装库存、仓库库位和 SKU 质检摘要字段会保持 `0 / NULL / []`。
|
||||
- 需要核对开发阶段头信息或 SKU 明细真值时,继续读取 `external.intake_products_ready_v1 / external.intake_sku_items_ready_v1`。
|
||||
- 图片、模型、贴图等 URL 字段在外部视图中输出为完整系统入口,例如 `https://cp.jiansun.vip/api/storage/object?key=...`。这些入口仍由应用侧鉴权和对象存储跳转控制,不把私有桶短期签名 URL 固化进数据库。
|
||||
- 非生产环境如需使用其他站点域名,可由数据库管理员设置 `ALTER DATABASE <DB_NAME> SET app.external_asset_base_url = '<APP_ORIGIN>';`;未设置时默认使用正式站点 `https://cp.jiansun.vip`。
|
||||
|
||||
限价口径固定如下:
|
||||
|
||||
- `存量商品明细` 的产品一般没有限价,宽表输出 `limit_price_status = NOT_APPLICABLE`。
|
||||
- `产品开发明细`、`模块化产品明细` 的产品到 `产品限价` 节点确认后,限价就是正式口径,宽表输出 `limit_price_status = CONFIRMED`。
|
||||
- 需要核对限价来源时读取 `external.intake_product_limits_v1`。
|
||||
|
||||
本次外部核心只读包包含:
|
||||
|
||||
| 方向 | 推荐视图 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 产品资料 | `external.product_catalog_wide_v1` | 一条码一行的商品目录宽表,覆盖正式条码与可消费的 non-integrated ready 条码 |
|
||||
| 产品限价 | `external.intake_product_limits_v1` | 非集成产品限价来源明细 |
|
||||
| 仓库管理 | `external.warehouse_locations_v1` | 库位资料 |
|
||||
| 仓库管理 | `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 |
|
||||
| 仓库管理 | `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 |
|
||||
| 仓库质检 | `external.sku_quality_inspections_v1` | SKU 质检记录 |
|
||||
|
||||
补充说明:`external.product_assets_v1` / `external.product_bom_v1` 是外部契约巡检覆盖的明细视图,但不在当前 `external_core_reader` 核心授权包内;外部系统确实需要逐条素材或 BOM 时,必须单独评审并追加白名单授权。
|
||||
|
||||
暂不开放:
|
||||
|
||||
- `public` schema 原表。
|
||||
- 准 SKU 草稿包装过程数据。
|
||||
- 质检 `metrics / attachments` 内部 JSON。
|
||||
- 审批、权限、部门、团队、可见性等内部流程字段。
|
||||
|
||||
## 3. 生产执行顺序
|
||||
|
||||
1. 先确认目标服务器固定公网 IP 或内网 IP。
|
||||
2. 在生产库完成迁移,确保本说明列出的 `external.*_v1` 视图已存在。
|
||||
3. 创建专用只读账号,只授权外部核心视图。
|
||||
4. 修改 PostgreSQL 网络白名单,只允许目标 IP 访问。
|
||||
5. 从目标服务器实际连接测试。
|
||||
6. 在生产侧跑外部只读契约巡检。
|
||||
|
||||
## 4. 数据库授权模板
|
||||
|
||||
在生产应用目录执行,密码从本地安全材料输入:
|
||||
|
||||
```bash
|
||||
psql "$DATABASE_URL" \
|
||||
-v database_name='<DB_NAME>' \
|
||||
-v reader_role='external_core_reader' \
|
||||
-v reader_password='<REDACTED_SECRET>' \
|
||||
-f scripts/sql/grant-external-product-catalog-readonly.sql
|
||||
```
|
||||
|
||||
该脚本只授予:
|
||||
|
||||
- `CONNECT` 当前数据库
|
||||
- `USAGE` `external` schema
|
||||
- `SELECT` 外部核心视图:
|
||||
- `external.product_catalog_wide_v1`
|
||||
- `external.intake_product_limits_v1`
|
||||
- `external.warehouse_locations_v1`
|
||||
- `external.sku_warehouse_stock_v1`
|
||||
- `external.packaged_items_v1`
|
||||
- `external.sku_quality_inspections_v1`
|
||||
|
||||
并撤销该角色对 `public` schema、`public` 表、`external` 未授权表/视图的权限。
|
||||
|
||||
## 5. PostgreSQL 网络白名单
|
||||
|
||||
`postgresql.conf` 至少确认:
|
||||
|
||||
```conf
|
||||
listen_addresses = '*'
|
||||
```
|
||||
|
||||
`pg_hba.conf` 增加一条只允许目标服务器的规则:
|
||||
|
||||
```conf
|
||||
hostssl <DB_NAME> external_core_reader <TARGET_SERVER_IP>/32 scram-sha-256
|
||||
```
|
||||
|
||||
如果当前没有启用 SSL,只能在受控内网/VPN 内临时使用:
|
||||
|
||||
```conf
|
||||
host <DB_NAME> external_core_reader <TARGET_SERVER_IP>/32 scram-sha-256
|
||||
```
|
||||
|
||||
不要写成 `0.0.0.0/0`。
|
||||
|
||||
## 6. 验证命令
|
||||
|
||||
从目标服务器验证:
|
||||
|
||||
```bash
|
||||
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
|
||||
-c "SELECT COUNT(*) FROM external.product_catalog_wide_v1;"
|
||||
```
|
||||
|
||||
验证仓库和质检明细:
|
||||
|
||||
```bash
|
||||
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
|
||||
-c "SELECT COUNT(*) FROM external.sku_warehouse_stock_v1;"
|
||||
|
||||
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
|
||||
-c "SELECT COUNT(*) FROM external.sku_quality_inspections_v1;"
|
||||
```
|
||||
|
||||
同时验证不能读业务原表:
|
||||
|
||||
```bash
|
||||
psql "postgresql://external_core_reader:<REDACTED_SECRET>@<DB_HOST>:5432/<DB_NAME>" \
|
||||
-c 'SELECT COUNT(*) FROM public."Product";'
|
||||
```
|
||||
|
||||
第二条必须失败,出现 `permission denied` 才是正确结果。
|
||||
|
||||
生产侧再跑:
|
||||
|
||||
```bash
|
||||
psql "$DATABASE_URL" -f scripts/sql/check-external-readonly-contract.sql
|
||||
```
|
||||
|
||||
## 7. 同步到 JSReportReplica.app 中间库
|
||||
|
||||
如果外部系统不直接连本项目 PostgreSQL,而是统一从 `JSReportReplica` 中间库读取,本项目维护一份 `external` 核心视图到 `app` schema 的项目专属前缀表同步。
|
||||
|
||||
`JSReportReplica.app` 是多个项目共用的应用 schema,本项目同步目标表固定使用 `furniture3d_` 前缀,避免和其他项目的对外数据混用。源库视图仍是 `external.*`,只有中间库目标表加前缀。
|
||||
|
||||
默认同步表:
|
||||
|
||||
- `app.furniture3d_product_catalog_wide_v1`
|
||||
- `app.furniture3d_intake_product_limits_v1`
|
||||
- `app.furniture3d_warehouse_locations_v1`
|
||||
- `app.furniture3d_sku_warehouse_stock_v1`
|
||||
- `app.furniture3d_packaged_items_v1`
|
||||
- `app.furniture3d_sku_quality_inspections_v1`
|
||||
- `app.furniture3d_external_readonly_sync_runs`:同步运行日志。
|
||||
|
||||
主宽表仍然是:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM app.furniture3d_product_catalog_wide_v1;
|
||||
```
|
||||
|
||||
同步入口:
|
||||
|
||||
```bash
|
||||
npm run sync:external-jsreport
|
||||
npm run sync:external-jsreport:apply
|
||||
```
|
||||
|
||||
第一个命令只演练并输出源视图行数;第二个命令才会写入 `JSReportReplica.app`。脚本会先写临时表,再替换正式表,避免同步中途留下半截数据。
|
||||
|
||||
生产定时器安装入口:
|
||||
|
||||
```bash
|
||||
bash scripts/install-external-jsreport-sync-timer.sh
|
||||
```
|
||||
|
||||
安装脚本只会在生产服务器本地生成 `/opt/furniture-3d-system-prod/shared/external-jsreport-sync.env` 模板。真实连接信息只允许写在这个本地 env 文件里,并保持 `600` 权限,不要写入 Git、文档或群公告。
|
||||
|
||||
必要环境变量:
|
||||
|
||||
- `DATABASE_URL` 或 `EXTERNAL_JSREPORT_SOURCE_DATABASE_URL`:源 PostgreSQL 连接。
|
||||
- `JSREPORT_REPLICA_HOST`
|
||||
- `JSREPORT_REPLICA_PORT`
|
||||
- `JSREPORT_REPLICA_DATABASE`
|
||||
- `JSREPORT_REPLICA_USERNAME`
|
||||
- `JSREPORT_REPLICA_PASSWORD`
|
||||
- `JSREPORT_REPLICA_SCHEMA`
|
||||
- `EXTERNAL_JSREPORT_TARGET_TABLE_PREFIX`:中间库目标表前缀,默认 `furniture3d_`。
|
||||
|
||||
默认定时频率是每 45 分钟同步一次。安装后 5 分钟触发首次同步,后续按上次同步完成时间间隔 45 分钟再次执行;可通过 `EXTERNAL_JSREPORT_SYNC_ON_BOOT_SEC` 调整首次触发延迟,通过 `EXTERNAL_JSREPORT_SYNC_INTERVAL` 调整同步间隔。
|
||||
|
||||
## 8. 规范审查
|
||||
|
||||
结论:`passed`
|
||||
|
||||
这是自审,不是独立复审。
|
||||
|
||||
- 架构:外部读取收口到 `external` 契约视图和专用只读角色,不开放业务原表;产品资料、仓库管理、仓库质检各有稳定视图。
|
||||
- 视觉:不涉及前端视觉。
|
||||
- 根因:通过数据库角色、schema 权限和网络白名单解决外部只读隔离,不靠口头约定。
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
- [ ] 任务范围没有失控,没有顺手改无关模块
|
||||
- [ ] 关键文档已同步
|
||||
- [ ] 先跑过 `npm run clean:local`
|
||||
- [ ] `git status --short` 里没有不该提交的日志、缓存、临时文件
|
||||
- [ ] `git status --short --branch` 已看过,知道哪些是本次改动、哪些是已有现场
|
||||
- [ ] `git status --short` 里没有准备误提交的日志、缓存、临时文件
|
||||
- [ ] 改动文件和任务单一致
|
||||
- [ ] 自己能用一句话说清根因和修复方式
|
||||
|
||||
@ -45,6 +45,8 @@ npm run build
|
||||
- [ ] 关键按钮/表单/保存动作能走通
|
||||
- [ ] 关键接口返回正常
|
||||
- [ ] 没有把别的入口改坏
|
||||
- [ ] 前端视觉改动按 1920x1080 做过浏览器自查;移动端受影响时也补移动端自查
|
||||
- [ ] 图片、AI、3D 或素材类页面确认主视觉不被说明块、统计块或弹层遮挡
|
||||
|
||||
---
|
||||
|
||||
@ -62,6 +64,7 @@ npm run build
|
||||
|
||||
- `docs/workflow/团队协作模式.md`
|
||||
- `docs/workflow/模块边界说明.md`
|
||||
- `docs/workflow/主线同步操作手册.md`
|
||||
- 对应模块的专项说明文档
|
||||
|
||||
---
|
||||
|
||||
@ -1,272 +1,297 @@
|
||||
# 模块边界说明
|
||||
|
||||
目标:让人和 AI 在开工前先知道“哪里是新平台主链,哪里是旧兼容链,哪里不能乱碰”。
|
||||
|
||||
配套入口:
|
||||
|
||||
- 先读 `docs/workflow/团队协作模式.md`,确认你属于哪个工作域、该用什么分支名
|
||||
- 再读 `docs/workflow/主线同步操作手册.md`,确认你当前分支该用 `merge origin/codex/xuxiake-mainline` 还是 `rebase origin/codex/xuxiake-mainline`
|
||||
- 这份文档只负责讲边界,不额外创造新的角色体系或新的协作入口
|
||||
- 这份文档只固定系统边界,不固定当前负责人;具体任务归属以当前任务单、交接摘要和已确认协作信息为准
|
||||
适用范围:`furniture-3d-system/`。
|
||||
目标:让前端、后端、数据库、AI 和包装协作时都知道自己该接在哪条链路上,避免把历史模块、新平台主链和运营入口混在一起。
|
||||
|
||||
---
|
||||
|
||||
## 1. 页面与入口边界
|
||||
## 1. 页面入口边界
|
||||
|
||||
### 新平台主链入口
|
||||
### 主业务链路
|
||||
|
||||
- `/`:新平台首页导航
|
||||
- `/asset-center`:资产中心
|
||||
- `/assisted-assembly`:辅助装配(手动拼装工作台入口)
|
||||
- `/draft-groups`:草稿组列表
|
||||
- `/draft-groups/[id]`:草稿组详情
|
||||
- `/intake`:正式入库
|
||||
- `/warehouse`:轻仓库
|
||||
主业务链路是:
|
||||
|
||||
### 旧兼容链入口
|
||||
```text
|
||||
资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库
|
||||
```
|
||||
|
||||
- `/products`
|
||||
- `/products/[id]`
|
||||
- `/products/[id]/assembly`
|
||||
- `/products/[id]/skus/[skuId]`
|
||||
- `/products/[id]/skus/[skuId]/assembly`
|
||||
对应入口:
|
||||
|
||||
### 不要混的原则
|
||||
- `src/app/asset-center/`
|
||||
- `src/app/assisted-assembly/`
|
||||
- `src/app/draft-groups/`
|
||||
- `src/app/intake/`
|
||||
- `src/app/warehouse/`
|
||||
|
||||
- 新平台入口负责“资产准备、辅助装配、草稿组承接、入库前整理”。
|
||||
- 旧兼容链负责“历史产品、CAD 导入、板件/孔位/五金/装配步骤”。
|
||||
- 不要把旧产品详情页重新改成新平台首页。
|
||||
### 运营和工作台入口
|
||||
|
||||
---
|
||||
|
||||
## 2. 新平台主链边界
|
||||
|
||||
### 前端目录
|
||||
这些入口服务工作台、看板、质检或 AI 生产,不等于主数据链路本身:
|
||||
|
||||
- `src/app/page.tsx`
|
||||
- `src/app/asset-center`
|
||||
- `src/app/assisted-assembly`
|
||||
- `src/app/draft-groups`
|
||||
- `src/app/intake`
|
||||
- `src/app/warehouse`
|
||||
- `src/components/platform`
|
||||
- `src/app/home/`
|
||||
- `src/app/intake/oa-board/`
|
||||
- `src/app/warehouse-quality/`
|
||||
- `src/app/ai-render-workbench/`
|
||||
- `src/app/profile/`
|
||||
- `src/app/users/`
|
||||
- `src/app/admin/`
|
||||
- `src/app/trash/`
|
||||
|
||||
### API 目录
|
||||
### 旧 CAD 兼容入口
|
||||
|
||||
- `src/app/api/asset-center`
|
||||
- `src/app/api/assisted-assembly`
|
||||
- `src/app/api/draft-groups`
|
||||
- `src/app/api/intake`
|
||||
- `src/app/api/planning`
|
||||
- `src/app/api/pre-skus`
|
||||
- `src/app/api/series-templates`
|
||||
- `src/app/api/warehouse`
|
||||
旧 CAD 链路继续保留,但它不是新平台主链:
|
||||
|
||||
### 服务与仓库
|
||||
- `src/app/products/`
|
||||
- `src/app/products/[id]/`
|
||||
- `src/app/products/[id]/assembly/`
|
||||
- `src/app/products/[id]/skus/[skuId]/assembly/`
|
||||
- `src/components/assembly/`
|
||||
- `src/components/3d/`
|
||||
|
||||
- `src/server/services/asset-center.service.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/server/services/planning.service.ts`
|
||||
- `src/server/services/series-template.service.ts`
|
||||
- `src/server/services/warehouse.service.ts`
|
||||
- `src/server/repositories/asset-center.repository.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
- `src/server/repositories/planning.repository.ts`
|
||||
- `src/server/repositories/series-template.repository.ts`
|
||||
- `src/server/repositories/warehouse.repository.ts`
|
||||
|
||||
### 纯规则与类型
|
||||
|
||||
- `src/lib/open-product`
|
||||
- `src/lib/planning`
|
||||
|
||||
### 这里的业务真相是什么
|
||||
|
||||
- 资产族:`AssetFamily`
|
||||
- 规格款:`AssetVariant`
|
||||
- 草稿组:`DraftGroup`
|
||||
- 准 SKU:`PreSku`
|
||||
- 入库结果:正式 `Product / SKU`
|
||||
|
||||
### 不要做的事
|
||||
|
||||
- 不直接从辅助装配跳过草稿组生成正式产品。
|
||||
- 不把历史 `auto-combinations` 旧链重新带回当前主线。
|
||||
- 不把旧 `Product / SKU` 当作新平台组合阶段的数据真相。
|
||||
- 不把包装做成绕过草稿组的独立系统。
|
||||
旧链路可以修 bug,但不能把旧对象模型反向扩散到新平台主链。
|
||||
|
||||
---
|
||||
|
||||
## 3. 包装与仓库协作边界
|
||||
## 2. 新平台主链模块
|
||||
|
||||
### 当前包装与仓库主要入口分四条
|
||||
### 资产中心
|
||||
|
||||
- 轻仓库页面:`src/app/warehouse`
|
||||
- 轻仓库接口:`src/app/api/warehouse`
|
||||
- 新平台承接口:`src/app/api/pre-skus/[id]/packaging/route.ts`
|
||||
- 旧正式产品口:`src/app/api/products/[id]/packaging`
|
||||
职责:
|
||||
|
||||
### 主要服务与仓库
|
||||
- 管理产品族、规格、材质、花色、元组件和基础资产。
|
||||
- 为辅助装配、草稿组、产品资料和 AI 链路提供上游事实。
|
||||
|
||||
- `src/server/services/warehouse.service.ts`
|
||||
- `src/server/repositories/warehouse.repository.ts`
|
||||
- `src/server/services/packaging.service.ts`
|
||||
- `src/server/repositories/packaging.repository.ts`
|
||||
主要位置:
|
||||
|
||||
### 当前约定
|
||||
- `src/app/asset-center/`
|
||||
- `src/app/api/asset-center/`
|
||||
- `src/components/platform/`
|
||||
|
||||
- 包装结果最终按单个 `PreSku` 保存。
|
||||
- 正式产品包装链保留,作为兼容与发布后使用。
|
||||
- 轻仓库工作区可以独立协作推进,但轻仓库承接的正式 `Product / SKU` 仍然属于主线真相,不在仓库侧重复造数。
|
||||
- 工艺图组合本期只预留接口,不在主线里单独展开。
|
||||
- 草稿组详情页里的包装区只负责主线最小联通版占位,不是最终包装工作区。
|
||||
- 轻仓库可以增强包装成品、库存、库位和展示交互,但不能绕过正式入库单独制造主链结果。
|
||||
不要在资产中心里直接完成正式入库、仓库流转或批次质检。
|
||||
|
||||
### 包装/仓库线可读但不默认直改的共享接缝
|
||||
### 辅助装配
|
||||
|
||||
职责:
|
||||
|
||||
- 基于资产和元组件生成或维护装配方案。
|
||||
- 输出可进入草稿组的中间结果。
|
||||
|
||||
主要位置:
|
||||
|
||||
- `src/app/assisted-assembly/`
|
||||
- `src/app/api/assisted-assembly/`
|
||||
- `src/lib/planning/`
|
||||
- `src/app/api/planning/`
|
||||
|
||||
辅助装配不能绕过草稿组直接写正式产品。
|
||||
|
||||
### 草稿组
|
||||
|
||||
职责:
|
||||
|
||||
- 承接辅助装配结果。
|
||||
- 管理进入产品资料前的草稿、预 SKU、批次和确认状态。
|
||||
|
||||
主要位置:
|
||||
|
||||
- `src/app/draft-groups/`
|
||||
- `src/app/api/draft-groups/`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
- `src/server/repositories/draft-group.repository.ts`
|
||||
|
||||
草稿组是高共享接缝,改动时必须检查 `intake`、`pre-skus`、包装和仓库调用方。
|
||||
|
||||
### 产品资料和正式入库
|
||||
|
||||
职责:
|
||||
|
||||
- 管理正式入库前后的产品资料。
|
||||
- 承接非集成实拍图、AI 白底图、素材库回传、SKU 和批次上下文。
|
||||
|
||||
主要位置:
|
||||
|
||||
- `src/app/intake/`
|
||||
- `src/app/api/intake/`
|
||||
- `src/server/services/intake.service.ts`
|
||||
- `src/app/intake`
|
||||
- `prisma/schema.prisma`
|
||||
- `src/app/draft-groups/[id]`
|
||||
- `src/lib/open-product/`
|
||||
|
||||
这些文件不是包装/仓库线的默认自由修改区。
|
||||
如果任务必须改这里,先在任务单里写清原因、影响范围,再由主线负责人确认后再动。
|
||||
非集成产品链路使用 `IntakeNonIntegratedProduct / IntakeNonIntegratedSkuItem`。这些对象不能和正式 `Product / SKU`、`AssetVariant` 或 `PreSku` 随便互换。
|
||||
|
||||
白话理解:
|
||||
### 仓库管理和仓库质检
|
||||
|
||||
- 可以看,可以评估,可以提出接缝需求
|
||||
- 不能在没有对齐的情况下直接开改
|
||||
- 特别是 `draft-group`、`intake`、`schema.prisma` 这几处,本阶段只允许做边界说明,不允许顺手把实现也改掉
|
||||
- 如果只是为了补包装 UI、材料选择器、最低成本方案,或补轻仓库展示与交互,不应默认落在这些共享接缝里完成
|
||||
- 主线收口人检查同事分支时,如果这些共享接缝被改了,默认不要直接整支 `merge`;优先确认是否该 `cherry-pick` 或手工整合
|
||||
职责:
|
||||
|
||||
### 包装与仓库负责人不要乱碰的地方
|
||||
- `warehouse` 处理仓库管理和轻仓库流转。
|
||||
- `warehouse-quality` 处理仓库质检工作台和质检任务。
|
||||
|
||||
- 辅助装配主链规则(含手动拼装工作台保存口径)
|
||||
- 草稿组转正式 SKU 的规则
|
||||
- 正式入库的核心落库规则
|
||||
- 旧 CAD 的板件/孔位/五金推导链
|
||||
主要位置:
|
||||
|
||||
- `src/app/warehouse/`
|
||||
- `src/app/api/warehouse/`
|
||||
- `src/app/warehouse-quality/`
|
||||
- `src/app/api/warehouse-quality/`
|
||||
|
||||
仓库质检可以读取产品和 SKU 上下文,但不能在没有业务确认的情况下替代产品资料链路创建正式 SKU。
|
||||
|
||||
---
|
||||
|
||||
## 4. 旧 CAD 兼容链边界
|
||||
## 3. SPU、SKU、barcode 和批次
|
||||
|
||||
### 页面目录
|
||||
这四个概念是当前产品资料和素材库对接的硬边界:
|
||||
|
||||
- `src/app/products/[id]`
|
||||
- `src/app/products/[id]/assembly`
|
||||
- `src/app/products/[id]/skus/[skuId]`
|
||||
- `src/app/products/[id]/skus/[skuId]/assembly`
|
||||
- `src/components/assembly`
|
||||
- `src/components/3d`
|
||||
- SPU 是产品款。
|
||||
- SKU 是 SPU 下的具体子项。
|
||||
- barcode 是逐步收敛的唯一业务定位值。
|
||||
- 批次是当前产品资料链路的批次上下文。
|
||||
|
||||
### API 目录
|
||||
规则:
|
||||
|
||||
- `src/app/api/products/[id]/assembly`
|
||||
- `src/app/api/products/[id]/panels`
|
||||
- `src/app/api/products/[id]/holes`
|
||||
- `src/app/api/products/[id]/import-hardware`
|
||||
- `src/app/api/holes`
|
||||
- `src/app/api/hardware`
|
||||
- `src/app/api/product-hardware`
|
||||
|
||||
### 服务与库
|
||||
|
||||
- `src/server/services/assembly.service.ts`
|
||||
- `src/server/services/panel.service.ts`
|
||||
- `src/server/services/hole.service.ts`
|
||||
- `src/server/services/hardware.service.ts`
|
||||
- `src/server/services/sku-cad-import.helper.ts`
|
||||
- `src/server/repositories/panel.repository.ts`
|
||||
- `src/server/repositories/hole.repository.ts`
|
||||
- `src/server/repositories/hardware.repository.ts`
|
||||
- `src/server/repositories/product-hardware.repository.ts`
|
||||
|
||||
### 纯规则与共享链路
|
||||
|
||||
- `src/lib/assembly-generator.ts`
|
||||
- `src/lib/panel-assembly-semantic.ts`
|
||||
- `src/lib/sku-cad-data.ts`
|
||||
- `src/lib/3d/engine.ts`
|
||||
|
||||
### 当前约定
|
||||
|
||||
- 装配语义是旧链增强能力,不是新平台主数据源。
|
||||
- 旧链可以继续生成安装步骤和预览,但不要反向主导新平台资产模型。
|
||||
- 不能把 SPU、SKU、barcode 当成同一个字段混用。
|
||||
- 基于某个 SKU 换色得到的新 SKU,只能属于同一个 SPU。
|
||||
- 换色得到的新 SKU 只能归属当前批次,不能漂到其他批次。
|
||||
- 如果外部素材库要求 barcode,必须传真实产品条码,不能拿 SKU 编号或 SPU 编号代替。
|
||||
- 如果一个功能还只是草稿或预览,命名里要写清楚 `draft`、`preview` 或 `pending`,不要伪装成正式 SKU。
|
||||
|
||||
---
|
||||
|
||||
## 5. 共享层边界
|
||||
## 4. AI、图片和素材库边界
|
||||
|
||||
### 所有人都可能会碰到,但不要随手改
|
||||
AI 白底图、AI 出图工作台、素材库回传和对象存储相关功能必须遵守:
|
||||
|
||||
- 图片上传后,数据库写入失败时要回滚刚上传的对象。
|
||||
- 软删除、回收站记录仍然算引用,不能同步删除文件。
|
||||
- 七牛或其他对象存储清理只能通过审计确认无引用后执行。
|
||||
- 图片类页面以图片为第一视觉重点,不堆重复说明和统计块。
|
||||
- 查看原图、缩放、拖拽、生成、上传素材库这些能力应就近服务当前产品、SKU、批次上下文。
|
||||
|
||||
主要位置:
|
||||
|
||||
- `src/app/ai-render-workbench/`
|
||||
- `src/app/api/intake/`
|
||||
- `src/app/api/storage/`
|
||||
- `src/app/api/external/`
|
||||
- `src/components/platform/`
|
||||
|
||||
素材库对接时必须严格区分 SPU、SKU、barcode 和产品分类。
|
||||
|
||||
---
|
||||
|
||||
## 5. 旧 CAD 兼容链路
|
||||
|
||||
旧链路主要围绕:
|
||||
|
||||
- `Product`
|
||||
- `SKU`
|
||||
- `Panel`
|
||||
- `Hole`
|
||||
- `ProductHardware`
|
||||
- 旧 3D 组装和板件孔位编辑
|
||||
|
||||
主要位置:
|
||||
|
||||
- `src/app/products/`
|
||||
- `src/app/api/products/`
|
||||
- `src/app/api/skus/`
|
||||
- `src/app/api/panels/`
|
||||
- `src/app/api/holes/`
|
||||
- `src/app/api/product-hardware/`
|
||||
- `src/components/assembly/`
|
||||
- `src/components/3d/`
|
||||
|
||||
旧链路可以和新平台共享基础数据,但不能把新平台的草稿组、资产中心、非集成产品资料逻辑硬塞进旧 CAD 编辑器。
|
||||
|
||||
---
|
||||
|
||||
## 6. 共享基础层
|
||||
|
||||
下面位置属于全项目共享基础层:
|
||||
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations`
|
||||
- `src/server/services/index.ts`
|
||||
- `src/server/repositories/index.ts`
|
||||
- `prisma/migrations/`
|
||||
- `src/app/layout.tsx`
|
||||
- `src/components/layout/`
|
||||
- `src/components/platform/`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `src/app/api/storage/`
|
||||
- `src/lib/prisma.ts`
|
||||
- `src/server/repositories/`
|
||||
- `src/server/services/`
|
||||
|
||||
共享层边界的意思是:
|
||||
要求:
|
||||
|
||||
- 这些地方一改,通常会同时影响主链、包装线或兼容链
|
||||
- 所以默认先对齐影响面,再决定谁来改,不是谁先看到谁就顺手动
|
||||
|
||||
### 共享层规则
|
||||
|
||||
- 任何 Prisma 模型调整,都必须带迁移。
|
||||
- 任何新接口,都要有明确输入输出 DTO。
|
||||
- 任何跨模块字段变更,都要先同步文档,再改代码。
|
||||
- 没有明确任务授权时,不要顺手扩写到 `draft-group`、`intake`、`schema.prisma` 这些敏感接缝实现。
|
||||
- 前端页面优先使用平台组件和元组件,不绕开组件库单独造一套视觉。
|
||||
- API 保持 `Route -> Service -> Repository` 分层,不在 route 里堆业务逻辑。
|
||||
- Prisma schema 变化必须配套 migration,不能在生产或预生产直接 `prisma db push`。
|
||||
- 共享服务改动后必须看调用方,不能只跑当前页面。
|
||||
|
||||
---
|
||||
|
||||
## 6. 看到这些情况要先停一下
|
||||
## 7. 视觉边界
|
||||
|
||||
- 一个任务同时改新平台主链和旧兼容链,但任务单里没写原因。
|
||||
- 包装任务直接改共享接缝文件,但没先和主线负责人对齐。
|
||||
- Route 里开始直接写 Prisma 查询。
|
||||
- 包装开始自己造一套 `Product / SKU`。
|
||||
- 页面样式开始绕开 `src/components/platform` 和既有视觉标准。
|
||||
- 改动影响命名、BOM、包装、安装步骤,但没说明数据来源。
|
||||
- 分支改动会影响 `intake / warehouse / Product / SKU` 承接链,但交接里没说明对正式入库或轻仓库的影响。
|
||||
当前视觉标准:
|
||||
|
||||
- 第一标准:白曜石工坊统一视觉。
|
||||
- 实现载体:项目组件库、元组件和 `src/components/platform/`。
|
||||
- 外部设计稿:Stitch 是规格和验收标准,不替代项目组件体系。
|
||||
- 项目内参考:资产中心页面优先级最高。
|
||||
|
||||
不要做:
|
||||
|
||||
- 普通 SaaS 卡片墙。
|
||||
- 营销式大 hero。
|
||||
- 旧苹果风玻璃大面积正文。
|
||||
- 默认黑底后台。
|
||||
- 一堆重复说明块、重复统计块和无效状态块。
|
||||
|
||||
表格、表单、Inspector、列表和长文本必须优先保证可读性。
|
||||
|
||||
---
|
||||
|
||||
## 7. 开工前最少要看什么
|
||||
## 8. 数据库和迁移边界
|
||||
|
||||
### 做新平台主链
|
||||
数据库相关任务先看:
|
||||
|
||||
先看:
|
||||
- `docs/workflow/数据库迁移操作手册.md`
|
||||
- `docs/开发指南/数据库基础命令速查.md`
|
||||
- `prisma/schema.prisma`
|
||||
- `prisma/migrations/`
|
||||
|
||||
- `docs/workflow/团队协作模式.md`
|
||||
- `src/components/platform/platform-navigation.ts`
|
||||
- `src/lib/open-product/types.ts`
|
||||
- `src/lib/open-product/assembly-workspace.ts`
|
||||
- `src/lib/open-product/manual-assembly-launch.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
规则:
|
||||
|
||||
### 做包装或仓库协作
|
||||
- 结构变化走 migration。
|
||||
- 数据修复先只读审计,再写修复脚本。
|
||||
- 生产数据操作必须有用户明确授权。
|
||||
- 不把一次性修复脚本伪装成长期业务逻辑。
|
||||
|
||||
先看:
|
||||
---
|
||||
|
||||
- `docs/workflow/团队协作模式.md`
|
||||
- `docs/开发指南/包装方案增强/包装方案增强-同事交接说明.md`
|
||||
- `src/app/warehouse`
|
||||
- `src/app/api/warehouse`
|
||||
- `src/app/api/pre-skus/[id]/packaging/route.ts`
|
||||
- `src/server/services/packaging.service.ts`
|
||||
- `src/server/services/draft-group.service.ts`
|
||||
## 9. 改动前最小检查
|
||||
|
||||
### 做旧 CAD 兼容链
|
||||
不知道该从哪里开始时,先看这些文件:
|
||||
|
||||
先看:
|
||||
- `AGENTS.md`
|
||||
- `src/app/page.tsx`
|
||||
- `src/components/layout/`
|
||||
- `src/components/platform/`
|
||||
- `src/app/asset-center/`
|
||||
- `src/app/intake/`
|
||||
- `src/app/draft-groups/`
|
||||
- `src/app/api/platform/schemas.ts`
|
||||
- `prisma/schema.prisma`
|
||||
|
||||
- `docs/solutions/workflow-issues/CAD插件装配语义导出改造说明-20260325.md`
|
||||
- `src/app/products/[id]/assembly/AssemblyEditorWorkspace.tsx`
|
||||
- `src/components/assembly/AssemblyEditorClient.tsx`
|
||||
- `src/server/services/assembly.service.ts`
|
||||
- `src/lib/assembly-generator.ts`
|
||||
如果任务涉及生产、部署、数据库或对象存储,先只读核查现场,再决定是否写入。
|
||||
|
||||
---
|
||||
|
||||
## 10. 判断是否越界
|
||||
|
||||
出现下面情况,先停下来重新确认:
|
||||
|
||||
- 为了修一个页面,改了 schema、全局 layout 或共享 service。
|
||||
- 为了一个 SKU,把 SPU、barcode、批次一起改了。
|
||||
- 为了临时过 UI,把旧 CAD 模型塞进新平台主链。
|
||||
- 为了清理文件,准备删除软删除或回收站仍引用的对象。
|
||||
- 为了追视觉,把页面绕开组件库重写成另一套体系。
|
||||
- 为了赶部署,跳过主线同步、构建、健康检查或规范审查。
|
||||
|
||||
越界不一定代表不能做,但必须说明原因、影响范围、验证方式和回滚路径。
|
||||
|
||||
@ -15,9 +15,14 @@
|
||||
|
||||
## 本仓库部署脚本
|
||||
|
||||
- 外层仓库脚本:`../scripts/create-yydd-prod-deploy.ps1`
|
||||
- 首选入口:仓库根目录 `scripts/deploy-yydd-prod-from-git.ps1`
|
||||
- 底层打包脚本:仓库根目录 `scripts/create-yydd-prod-deploy.ps1`
|
||||
|
||||
执行部署或生产核查前,优先读取上面脚本的默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。该脚本生成的远端发布脚本会执行 `npx prisma migrate deploy`,因此生产发布前必须先确认迁移目录、迁移历史和主数据 strict 巡检口径没有分叉。
|
||||
执行部署或生产核查前,优先读取上面两个脚本的当前默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。部署入口默认从 `origin/main` 取代码,发布前必须核对远端 `refs/heads/main` 和本地 `origin/main` 是否一致。
|
||||
|
||||
底层发布流程会执行依赖安装、`prisma generate`、迁移、构建、PM2 重启和健康检查。因此生产发布前必须先确认迁移目录、迁移历史和主数据 strict 巡检口径没有分叉。
|
||||
|
||||
只做生产/目标库地基核查时,优先在远端应用目录执行 `npm run db:foundation:audit:production-readonly`。该命令不事务回放治理迁移 SQL,只通过只读迁移状态和质量查询生成 `.master-data-audits/foundation-production-readonly-current/manifest.json`,适合把脱敏 summary 或 manifest 交回主线收口。拿到 manifest 后,用 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>` 做机器验收;只有 `accepted: true` 才能把这份证据计入生产级地基收口。
|
||||
|
||||
## 花名册同步
|
||||
|
||||
@ -29,6 +34,57 @@
|
||||
- 定时频率:每天 `03:00` 和 `15:00`。
|
||||
- 源库连接信息只允许放在生产本地 `/opt/furniture-3d-system-prod/shared/employee-directory-sync.env`,不能写入 Git 或共享文档。
|
||||
- 生产应用内部接口密钥继续从 `/opt/furniture-3d-system-prod/shared/.env` 的 `HR_SYNC_SECRET` 读取。
|
||||
- 人员信息表的钉钉组织同步使用 `furniture-3d-system/scripts/install-dingtalk-directory-sync-timer.sh`,生产 systemd 名称为 `furniture-dingtalk-directory-sync.service` / `furniture-dingtalk-directory-sync.timer`。
|
||||
- 钉钉同步每天 `03:00` 执行一次,读取生产共享 `.env` 中的 `DINGTALK_API_CLIENT_ID` 和 `DINGTALK_API_CLIENT_SECRET`,并自动更新人员信息;正式页面仍保持只读。钉钉网页登录使用独立的 `DINGTALK_LOGIN_CLIENT_ID`、`DINGTALK_LOGIN_CLIENT_SECRET`,两组凭据不能混用。
|
||||
- 人员信息的每日更新口径:钉钉新人自动新增一行;在职人员按钉钉更新部门/负责人;人工覆盖(manualOverride)的行保持不变;**离职人员不做删除,只在页面把「在职状态」变为离职**(方案 B:先比对上一轮在册且本轮抓取范围内消失的人,再逐个调 `user/get` 确认 60121 用户不存在才标记;抓取人数明显缩水时整轮跳过并写入告警)。
|
||||
- 首次部署后需在正式服务器执行该安装脚本,并手动执行一次 `npm run sync:dingtalk-directory:apply` 完成首次填充;后续由 systemd 每日执行。
|
||||
|
||||
## BOM 成本参考镜像
|
||||
|
||||
- 用户核算请求只读取本地 PostgreSQL 镜像,不在请求中连接上游 `rpt.pm_bomBaseInfo`。
|
||||
- 低压只读预检:`npm run sync:bom-cost-reference -- --dry-run --max-pages=2`;默认单连接、每页 100 行、完整页之间等待 250ms。
|
||||
- 真实同步必须显式传 `--apply`、固定确认词、目标 PostgreSQL 身份和来源 SQL Server 身份;上游登录必须是 `report_reader`,`app_writer` 会被程序拒绝。
|
||||
- 生产定时器安装脚本:`furniture-3d-system/scripts/install-bom-cost-reference-sync-timer.sh`。
|
||||
- 生产 systemd 名称:`furniture-bom-cost-reference-sync.service` / `furniture-bom-cost-reference-sync.timer`。
|
||||
- 默认调度:每天中国标准时间 `02:00`,附带最多 10 分钟随机延迟;安装时可通过非敏感环境变量调整。
|
||||
- 上游连接继续只使用生产共享 `.env` 中现有的 `JST_PLANNING_SQLSERVER_* / report_reader`;不在第二个文件中重复保存密码。
|
||||
- 首次运行安装脚本只会创建权限为 `600` 的 `/opt/furniture-3d-system-prod/shared/bom-cost-reference-sync.env` 门禁与限速模板;目标/来源身份未填齐或现有只读配置不完整时拒绝安装定时器。
|
||||
- 本地参考行只保留最新成功批次,避免每日约 76 万行无限增长;历史场景在自己的明细快照中保留来源行、指纹、单价、规则和原始计算依据,不依赖旧镜像行回查。
|
||||
- 镜像新鲜度按最新成功批次的完成时间判断。上游当前可见的创建时间只作审计展示,不能用来判断当天同步是否过期。
|
||||
- 准确度回测:`npm run backtest:bom-cost-reference -- --sample-size=30`,只读最新成功镜像,不访问上游;样本限制 20 至 50 个,任一可比较 SKU 超过固定 10% 误差门槛即失败。
|
||||
- 迁移、真实同步、安装/启用定时器和生产回测都必须另行获得生产操作授权;部署代码本身不代表这些动作已经执行。
|
||||
|
||||
## 商品 SABC 月度核算
|
||||
|
||||
- 核算命令:`npm run run:product-sabc -- --year=2026 --month=8 --mode=APPLY --confirm=run-product-sabc`;试算(只算不写库)用 `--mode=PREVIEW`,末尾加 `--skip-if-exists` 可跳过已生成月份。
|
||||
- 8 月人工表对账命令:`npm run compare:product-sabc-xlsx`,默认读取本机下载的人工工作簿;生产执行前应先完成产品全量档案日销售数据同步。
|
||||
- 生产定时器安装脚本:`furniture-3d-system/scripts/install-product-sabc-timer.sh`。
|
||||
- 生产 systemd 名称:`furniture-product-sabc-monthly.service` / `furniture-product-sabc-monthly.timer`。
|
||||
- 默认调度:每月 3 日中国标准时间 `03:00`,附带最多 10 分钟随机延迟;服务生成上一个自然月的数据,已生成过则跳过(不覆盖管理员的重算结果)。
|
||||
- 生成即冻结:每次生成会把整行明细写入 `SupplyChainProductArchiveSabcResult.snapshot`,页面、筛选下拉、导出和经营总览都只读取该快照。生成之后档案的销售状态、人员字段等变化都不会再改变已生成月份的数据。
|
||||
- 重新生成(页面「执行核算」)仅限管理员(`ADMIN` / `SUPER_ADMIN`);其他账号只能查看冻结结果。本地一键刷新 `node scripts/local-sync-archive.mjs` 在每月 3 日前会跳过 SABC 步骤,需要立刻重算时用 `--force-sabc`。
|
||||
- SABC 的销售收款总金额、真实业绩、发货件数、商品毛利额和售后金额统一读取 `SupplyChainProductArchiveSalesPerformanceDaily`,与产品全量档案页面保持同一月度口径。
|
||||
- 首次安装生成权限为 `600` 的 `/opt/furniture-3d-system-prod/shared/product-sabc-timer.env`,默认 `PRODUCT_SABC_TIMER_MODE=APPLY`;如需先观察可临时改为 `PREVIEW`(只试算不写库、不冻结)。
|
||||
- SABC 自动任务只读取现有 `DATABASE_URL`;上游日销售同步任务继续使用 `JST_REPORT_DB_*`,凭据不写入定时器配置。安装、迁移、首次 APPLY 和生产核算都需要单独授权。
|
||||
|
||||
## 操作日志保留策略
|
||||
|
||||
- 清理脚本:`npm run db:cleanup-operation-logs` 默认只做 dry-run;只有显式传入 `--apply --confirm=cleanup-operation-logs` 才会删除数据。
|
||||
- 自动清理范围严格限定为 `OperationLog` 中 `SYSTEM + SUPPLY_CHAIN_PRODUCT_ARCHIVE + details.kind=UPDATED + SUCCESS` 且创建时间早于 30 天的字段变更。
|
||||
- 用户操作、商品档案导入、失败/重试日志及其他业务范围日志至少保留 365 天;本次策略不自动删除超过 365 天的审计日志。
|
||||
- 生产安装入口:`furniture-3d-system/scripts/install-operation-log-retention-timer.sh`。
|
||||
- 生产 systemd 名称:`furniture-operation-log-retention.service` / `furniture-operation-log-retention.timer`,默认每天中国标准时间 `04:30` 执行,附带最多 10 分钟随机延迟。
|
||||
- 安装脚本会在 `/opt/furniture-3d-system-prod/shared/operation-log-retention.env` 保存权限为 `600` 的数据库身份门禁和非敏感参数;身份未填写或保留天数不是 30 天时拒绝启用。
|
||||
- 首次启用前必须先在生产执行 dry-run,核对候选数量、数据库身份和 systemd 落点;生产安装、首次 `--apply` 和后续代码部署均需另行获得授权。
|
||||
|
||||
## 停产需求确认超时收口
|
||||
|
||||
- 收口脚本:`furniture-3d-system/scripts/finalize-production-stop-confirmations.ts`,把超过确认截止时间仍未反馈的停产需求按「默认同意停产」落库,并给未反馈的确认人记 `TIMEOUT` 票。
|
||||
- 生产安装入口:`furniture-3d-system/scripts/install-production-stop-confirmation-finalize-timer.sh`。
|
||||
- 生产 systemd 名称:`furniture-production-stop-confirmation-finalize.service` / `furniture-production-stop-confirmation-finalize.timer`,默认每 10 分钟检查一次(北京时间),附带最多 1 分钟随机延迟;可用 `PRODUCTION_STOP_FINALIZE_CALENDAR` 覆盖调度。
|
||||
- 收口逻辑幂等:只处理「已过截止时间且仍待确认」的需求,重复执行不会重复收口;失败由 systemd 在 5 分钟后自动重试。
|
||||
- 运行日志:`journalctl -u furniture-production-stop-confirmation-finalize -n 100 --no-pager`,每次运行输出本次收口的需求数量。
|
||||
- 首次安装与启用:在项目根执行 `bash scripts/install-production-stop-confirmation-finalize-timer.sh`(需 root,脚本写入 `/etc/systemd/system/` 并 `enable --now`);安装/启用定时器属于生产操作,需要另行获得授权。
|
||||
|
||||
## 明确排除
|
||||
|
||||
@ -44,4 +100,18 @@
|
||||
2. 远端存在 `/opt/furniture-3d-system-prod`。
|
||||
3. PM2 中存在 `furniture-3d-system-prod`。
|
||||
4. 应用监听端口是 `3001`。
|
||||
5. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
|
||||
5. 部署来源是 `origin/main`,且远端 `main` 和本地 `origin/main` 一致。
|
||||
6. 生产只读地基核查优先使用 `npm run db:foundation:audit:production-readonly`。
|
||||
7. 生产只读 manifest 回传后,使用 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>` 验收,不能人工挑几个字段就宣称生产通过。
|
||||
8. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
|
||||
|
||||
## 部署后核查
|
||||
|
||||
部署后不能只看公网 `/api/health`。至少补核:
|
||||
|
||||
- 远端 `REVISION` 或当前发布目录 commit。
|
||||
- `pm2 describe furniture-3d-system-prod`。
|
||||
- 应用端口 `3001` 是否监听。
|
||||
- 最近错误日志是否有启动失败、数据库失败、对象存储失败。
|
||||
- `https://cp.jiansun.vip/api/health`。
|
||||
- 关键页面:首页、产品资料、仓库质检、非集成详情或本次改动涉及页面。
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
- 有没有明显偏离当前统一视觉标准
|
||||
- 新平台主链页面、后台管理页面是否优先符合 `白曜石工坊统一视觉标准.md`
|
||||
- 是否仍把旧苹果风格规范当作当前视觉依据;新平台验收只按白曜石工坊口径判断
|
||||
- 是否优先使用 `src/components/platform`、元组件和组件库,而不是另起一套页面皮肤
|
||||
- 如果有 Stitch 设计稿,是否在组件库体系内对齐它的密度、层级、控件、留白、字号和低噪音表达
|
||||
- 有没有临时拼凑感很强的新界面
|
||||
- 新页面和现有页面的层级、间距、交互有没有失控
|
||||
|
||||
@ -38,5 +40,6 @@
|
||||
|
||||
- 明显破坏模块边界
|
||||
- 明显破坏统一视觉
|
||||
- 绕开组件库另造一套新平台视觉
|
||||
- 明显只是补丁式修复
|
||||
- 没做最小验证就宣称完成
|
||||
|
||||
@ -24,10 +24,19 @@
|
||||
- 每天开始开发前,先同步一次最新 `origin/codex/xuxiake-mainline`
|
||||
- 主负责人刚推了重要改动后,再同步一次
|
||||
- 自己准备提交前,再同步一次
|
||||
- 每次回答“主线有没有新提交”前,先 `git fetch origin`
|
||||
|
||||
白话理解:
|
||||
不要拿旧主线继续往下改。
|
||||
|
||||
推荐固定检查:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git status --short --branch
|
||||
git rev-list --left-right --count HEAD...origin/codex/xuxiake-mainline
|
||||
```
|
||||
|
||||
### 铁律 2:每次任务先写清边界
|
||||
|
||||
- 先说这次只改什么
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# 3D 装配动画使用指南
|
||||
|
||||
> 如何使用装配动画功能展示家具组装过程
|
||||
>
|
||||
> 当前状态:历史兼容指南。旧文中的 Apple 风格表述只代表当时实现口径;新平台 3D、装配、资产工作台视觉以白曜石工坊标准、平台组件和当前资产中心样板为准。
|
||||
|
||||
---
|
||||
|
||||
@ -10,7 +12,7 @@
|
||||
- ✅ 流畅播放 3D 装配动画(60 FPS)
|
||||
- ✅ 分步骤展示组装过程
|
||||
- ✅ 支持播放控制(播放/暂停/跳转)
|
||||
- ✅ Apple 风格的精美 UI
|
||||
- ✅ 历史实现使用 Apple 风格 UI;新平台应按白曜石工坊和平台组件重整
|
||||
- ✅ 响应式设计
|
||||
|
||||
---
|
||||
@ -105,7 +107,9 @@ interface Panel {
|
||||
|
||||
## 🎨 UI 特性
|
||||
|
||||
### Apple 风格设计
|
||||
### 历史 Apple 风格设计
|
||||
|
||||
当前新平台不再把 Apple 风格作为第一标准;这里仅用于理解旧实现。
|
||||
|
||||
- **流畅动画**:使用 GSAP 实现 60 FPS 动画
|
||||
- **毛玻璃效果**:`backdrop-blur-md`
|
||||
@ -240,7 +244,7 @@ GLTF 节点名称应该清晰易懂:
|
||||
|
||||
- [Three.js 文档](https://threejs.org/docs/)
|
||||
- [GSAP 文档](https://greensock.com/docs/)
|
||||
- [Apple HIG 2025](https://developer.apple.com/design/human-interface-guidelines/)
|
||||
- [Apple HIG 2025](https://developer.apple.com/design/human-interface-guidelines/):历史参考,不作为当前新平台验收标准
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 产品上传 API 使用指南
|
||||
|
||||
> 当前状态:历史兼容指南。本文记录旧产品上传链路,里面出现的 Apple HIG、MinIO 等表述不能作为当前新平台视觉或生产存储真相。新前端视觉以 `docs/设计规范/白曜石工坊统一视觉标准.md` 和 `docs/开发指南/组件使用指南.md` 为准;对象存储以当前代码、`src/lib/storage`、对象存储接入文档和生产环境变量核对为准。
|
||||
|
||||
## 概述
|
||||
|
||||
产品上传功能已完成,支持批量上传 3D 模型文件并自动创建产品和 SKU。
|
||||
@ -7,13 +9,13 @@
|
||||
## 功能特性
|
||||
|
||||
✅ **已完成**:
|
||||
- Apple HIG 2025 风格的上传弹窗
|
||||
- 历史实现曾使用 Apple HIG 2025 风格上传弹窗;新界面应改按白曜石工坊和平台组件口径
|
||||
- 拖拽上传和点击选择文件
|
||||
- 批量上传多个文件
|
||||
- 实时上传进度显示
|
||||
- 文件验证(格式、大小)
|
||||
- 产品信息表单
|
||||
- 文件上传到 MinIO
|
||||
- 历史实现上传到 MinIO;当前对象存储真相需按对象存储接入文档和代码核对
|
||||
- 自动创建产品和 SKU 记录
|
||||
- 上传成功后自动刷新列表
|
||||
|
||||
@ -84,7 +86,7 @@ GET /api/products
|
||||
4. 拖拽或选择 3D 模型文件
|
||||
5. 显示上传进度
|
||||
6. 点击"确认上传"
|
||||
7. 文件上传到 MinIO
|
||||
7. 文件上传到历史 MinIO 存储;当前实现需按对象存储接入文档核对
|
||||
8. 创建产品和 SKU 记录
|
||||
9. 自动刷新产品列表
|
||||
|
||||
@ -100,7 +102,7 @@ GET /api/products
|
||||
确保 `.env` 文件中配置了以下变量:
|
||||
|
||||
```env
|
||||
# MinIO 配置
|
||||
# 历史 MinIO 配置
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET=furniture-models
|
||||
@ -118,7 +120,7 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/furniture3d
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. 确保 MinIO 和 PostgreSQL 正在运行
|
||||
2. 历史本地联调需确保 MinIO 和 PostgreSQL 正在运行;当前存储联调按对象存储接入文档核对
|
||||
|
||||
3. 访问 http://localhost:3000
|
||||
|
||||
@ -131,7 +133,7 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/furniture3d
|
||||
## 故障排查
|
||||
|
||||
### 上传失败
|
||||
- 检查 MinIO 是否运行:访问 http://localhost:9000
|
||||
- 历史 MinIO 联调检查:访问 http://localhost:9000;当前对象存储问题按现行存储配置排查
|
||||
- 检查数据库连接:运行 `npm run prisma:studio`
|
||||
- 查看浏览器控制台错误信息
|
||||
- 查看服务器日志
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 包装方案增强同事交接说明(新平台一期版)
|
||||
|
||||
当前状态:包装专项历史交接。本文适合理解包装增强任务背景,但当前分支、主线、视觉标准、共享接缝和路径判断必须以仓库根目录 `AGENTS.md`、`docs/workflow/团队协作模式.md`、`docs/workflow/模块边界说明.md` 和当前代码为准。
|
||||
|
||||
## 1. 先说结论
|
||||
|
||||
这份文档是给接手「包装方案增强」任务的同事看的。
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 包装方案增强操作手册(非程序员版,新平台一期)
|
||||
|
||||
当前状态:包装专项历史说明。本文可以帮助理解包装功能,但涉及分支、主线、视觉和代码路径时,必须以仓库根目录 `AGENTS.md`、workflow 文档和当前代码为准。
|
||||
|
||||
## 1. 先看结论
|
||||
|
||||
你这次要做的事情只有一条:
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
适用对象:接手「新平台包装子工作区」的 Codex。
|
||||
目标:让你直接在现有后端底座上继续做包装 UI、算料和最低成本方案,不要再重定 DTO、数据表和入库规则。
|
||||
|
||||
当前状态:包装专项历史交接。本文仍可用于理解包装底座,但分支、主线、视觉、共享接缝和路径判断必须先看仓库根目录 `AGENTS.md`、`docs/workflow/团队协作模式.md`、`docs/workflow/模块边界说明.md`、`docs/workflow/主线同步操作手册.md`,再核对当前代码。不要把本文里的固定包装分支或旧协作口径当成当前唯一规则。
|
||||
|
||||
---
|
||||
|
||||
## 1. 先说结论
|
||||
|
||||
@ -1,653 +1,170 @@
|
||||
# Apple 风格 UI 组件使用指南
|
||||
# 平台组件与元组件使用指南
|
||||
|
||||
> **⚠️ 重要提示**: 本文档提供代码示例,但设计标准以 `docs/设计规范/苹果风格视觉标准.md` 为准。在使用代码示例时,请确保符合最新的设计 Token(如颜色、字体等)。
|
||||
适用范围:新平台页面、后台管理页、产品资料、AI 白底图、资产中心、仓库质检等前端改造。
|
||||
|
||||
本指南提供了 Apple 风格 UI 设计系统中所有组件的详细使用说明和最佳实践。
|
||||
当前项目不再以旧 Apple 风格组件作为新页面标准。旧 `src/components/ui` 里的组件和旧苹果风文档只作为历史兼容参考;新平台默认优先使用平台组件、元组件和白曜石工坊视觉口径。
|
||||
|
||||
## 目录
|
||||
---
|
||||
|
||||
1. [设计令牌](#设计令牌)
|
||||
2. [基础组件](#基础组件)
|
||||
3. [导航组件](#导航组件)
|
||||
4. [数据展示组件](#数据展示组件)
|
||||
5. [反馈组件](#反馈组件)
|
||||
6. [媒体组件](#媒体组件)
|
||||
7. [布局组件](#布局组件)
|
||||
8. [动画组件](#动画组件)
|
||||
9. [可访问性工具](#可访问性工具)
|
||||
10. [性能优化](#性能优化)
|
||||
## 1. 先记住一句话
|
||||
|
||||
## 设计令牌
|
||||
新平台前端优先顺序是:
|
||||
|
||||
### 字体系统
|
||||
|
||||
```typescript
|
||||
import { LargeTitle, Title1, Body, Caption1 } from '@/components/ui/Typography'
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<LargeTitle>大标题 (34pt)</LargeTitle>
|
||||
<Title1>标题 1 (28pt)</Title1>
|
||||
<Body>正文内容 (17pt)</Body>
|
||||
<Caption1>说明文字 (12pt)</Caption1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```text
|
||||
业务组件和元组件 -> src/components/platform -> 已有页面样板 -> 基础 UI 组件 -> 临时自写样式
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 使用语义化的字体组件而不是直接的 CSS 类
|
||||
- 遵循苹果的字体层级,不要跳级使用
|
||||
- 中英文混排时使用 `fontType="chinese"` 属性
|
||||
不要为了一个页面单独造一套皮肤。
|
||||
如果 Stitch 设计稿和当前组件默认皮肤不一致,应该在组件库体系内做变体、重写皮肤或局部重构,而不是绕开组件库硬写静态稿。
|
||||
|
||||
### 颜色系统
|
||||
---
|
||||
|
||||
```typescript
|
||||
// 使用 Tailwind 类名
|
||||
<div className="bg-apple-blue-light text-white">
|
||||
蓝色背景
|
||||
</div>
|
||||
## 2. 当前第一视觉标准
|
||||
|
||||
// 使用设计令牌
|
||||
import { appleColorTokens } from '@/lib/design-tokens'
|
||||
const blueColor = appleColorTokens.system.blue.light
|
||||
- 第一视觉标准:`docs/设计规范/白曜石工坊统一视觉标准.md`
|
||||
- 规范审查:`docs/workflow/规范审查清单.md`
|
||||
- 页面样板:资产中心页面
|
||||
- 核心组件目录:`src/components/platform/`
|
||||
|
||||
页面应保持:
|
||||
|
||||
- 浅色工作台。
|
||||
- 低噪音。
|
||||
- 主体内容优先。
|
||||
- 图片、3D、数据和操作路径优先。
|
||||
- 表格、Inspector、表单和长文本可读性优先。
|
||||
|
||||
不要回到:
|
||||
|
||||
- 旧苹果风大玻璃组件。
|
||||
- 普通 SaaS 卡片墙。
|
||||
- 默认黑底后台。
|
||||
- 营销式大 hero。
|
||||
- 一堆解释性卡片和重复统计块。
|
||||
|
||||
---
|
||||
|
||||
## 3. 平台组件怎么选
|
||||
|
||||
优先看 `src/components/platform/`:
|
||||
|
||||
- `PlatformPageFrame`:页面骨架和工作台外框。
|
||||
- `PlatformSplitLayout`:左右分栏,适合主画布 + Inspector。
|
||||
- `PlatformSection`:工作台内的低噪音区块。
|
||||
- `PlatformFlowNav`:平台流程导航。
|
||||
- `PlatformHeaderTools`:顶部工具区。
|
||||
- `PlatformMetricStrip`:少量关键指标条,不能堆成统计墙。
|
||||
- `ViewportStageShell`:视口、画布、图片或 3D 工作台容器。
|
||||
- `AssemblyWorkbench*`:装配和资产工作台相关承载。
|
||||
- `AssetVariantThumbnailRenderer`、`AssemblyMaterialImage`:资产图和材质图展示。
|
||||
|
||||
使用前先搜现有调用:
|
||||
|
||||
```bash
|
||||
rg -n "PlatformSplitLayout|PlatformPageFrame|ViewportStageShell|PlatformSection" src
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 优先使用语义化颜色(如 `apple-bg-primary`)
|
||||
- 当前产品固定浅色主题,不再提供深浅色切换
|
||||
- 使用系统颜色保持一致性
|
||||
---
|
||||
|
||||
## 基础组件
|
||||
## 4. 元组件表达
|
||||
|
||||
### Button 按钮
|
||||
板材、五金、组件、包装、花色、贴图、资产规格款都不是普通图片卡片。它们应按 `Meta Component(元组件)` 表达:
|
||||
|
||||
```typescript
|
||||
import Button from '@/components/ui/Button'
|
||||
- 有稳定业务身份。
|
||||
- 有规格、材质、状态、来源。
|
||||
- 能打开 Inspector 检视。
|
||||
- 能进入装配、草稿、入库、仓库或质检链路。
|
||||
- 能被 AI、素材库或对象存储链路追踪。
|
||||
|
||||
function ButtonExample() {
|
||||
return (
|
||||
<div className="space-x-4">
|
||||
{/* 四种变体 */}
|
||||
<Button variant="filled">Filled</Button>
|
||||
<Button variant="tinted">Tinted</Button>
|
||||
<Button variant="gray">Gray</Button>
|
||||
<Button variant="plain">Plain</Button>
|
||||
|
||||
{/* 三种尺寸 */}
|
||||
<Button size="small">Small (32px)</Button>
|
||||
<Button size="regular">Regular (44px)</Button>
|
||||
<Button size="large">Large (50px)</Button>
|
||||
|
||||
{/* 状态 */}
|
||||
<Button loading>Loading</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
如果只是展示图片,也要保留业务身份和状态,不要退化成“图片 + 标题 + 说明”的营销卡片。
|
||||
|
||||
---
|
||||
|
||||
## 5. 图片和 AI 页面
|
||||
|
||||
AI 白底图、素材库、质检九视图、原图预览这类页面要把图片放在第一优先级:
|
||||
|
||||
- 图片网格要稳定,不因状态文字改变尺寸。
|
||||
- 缺图要用明确空状态,不显示坏图图标。
|
||||
- 原图查看要避免被页面内其他卡片盖住。
|
||||
- 缩放、拖拽、重新生成、上传素材库等操作应贴近图片工作流。
|
||||
- 不要堆重复说明、重复计数和无效提示。
|
||||
|
||||
目标花色、SPU、SKU、barcode、批次等字段必须从业务上下文传递,不要靠页面文案兜底。
|
||||
|
||||
---
|
||||
|
||||
## 6. Inspector 和表单
|
||||
|
||||
Inspector 是长期阅读和编辑区,不使用大面积毛玻璃。
|
||||
|
||||
推荐:
|
||||
|
||||
- 实体白底。
|
||||
- 清楚边界。
|
||||
- 紧凑但不拥挤。
|
||||
- 字段分组少而明确。
|
||||
- 关键操作按钮固定在可预期位置。
|
||||
|
||||
避免:
|
||||
|
||||
- 把说明文字堆进 Inspector。
|
||||
- 用半透明背景承载正文。
|
||||
- 用多个统计块挤压主图或主表单。
|
||||
- 表单下拉展开后改变整体页面宽高或遮挡关键按钮。
|
||||
|
||||
---
|
||||
|
||||
## 7. Stitch 设计稿落地
|
||||
|
||||
当用户指定 Stitch 设计稿时:
|
||||
|
||||
- Stitch 是当前功能的视觉规格和验收标准。
|
||||
- 项目组件库是实现载体。
|
||||
- 资产中心是项目内参考样板。
|
||||
- 低噪音、抓重点、图片优先是默认方向。
|
||||
|
||||
落地时重点对齐:
|
||||
|
||||
- 密度。
|
||||
- 控件大小。
|
||||
- 留白。
|
||||
- 字号。
|
||||
- 圆角。
|
||||
- 阴影。
|
||||
- 列表和表单层级。
|
||||
- hover、focus、disabled、loading、empty、error 状态。
|
||||
|
||||
不要自行补设计稿里没有的说明区、统计区或装饰区,除非用户明确要求。
|
||||
|
||||
---
|
||||
|
||||
## 8. 基础 UI 组件使用边界
|
||||
|
||||
`src/components/ui/` 可以继续服务历史页面和基础控件,但新平台不能只看这个目录就定视觉方向。
|
||||
|
||||
使用原则:
|
||||
|
||||
- 能用平台组件就先用平台组件。
|
||||
- 需要按钮、输入框、下拉、弹层等基础控件时,可以复用现有 UI 组件。
|
||||
- 如果基础控件默认样式和白曜石/Stitch 冲突,应通过组件变体或局部样式修正。
|
||||
- 不新增 Apple 命名的新组件,不新增深浅色主题切换入口。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验证要求
|
||||
|
||||
前端改动至少自查:
|
||||
|
||||
- 1920x1080 下页面不拥挤、不重叠、不遮挡主内容。
|
||||
- 关键移动端宽度没有明显炸版。
|
||||
- 图片、3D、表格、Inspector、下拉、弹层状态都能正常显示。
|
||||
- 文案不溢出按钮、标签、卡片或表格单元格。
|
||||
- 没有引入一套和平台组件明显割裂的新皮肤。
|
||||
|
||||
收口时必须给规范审查结论:
|
||||
|
||||
```text
|
||||
规范审查:passed / passed with warnings / rejected
|
||||
这是自审,不是独立复审。
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 主要操作使用 `filled` 变体
|
||||
- 次要操作使用 `tinted` 或 `gray` 变体
|
||||
- 确保按钮最小尺寸为 44px(苹果触摸目标标准)
|
||||
- 使用 `loading` 状态而不是禁用按钮
|
||||
|
||||
### Input 输入框
|
||||
|
||||
```typescript
|
||||
import Input from '@/components/ui/Input'
|
||||
|
||||
function InputExample() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 基础输入框 */}
|
||||
<Input
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
helperText="用户名必须唯一"
|
||||
required
|
||||
/>
|
||||
|
||||
{/* 状态变体 */}
|
||||
<Input
|
||||
label="邮箱"
|
||||
type="email"
|
||||
state="success"
|
||||
helperText="邮箱格式正确"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="密码"
|
||||
type="password"
|
||||
errorText="密码长度至少 8 位"
|
||||
/>
|
||||
|
||||
{/* 带图标 */}
|
||||
<Input
|
||||
label="搜索"
|
||||
leftIcon="🔍"
|
||||
rightIcon="✕"
|
||||
onRightIconClick={() => console.log('清除')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 始终提供 `label` 属性以支持可访问性
|
||||
- 使用 `helperText` 提供额外说明
|
||||
- 错误状态使用 `errorText` 而不是 `helperText`
|
||||
- 合理使用图标增强用户体验
|
||||
|
||||
### Card 卡片
|
||||
|
||||
```typescript
|
||||
import Card, { CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/Card'
|
||||
|
||||
function CardExample() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 简单卡片 */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-2">卡片标题</h3>
|
||||
<p className="text-gray-600">卡片内容</p>
|
||||
</Card>
|
||||
|
||||
{/* 组合卡片 */}
|
||||
<Card variant="elevated">
|
||||
<CardHeader>
|
||||
<CardTitle>高级卡片</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>使用组合组件构建的卡片</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button size="small">操作</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* 交互式卡片 */}
|
||||
<Card
|
||||
interactive
|
||||
onClick={() => console.log('卡片点击')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="p-6">
|
||||
<h3>可点击卡片</h3>
|
||||
<p>支持 hover 和 active 动画</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 使用 `variant="elevated"` 创建层次感
|
||||
- 交互式卡片添加 `interactive` 属性
|
||||
- 保持卡片内容简洁,避免过度嵌套
|
||||
|
||||
## 导航组件
|
||||
|
||||
### NavigationBar 导航栏
|
||||
|
||||
```typescript
|
||||
import NavigationBar, { BackButton, MenuButton } from '@/components/ui/NavigationBar'
|
||||
|
||||
function NavigationExample() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 大标题模式 */}
|
||||
<NavigationBar
|
||||
mode="large"
|
||||
title="大标题导航"
|
||||
subtitle="副标题说明"
|
||||
leftContent={<BackButton onBack={() => history.back()} />}
|
||||
rightContent={<MenuButton onMenu={() => console.log('菜单')} />}
|
||||
/>
|
||||
|
||||
{/* 标准模式 */}
|
||||
<NavigationBar
|
||||
mode="standard"
|
||||
title="标准导航"
|
||||
leftContent={<BackButton />}
|
||||
rightContent={
|
||||
<div className="flex gap-2">
|
||||
<Button size="small" variant="plain">编辑</Button>
|
||||
<Button size="small" variant="tinted">保存</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### TabBar 标签栏
|
||||
|
||||
```typescript
|
||||
import TabBar, { useTabBar } from '@/components/ui/TabBar'
|
||||
|
||||
function TabBarExample() {
|
||||
const tabBar = useTabBar('home')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'home', label: '首页', icon: '🏠' },
|
||||
{ id: 'search', label: '搜索', icon: '🔍' },
|
||||
{ id: 'favorites', label: '收藏', icon: '❤️', badge: 3 },
|
||||
{ id: 'profile', label: '我的', icon: '👤' },
|
||||
]
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
items={tabs}
|
||||
activeId={tabBar.activeTab}
|
||||
onTabChange={tabBar.switchTab}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 标签栏最多显示 5 个标签
|
||||
- 使用 `useTabBar` Hook 管理状态
|
||||
- 为重要标签添加徽章提示
|
||||
|
||||
## 数据展示组件
|
||||
|
||||
### Badge 徽章
|
||||
|
||||
```typescript
|
||||
import Badge, { Tag, NumberBadge, StatusDot } from '@/components/ui/Badge'
|
||||
|
||||
function BadgeExample() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 基础徽章 */}
|
||||
<div className="flex gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge type="success">Success</Badge>
|
||||
<Badge type="error">Error</Badge>
|
||||
<Badge type="warning">Warning</Badge>
|
||||
<Badge type="info">Info</Badge>
|
||||
</div>
|
||||
|
||||
{/* 数字徽章 */}
|
||||
<div className="flex gap-2">
|
||||
<NumberBadge count={5} />
|
||||
<NumberBadge count={99} max={99} />
|
||||
<NumberBadge count={999} max={99} />
|
||||
</div>
|
||||
|
||||
{/* 标签 */}
|
||||
<div className="flex gap-2">
|
||||
<Tag closable onClose={() => console.log('关闭')}>
|
||||
可关闭标签
|
||||
</Tag>
|
||||
<Tag icon="🔥" type="success">
|
||||
带图标标签
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* 状态点 */}
|
||||
<div className="flex gap-2">
|
||||
<StatusDot status="online" />
|
||||
<StatusDot status="busy" />
|
||||
<StatusDot status="away" />
|
||||
<StatusDot status="offline" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### ProgressBar 进度条
|
||||
|
||||
```typescript
|
||||
import ProgressBar, { StepProgress } from '@/components/ui/ProgressBar'
|
||||
|
||||
function ProgressExample() {
|
||||
const [progress, setProgress] = useState(45)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 线性进度条 */}
|
||||
<ProgressBar
|
||||
value={progress}
|
||||
showLabel
|
||||
showPercentage
|
||||
label="下载进度"
|
||||
animated
|
||||
/>
|
||||
|
||||
{/* 环形进度条 */}
|
||||
<ProgressBar
|
||||
value={progress}
|
||||
variant="circular"
|
||||
size="large"
|
||||
type="success"
|
||||
showPercentage
|
||||
/>
|
||||
|
||||
{/* 步骤进度 */}
|
||||
<StepProgress
|
||||
current={2}
|
||||
total={4}
|
||||
showLabels
|
||||
labels={['开始', '配置', '确认', '完成']}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 反馈组件
|
||||
|
||||
### Modal 模态框
|
||||
|
||||
```typescript
|
||||
import Modal, { ConfirmModal } from '@/components/ui/Modal'
|
||||
|
||||
function ModalExample() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => setShowModal(true)}>
|
||||
打开模态框
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
open={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
title="模态框标题"
|
||||
description="模态框描述"
|
||||
size="medium"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p>模态框内容</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="filled">确认</Button>
|
||||
<Button variant="gray" onClick={() => setShowModal(false)}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
open={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
title="确认删除"
|
||||
description="此操作无法撤销"
|
||||
onConfirm={() => console.log('确认删除')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Toast 提示
|
||||
|
||||
```typescript
|
||||
import { useToast, ToastContainer } from '@/components/ui/Toast'
|
||||
|
||||
function ToastExample() {
|
||||
const toast = useToast()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={() => toast.success('操作成功')}>
|
||||
成功提示
|
||||
</Button>
|
||||
<Button onClick={() => toast.error('操作失败')}>
|
||||
错误提示
|
||||
</Button>
|
||||
<Button onClick={() => toast.info('新消息', {
|
||||
title: '系统通知',
|
||||
action: {
|
||||
label: '查看',
|
||||
onClick: () => console.log('查看消息')
|
||||
}
|
||||
})}>
|
||||
信息提示
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ToastContainer
|
||||
toasts={toast.toasts}
|
||||
onClose={toast.removeToast}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 动画组件
|
||||
|
||||
### AnimatedEntry 进入动画
|
||||
|
||||
```typescript
|
||||
import AnimatedEntry, { SlideInUp, StaggeredAnimation } from '@/components/ui/AnimatedEntry'
|
||||
|
||||
function AnimationExample() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* 基础进入动画 */}
|
||||
<AnimatedEntry direction="up" duration="normal">
|
||||
<Card className="p-6">
|
||||
<h3>从下方滑入的卡片</h3>
|
||||
</Card>
|
||||
</AnimatedEntry>
|
||||
|
||||
{/* 预配置动画 */}
|
||||
<SlideInUp delay={200}>
|
||||
<div>延迟 200ms 的滑入动画</div>
|
||||
</SlideInUp>
|
||||
|
||||
{/* 交错动画 */}
|
||||
<StaggeredAnimation staggerDelay={100}>
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<Card key={i} className="p-4">
|
||||
<p>卡片 {i}</p>
|
||||
</Card>
|
||||
))}
|
||||
</StaggeredAnimation>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践:**
|
||||
- 使用 `trigger="visible"` 实现滚动触发动画
|
||||
- 交错动画的延迟时间建议 50-150ms
|
||||
- 响应用户的减少动画偏好设置
|
||||
|
||||
## 可访问性工具
|
||||
|
||||
### 用户偏好设置
|
||||
|
||||
```typescript
|
||||
import { PreferencesProvider, AccessibilityPanel, useUserPreferences } from '@/lib/preferences'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PreferencesProvider>
|
||||
<div className="app">
|
||||
<AccessibilityPanel />
|
||||
<MyContent />
|
||||
</div>
|
||||
</PreferencesProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function MyContent() {
|
||||
const { preferences } = useUserPreferences()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>当前字体大小: {preferences.fontSize}%</p>
|
||||
<p>减少动画: {preferences.prefersReducedMotion ? '是' : '否'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 包容性设计
|
||||
|
||||
```typescript
|
||||
import { createInclusiveStatusIndicator } from '@/lib/accessibility'
|
||||
|
||||
function StatusExample() {
|
||||
const successIndicator = createInclusiveStatusIndicator('success', {
|
||||
showIcon: true,
|
||||
showShape: true,
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="status-indicator success shape-circle"
|
||||
style={successIndicator.style}
|
||||
aria-label={successIndicator.ariaLabel}
|
||||
>
|
||||
{successIndicator.icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 虚拟滚动
|
||||
|
||||
```typescript
|
||||
import VirtualList from '@/components/ui/VirtualList'
|
||||
|
||||
function VirtualListExample() {
|
||||
const items = Array.from({ length: 10000 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `项目 ${i + 1}`,
|
||||
description: `描述 ${i + 1}`
|
||||
}))
|
||||
|
||||
return (
|
||||
<VirtualList
|
||||
items={items}
|
||||
itemHeight={60}
|
||||
height={400}
|
||||
renderItem={(item, index) => (
|
||||
<div className="flex items-center p-4 border-b">
|
||||
<div className="flex-1">
|
||||
<h4>{item.name}</h4>
|
||||
<p className="text-sm text-gray-500">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
import { usePerformanceMonitor, useMemoryMonitor } from '@/lib/performance'
|
||||
|
||||
function PerformanceExample() {
|
||||
usePerformanceMonitor('MyComponent')
|
||||
const memoryInfo = useMemoryMonitor()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{memoryInfo && (
|
||||
<p>内存使用: {Math.round(memoryInfo.usedJSHeapSize / 1024 / 1024)}MB</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从传统 CSS 迁移
|
||||
|
||||
```typescript
|
||||
// 旧方式
|
||||
<div className="bg-blue-500 text-white p-4 rounded-lg">
|
||||
内容
|
||||
</div>
|
||||
|
||||
// 新方式
|
||||
<Card variant="filled" className="bg-apple-blue-light text-white">
|
||||
<div className="p-apple-4">
|
||||
内容
|
||||
</div>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### 从其他 UI 库迁移
|
||||
|
||||
```typescript
|
||||
// Material-UI Button
|
||||
<Button variant="contained" color="primary">
|
||||
按钮
|
||||
</Button>
|
||||
|
||||
// Apple UI Button
|
||||
<Button variant="filled">
|
||||
按钮
|
||||
</Button>
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何自定义主题颜色?
|
||||
A: 修改 `tailwind.config.ts` 中的颜色令牌,或在 CSS 中覆盖 CSS 变量。
|
||||
|
||||
### Q: 动画不生效怎么办?
|
||||
A: 检查用户是否启用了减少动画偏好,或者组件是否设置了 `disabled` 属性。
|
||||
|
||||
### Q: 如何确保可访问性?
|
||||
A: 使用语义化 HTML,提供 ARIA 属性,确保键盘导航,测试对比度。
|
||||
|
||||
### Q: 性能优化建议?
|
||||
A: 使用 React.memo,避免不必要的重渲染,大列表使用虚拟滚动。
|
||||
|
||||
## Slider 滑动条(统一元组件)
|
||||
|
||||
`Slider` 元组件位于 `src/components/ui/Slider.tsx`,用于统一替代原生 `input[type="range"]`。
|
||||
|
||||
```tsx
|
||||
import Slider from '@/components/ui/Slider'
|
||||
|
||||
function DemoSlider() {
|
||||
return (
|
||||
<Slider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={0.45}
|
||||
onValueChange={(value) => console.log(value)}
|
||||
aria-label="爆炸程度"
|
||||
className="w-40"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
最佳实践:
|
||||
- 必须使用 `onValueChange` 接收数值,避免在业务层反复做字符串转数字。
|
||||
- 必须补齐 `aria-label`,保证键盘与读屏可访问性。
|
||||
- 禁止新增独立的 range 样式分叉,统一使用 `apple-slider` 视觉规范。
|
||||
|
||||
## 总结
|
||||
|
||||
Apple 风格 UI 设计系统提供了完整的组件库和工具集,遵循苹果 Human Interface Guidelines 2025 标准。通过合理使用这些组件和工具,可以构建出美观、易用、高性能的用户界面。
|
||||
|
||||
记住始终考虑可访问性、性能和用户体验,让每个用户都能享受到优质的交互体验。
|
||||
|
||||
163
furniture-3d-system/docs/数据字典/03-类目配置.md
Normal file
163
furniture-3d-system/docs/数据字典/03-类目配置.md
Normal file
@ -0,0 +1,163 @@
|
||||
# 类目配置取数与筛选逻辑
|
||||
|
||||
> 用途:给需要接入类目配置数据的开发人员使用。本文描述当前代码和数据库的真实口径(2026-09-24 更新:合并为单表展示结构)。
|
||||
>
|
||||
> 重要:本文不包含数据库连接串、账号、密码。连接信息统一见《README.md》。
|
||||
|
||||
## 1. 数据源与责任边界
|
||||
|
||||
- **权威数据源**:本系统 `DATABASE_URL` 指向的 PostgreSQL,schema `public`,共 2 张表(见第 2 节)。数据通过类目配置页面人工维护 + Excel 导入,**没有外部同步来源**。
|
||||
- **业务作用**:
|
||||
1. 维护「二级类目 → 一级类目」的唯一归属(产品全量档案类目列的规范值来源);
|
||||
2. 维护每个类目的 4 档规模阈值(爆款/热卖款/动销款/低动销款 × 业绩区间、日销区间)与「分类」归属。
|
||||
- **人工维护原则**:直接写表会绕过唯一键校验和页面权限,接入方一律只读。
|
||||
|
||||
### 1.1 代码依据
|
||||
|
||||
- Prisma 模型:`prisma/schema.prisma` 中 `SupplyChainProductArchiveCategoryConfig`、`SupplyChainProductArchiveCategoryScaleRow`。
|
||||
- 页面:`src/app/supply-chain/database/category-config/`(单表展示,数据源即规模行表)。
|
||||
- 服务:`supply-chain-product-archive-category-scale-row.service.ts`(页面主数据源,含类目清单同步)、`supply-chain-product-archive-category-config.service.ts`(类目主表维护)。
|
||||
|
||||
## 2. 物理表清单与分工
|
||||
|
||||
| 表名 | 角色 | 当前行数量级 |
|
||||
| --- | --- | --- |
|
||||
| `"SupplyChainProductArchiveCategoryScaleRow"` | **类目规模行(页面当前数据源)**:一行 = 二级类目 × 规模标签,自带一级类目、分类、阈值 | 类目数 × 4 |
|
||||
| `"SupplyChainProductArchiveCategoryConfig"` | **类目主表**:一行一个二级类目的身份记录(唯一性、启停状态),供产品档案同步、类目下拉等消费方使用 | 80+ |
|
||||
|
||||
两表关系:
|
||||
|
||||
```text
|
||||
ScaleRow(页面表:一级/二级/分类/阈值/标签,行=类目×档位,唯一键 categoryL2+scaleLabel)
|
||||
│ 服务写入时自动同步(新增/改行会把一级类目、分类落到主表)
|
||||
▼
|
||||
Config(类目身份表:categoryL2 全表唯一 + status 启停,供外部消费方取类目清单)
|
||||
```
|
||||
|
||||
> 历史说明:旧三表结构中的「分类档位模板表」(`SupplyChainProductArchiveCategoryGroupTier`)已于 2026-09-24 迁移(20260924160000)删除,其数据已按类目展开并入规模行表;旧接口 `/api/supply-chain/category-config/group-tiers/*` 已下线。
|
||||
|
||||
## 3. 表字段字典
|
||||
|
||||
### 3.1 `"SupplyChainProductArchiveCategoryScaleRow"`(类目规模行,页面数据源)
|
||||
|
||||
| 字段 | 类型/约束 | 业务含义 | 取值规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` 主键 | 行 ID | 系统生成 |
|
||||
| `categoryL1` | `TEXT NOT NULL DEFAULT ''` | 一级类目(≤160 字符) | 页面/导入必填 |
|
||||
| `categoryL2` | `TEXT NOT NULL` | 二级类目名(≤160 字符) | 与 `scaleLabel` 联合唯一 |
|
||||
| `categoryGroup` | `TEXT NOT NULL DEFAULT ''` | 分类(如 电脑桌/书桌、其他沙发类) | 空 = 未分类 |
|
||||
| `scaleLabel` | `TEXT NOT NULL` | 规模标签 | 只能是 爆款/热卖款/动销款/低动销款 |
|
||||
| `performanceStart` | `DECIMAL(14,2)` NOT NULL | 业绩起点(元) | |
|
||||
| `performanceEnd` | `DECIMAL(14,2)` 可空 | 业绩终点(元) | **NULL = 不设上限** |
|
||||
| `dailySalesStart` | `DECIMAL(12,2)` NOT NULL | 日销起点(件/天) | 支持小数 |
|
||||
| `dailySalesEnd` | `DECIMAL(12,2)` 可空 | 日销终点(件/天) | NULL = 不设上限 |
|
||||
| `sortOrder` | `INTEGER` | 档位顺序:爆款1 → 热卖款2 → 动销款3 → 低动销款4 | 判定从高档往低档取第一个命中 |
|
||||
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 系统字段 | |
|
||||
|
||||
约束:`UNIQUE(categoryL2, scaleLabel)`——一个类目每个标签只有一行;索引:`categoryL2`、`categoryGroup`。
|
||||
|
||||
### 3.2 `"SupplyChainProductArchiveCategoryConfig"`(类目主表)
|
||||
|
||||
| 字段 | 类型/约束 | 业务含义 | 取值规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` 主键 | 配置记录 ID | 系统生成 |
|
||||
| `categoryL2` | `TEXT` 唯一 | 二级类目名(≤160 字符) | 全表唯一 |
|
||||
| `categoryL1` | `TEXT NOT NULL` | 一级类目(≤160 字符) | |
|
||||
| `categoryGroup` | `TEXT NOT NULL DEFAULT ''` | 分类 | 空 = 未分类 |
|
||||
| `scaleLabel` | `TEXT NOT NULL DEFAULT ''` | 【预留未使用】 | 阈值数据在 ScaleRow 表 |
|
||||
| `performanceStart` / `performanceEnd` | `DECIMAL(14,2)` 可空 | 【预留未使用】 | 同上 |
|
||||
| `dailySalesStart` / `dailySalesEnd` | `DECIMAL(12,2)` 可空 | 【预留未使用】 | 同上 |
|
||||
| `status` | `TEXT`,`ACTIVE` / `INACTIVE` | 启用/停用;停用的不进类目选项 | 默认 `ACTIVE` |
|
||||
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 系统字段 | |
|
||||
|
||||
索引:`categoryL1`、`categoryGroup`、`status`;`categoryL2` 唯一。
|
||||
|
||||
## 4. 其他线程取数方式
|
||||
|
||||
### 4.1 只读 SQL(直连数据库)
|
||||
|
||||
```sql
|
||||
-- 4.1.1 页面同款数据:全部规模行(一级类目、二级类目、分类、4 档阈值)
|
||||
SELECT "categoryL1", "categoryL2", "categoryGroup", "scaleLabel",
|
||||
"performanceStart", "performanceEnd", "dailySalesStart", "dailySalesEnd"
|
||||
FROM "SupplyChainProductArchiveCategoryScaleRow"
|
||||
ORDER BY "categoryL2" ASC, "sortOrder" ASC;
|
||||
|
||||
-- 4.1.2 某个类目的 4 档阈值
|
||||
SELECT "scaleLabel", "performanceStart", "performanceEnd", "dailySalesStart", "dailySalesEnd"
|
||||
FROM "SupplyChainProductArchiveCategoryScaleRow"
|
||||
WHERE "categoryL2" = '住宅家具 - 书桌'
|
||||
ORDER BY "sortOrder" ASC;
|
||||
|
||||
-- 4.1.3 类目身份清单(供下拉/同步用,只取启用的)
|
||||
SELECT "categoryL1", "categoryL2", "categoryGroup"
|
||||
FROM "SupplyChainProductArchiveCategoryConfig"
|
||||
WHERE "status" = 'ACTIVE'
|
||||
ORDER BY "categoryL1" ASC, "categoryL2" ASC;
|
||||
|
||||
-- 4.1.4 某分类下的全部类目及其阈值
|
||||
SELECT "categoryL2", "scaleLabel", "performanceStart", "performanceEnd"
|
||||
FROM "SupplyChainProductArchiveCategoryScaleRow"
|
||||
WHERE "categoryGroup" = '电脑桌/书桌'
|
||||
ORDER BY "categoryL2" ASC, "sortOrder" ASC;
|
||||
```
|
||||
|
||||
注意事项:
|
||||
|
||||
- 表名/字段名**必须带双引号**保持大小写。
|
||||
- 终点列为 `NULL` 表示不设上限,不要写成 0。
|
||||
- 停用(`INACTIVE`)类目历史仍在 Config 表里,统计按 `status` 过滤。
|
||||
- 商品/档案行上的类目与分类是写入时的快照,改配置不回刷历史数据。
|
||||
|
||||
### 4.2 代码内取数(同仓库服务)
|
||||
|
||||
```ts
|
||||
// 规模行(页面同款数据:一行=类目×档位,含一级类目与分类)
|
||||
import { supplyChainProductArchiveCategoryScaleRowService } from
|
||||
'@/server/services/supply-chain-product-archive-category-scale-row.service'
|
||||
await supplyChainProductArchiveCategoryScaleRowService.list({ page: 1, pageSize: 100 })
|
||||
await supplyChainProductArchiveCategoryScaleRowService.listAll({ categoryGroup: '电脑桌/书桌' })
|
||||
|
||||
// 类目身份清单(产品全量档案同步、类目下拉在用)
|
||||
import { supplyChainProductArchiveCategoryConfigService } from
|
||||
'@/server/services/supply-chain-product-archive-category-config.service'
|
||||
await supplyChainProductArchiveCategoryConfigService.list({ page: 1, pageSize: 100, status: 'ACTIVE' })
|
||||
|
||||
import { supplyChainProductArchiveCategoryConfigRepository } from
|
||||
'@/server/repositories/supply-chain-product-archive-category-config.repository'
|
||||
await supplyChainProductArchiveCategoryConfigRepository.findActiveByCategoryL2('住宅家具 - 书桌')
|
||||
```
|
||||
|
||||
### 4.3 走 API(需要登录 + 供应链权限)
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `/api/supply-chain/category-config/scale-rows` | GET / POST | 规模行列表 / 新增(写入时自动同步类目主表) |
|
||||
| `/api/supply-chain/category-config/scale-rows/{id}` | PATCH / DELETE | 规模行编辑 / 删除 |
|
||||
| `/api/supply-chain/category-config/scale-rows/export` · `/import` | GET / POST | 导出(筛选跟随当前搜索/分类/标签) / 流式导入(表头:一级类目、二级类目、分类、规模标签、业绩起终点、日销起终点) |
|
||||
| `/api/supply-chain/category-config/scale-rows/groups` | GET | 去重分类列表(筛选项) |
|
||||
| `/api/supply-chain/category-config` · `/{id}` | GET/POST/PATCH | 类目身份维护(供应商新品推荐下拉也走这里) |
|
||||
| `/api/supply-chain/category-config/export` · `/import` | GET / POST | 类目身份导出 / 导入 |
|
||||
|
||||
权限:查询走类目节点「查看」,导出走「下载」,写入走「编辑」;角色 `SUPER_ADMIN/ADMIN/SUPERVISOR` 或业务角色 `MANAGEMENT`。
|
||||
|
||||
## 5. 现有消费方(改动前必读)
|
||||
|
||||
| 消费方 | 取数点 | 依赖 |
|
||||
| --- | --- | --- |
|
||||
| 产品全量档案(DF/聚水潭同步) | `jst-sku-product-archive-sync.service.ts`、`jst-sku-product-archive-full-sync.service.ts` | `findActiveByCategoryL2`:仅启用类目作为规范值 |
|
||||
| 供应商新品推荐类目下拉 | `/api/supply-chain/supplier-recommended-products/category-options` | Config `service.list`,仅 `ACTIVE` |
|
||||
| 类目配置页面 | 第 4.3 节接口 | — |
|
||||
|
||||
改 `status`、`categoryL2` 名称、删除类目前先确认不影响以上消费方。
|
||||
|
||||
## 6. 接入前检查清单
|
||||
|
||||
- [ ] 表名/字段名带双引号保持大小写。
|
||||
- [ ] 方向:`categoryL2`(二级)→ `categoryL1`(一级);`categoryGroup`(分类)是独立维度,不是一级类目,空 = 未分类。
|
||||
- [ ] 终点列 `NULL` = 不设上限,不要当 0 处理。
|
||||
- [ ] 档位判定顺序:爆款 → 热卖款 → 动销款 → 低动销款,从高到低取第一个命中区间。
|
||||
- [ ] 阈值数据取 `ScaleRow` 表;Config 表上的 `scaleLabel`/阈值列为预留未使用。
|
||||
- [ ] 停用(`INACTIVE`)配置历史仍在 Config 表里,统计按 `status` 过滤。
|
||||
- [ ] 商品/档案行上的类目、分类是写入时快照,改配置不回刷历史数据。
|
||||
- [ ] 页面写操作会双写两表(规模行 + 类目主表同步),外部只读不写就不会产生不一致。
|
||||
99
furniture-3d-system/docs/数据字典/13-内容视频流程看板.md
Normal file
99
furniture-3d-system/docs/数据字典/13-内容视频流程看板.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 内容视频流程看板取数与筛选逻辑
|
||||
|
||||
> 用途:给需要接入「新品-视频进度(内容部)」(钉钉内容视频流程)数据的开发人员使用。本文描述当前代码和数据库的真实口径(2026-10-08 首版)。
|
||||
>
|
||||
> 重要:本文不包含数据库连接串、账号、密码。该页面数据**不存在本系统 PostgreSQL**,全部来自企划 MySQL 的同步快照。
|
||||
|
||||
## 1. 数据源与责任边界
|
||||
|
||||
- **页面直接数据源**:企划 MySQL(连接变量 `JST_PLANNING_DB_*`,生产为生产服务器本机实例)的 `planning_database` 库,快照表 `oa_video_flow_board_snapshot`。
|
||||
- **原始数据源**:钉钉 OA 审批镜像库的 `ding_Workflow` / `ding_WorkflowScheme` / `ding_WorkflowTask` 三张表,按流程编码过滤:
|
||||
- 内容视频流程 `PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673`(登记在 `oa-board.constants.ts` 的 `OA_BOARD_PROCESS_CODES.video`)。
|
||||
- **同步方式**:`scripts/sync-oa-video-flow-board.ts --apply`(即 `npm run sync:oa-video-flow:apply`,服务 `oa-video-flow-board.service.ts`)全量拉取该流程全部实例与表单字段,按"一条流程实例 = 一行"写入快照表,并用 `sync_version` 清理已消失的实例。生产由 systemd 定时器(`install-oa-board-sync-timer.sh` 安装的 `furniture-oa-board-sync.timer`)每 15 分钟在主看板、品牌策划看板之后顺序执行。
|
||||
- **责任边界**:镜像库与钉钉侧的数据质量不由本系统负责;本系统保证"镜像库 → 快照 → 页面"这一段。下游一律**只读快照表**,不要直连 ding_ 三张源表(源表结构随钉钉变化,无稳定性承诺)。
|
||||
- **与新品主链的关系**:内容视频流程**不参与** OA 新品看板主链的项目聚合、总状态与时效矩阵(其表单无开款编号/关联单号,仅有 SPU)。本表是独立流程维度的进度数据。
|
||||
|
||||
### 1.1 代码依据
|
||||
|
||||
- 同步与快照读写的唯一实现:`src/server/services/oa-video-flow-board.service.ts`(建表 DDL 在 `ensureSnapshotTable`,自动建表)。
|
||||
- 源库连接复用:`oa-board-sync.service.ts` 导出的 `openOaBoardSourceMysqlConnection` / `openOaBoardSourceSqlServer`(跟随 `OA_BOARD_SOURCE_DRIVER`)。
|
||||
- 页面接口:`GET /api/intake/oa-board/video-flow`(参数 `keyword` / `demandType` / `brand` / `status` / `page` / `pageSize`,需登录态)。
|
||||
- 页面组件:`src/app/intake/oa-board/OaBoardVideoFlowPanel.tsx`(Tab 参数 `tab=video-flow`)。
|
||||
|
||||
## 2. 物理表(MySQL · planning_database)
|
||||
|
||||
| 物理表 | 用途 | 键与索引 |
|
||||
| --- | --- | --- |
|
||||
| `oa_video_flow_board_snapshot` | 最新快照,一行 = 一条内容视频流程实例 | `row_key` 唯一;`spu` / `status` / `stage_label` / `demand_type` / `brand` / `created_date` 有索引 |
|
||||
|
||||
同步任务自动建表(`CREATE TABLE IF NOT EXISTS`),引擎 InnoDB,字符集 utf8mb4。
|
||||
|
||||
## 3. 字段字典
|
||||
|
||||
| 字段 | 类型 | 含义 | 取值规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| `row_key` | `VARCHAR(191)` 唯一 | 钉钉流程实例 ID(processInstanceId),幂等拉取键 | 源表主键,稳定不变 |
|
||||
| `business_id` | `VARCHAR(128)` | 流程发起编号 | 形如 `202610071758000088167` |
|
||||
| `title` | `VARCHAR(255)` | 流程标题 | `XX提交的内容视频和图片申请流程` |
|
||||
| `submitter_name` | `VARCHAR(255)` | 提交人姓名 | 从标题提取;`originator_user_id` 存钉钉用户 ID 兜底 |
|
||||
| `status` | `VARCHAR(32)` | 流程状态 | `RUNNING` / `COMPLETED` / `TERMINATED` |
|
||||
| `demand_department` | `VARCHAR(255)` | 需求部门 | 表单字段「需求部门」 |
|
||||
| `demand_type` | `VARCHAR(255)` | 需求类型 | 如 淘系-逛逛视频 / 抖音-千川素材视频 / 安装视频 / AI视频 等 13 种 |
|
||||
| `task_type` | `VARCHAR(255)` | 任务类型 | 如 布景拍摄 / 二次剪辑 |
|
||||
| `install_video_category` | `VARCHAR(255)` | 安装视频分类 | 仅安装视频类需求有值(如 B档) |
|
||||
| `brand` | `VARCHAR(255)` | 品牌 | 表单字段「品牌」 |
|
||||
| `scene_category` | `VARCHAR(255)` | 场景类别 | 如 卧室 / 客厅 / 玄关 |
|
||||
| `spu` | `VARCHAR(64)` | SPU | 大写规范化;约 3% 行无值(账号维护等非产品需求) |
|
||||
| `product_id` | `VARCHAR(128)` | 商品ID | 表单字段「商品ID」,可能为"暂无" |
|
||||
| `sample_barcodes` | `TEXT` | 样品条码 | 表单「样品明细」表格中商品编码列,多值分号分隔 |
|
||||
| `content_owner` | `VARCHAR(255)` | 内容部负责人 | 表单字段 |
|
||||
| `planning_supervisor` | `VARCHAR(255)` | 品牌策划主管 | 表单字段 |
|
||||
| `script_director` | `VARCHAR(255)` | 编导 | 流程推进到该环节时才填,未填为 NULL |
|
||||
| `cameraman` | `VARCHAR(255)` | 摄像 | 同上 |
|
||||
| `editor` | `VARCHAR(255)` | 剪辑 | 同上 |
|
||||
| `retoucher` | `VARCHAR(255)` | 修图 | 可选环节 |
|
||||
| `cover` | `VARCHAR(255)` | 封面 | 同上 |
|
||||
| `stage_label` | `VARCHAR(64)` | 当前阶段 | 推断规则见第 4 节 |
|
||||
| `acceptance_result` | `VARCHAR(255)` | 验收结果 | 表单「运营/策划/客服验收结果」 |
|
||||
| `score` | `VARCHAR(64)` | 评分 | 表单「评分」 |
|
||||
| `created_date` | `DATETIME` | 流程发起时间(北京时间) | 源表 createAt |
|
||||
| `finished_date` | `DATETIME` | 流程完成时间 | RUNNING 为 NULL |
|
||||
| `duration_days` | `DECIMAL(10,2)` | 总时效(天) | 发起 → 完成;未完成计至同步时刻 |
|
||||
| `last_source_updated_at` | `DATETIME` | 源表最近更新时间 | 增量拉取判据 |
|
||||
| `sync_version` | `VARCHAR(64)` | 本次同步批次标识 | 全表同批;用于清理已消失实例 |
|
||||
| `synced_at` | `TIMESTAMP` | 本行最近同步时间 | 数据新鲜度判据(`MAX(synced_at)`) |
|
||||
|
||||
## 4. 阶段(stage_label)推断规则
|
||||
|
||||
源表只有 activityId 没有节点名,首版按表单字段的推进顺序推断(钉钉流程设计中角色字段按环节顺序逐步填写):
|
||||
|
||||
```text
|
||||
status = COMPLETED → 已完结
|
||||
status = TERMINATED → 已终止
|
||||
验收结果或评分已填 → 验收/评分
|
||||
否则取已填角色的最后一个:
|
||||
编导 → 编导制作 → 摄像 → 摄影拍摄 → 剪辑 → 剪辑制作
|
||||
→ 修图/封面 → 封面/修图制作
|
||||
一个角色都没填 → 需求发起
|
||||
```
|
||||
|
||||
若后续拿到钉钉流程节点名清单,可升级为 activityId 级精确映射(预留位:`oa-board.constants.ts` 的 `OA_BOARD_ACTIVITY_MAPPING_SEED.video` 当前为空数组)。
|
||||
|
||||
## 5. 接入示例(一律只读)
|
||||
|
||||
```sql
|
||||
-- 按 SPU 拉该产品的全部视频流程
|
||||
SELECT * FROM oa_video_flow_board_snapshot WHERE spu = 'SJ0159';
|
||||
|
||||
-- 只看进行中的
|
||||
SELECT * FROM oa_video_flow_board_snapshot WHERE status = 'RUNNING';
|
||||
|
||||
-- 增量拉取(幂等键 row_key)
|
||||
SELECT * FROM oa_video_flow_board_snapshot
|
||||
WHERE last_source_updated_at > :上次拉取时间;
|
||||
|
||||
-- 取数前先看新鲜度(生产最多滞后 15 分钟)
|
||||
SELECT MAX(synced_at) FROM oa_video_flow_board_snapshot;
|
||||
```
|
||||
|
||||
关联口径:与产品数据对齐用 `spu` 或 `sample_barcodes` 中的条码;本表**没有开款编号**,不要与主看板的 `OPENING:` 维度直接关联。
|
||||
188
furniture-3d-system/docs/架构设计/BOM成本预估v2-agent方案设计-20260916.md
Normal file
188
furniture-3d-system/docs/架构设计/BOM成本预估v2-agent方案设计-20260916.md
Normal file
@ -0,0 +1,188 @@
|
||||
# BOM 成本预估 v2(agent 方案)设计文档
|
||||
|
||||
> 日期:2026-09-16
|
||||
> 状态:设计稿,待评审
|
||||
> 前置阅读:`docs/项目总览.md`、`src/server/services/bom-costing/agent/`(已建成的引擎)
|
||||
> 关联会话结论:1688 以图搜图锚定为过渡方案(双重失败已实测证伪),终局为 agent 估算 + 公司数据
|
||||
|
||||
## 1. 一句话定位
|
||||
|
||||
**粘贴竞品链接 → 几分钟后得到一张诚实的成本预估卡。**
|
||||
|
||||
原始需求从未变过。v2 的一切设计决策都回到这句话:数字 + 构成故事 + 诚实的区间,工序全部退到幕后。
|
||||
|
||||
## 2. v1 的教训(为什么 v2 长这样)
|
||||
|
||||
| v1 的问题 | 根因 | v2 的对策 |
|
||||
|---|---|---|
|
||||
| 工序暴露给用户(逐图泳道、五阶段时间线、重算模式、复核流) | 工程美德当成了产品 | 一根进度条 + 一张卡;审计链存在但默认隐藏 |
|
||||
| 27 个 SKU 全量核算(用户等十几分钟才见结果) | 首结果时间没有被设计 | 首批 3 个精算先出结果,其余按需点算,结果缓存 |
|
||||
| 十几分钟等待 | 按 SKU 串行/并发全量跑 | case 级一次运行,2-3 分钟 |
|
||||
| 复杂度随精度承诺膨胀 | 用流程堆精度 | 用数据+校准逼近精度(±10% 是"尽量"的校准目标) |
|
||||
| 1688 锚定顶替估算 | 上线压力下的权宜 | 1688 永远是后台交叉验证信号 |
|
||||
|
||||
**必须保留的教训**:用户会锚定数字。区间 + 置信度 + 不确定点是答案的一部分,不是脚注——这是"给一个数"和"给一个诚实的数"的差别。
|
||||
|
||||
## 3. 设计总纲(五条,裁决一切分歧)
|
||||
|
||||
1. **预估,不是核算**:输出是带区间与置信度的假设生成器,供决策筛选,永不承诺报价级精度。
|
||||
2. **agent 主导分析,代码承载确定性**:看什么图、怎么拆结构、查什么价格由 agent 决定;一切数值计算(汇总、区间合成、损耗)由确定性代码完成。模型永不算数、永不编价。
|
||||
3. **价格只认公司数据**:历史 BOM 镜像是唯一定价依据(检索→分类中位→cohort→损耗四级回退);1688 行情仅作后台交叉验证,永不成为展示价。
|
||||
4. **前端来源中性(不可触碰)**:界面永不出现 1688 字样/报价/链接;一切序列化出口经过 `src/lib/bom-costing/public-estimate-text.ts`。
|
||||
5. **实测闸门**:agent 流程未经回测与真实案例对比验证(偏差分布达标)前,不替换任何线上行为。
|
||||
|
||||
## 4. 用户形态
|
||||
|
||||
### 4.1 默认入口:预估卡 + 全量 SKU 按需核算
|
||||
|
||||
**覆盖率是需求,等待时间是体验问题**——所有 SKU 都要能出结果,但采用"先出一批、其余按需点算"的渐进交付:
|
||||
|
||||
```
|
||||
粘贴链接 → [进度条:抓取中 → 分析中 → 出预估](2-3 分钟)→
|
||||
|
||||
┌────────────────────────────────────────┐
|
||||
│ 源氏木语 白蜡木实木衣柜 │
|
||||
│ 预计出厂成本 ¥260 – 380(首批 3 个 SKU)│
|
||||
│ 置信度:中等 │
|
||||
│ 板材 ¥180± · 五金 ¥25± · 加工+人工 ¥60± · 包材 ¥15± │
|
||||
│ 依据:8 张商品图 + 公司历史 BOM 12 条 │
|
||||
│ 不确定:板厚未确认;铰链数量按两门推定 │
|
||||
└────────────────────────────────────────┘
|
||||
SKU 列表(27 个):
|
||||
[¥260.29 已核算] (3) 原木色 0.9米两门 ← 首批自动精算
|
||||
[¥375.48 已核算] (7) 原木色 1.0米 ← 首批自动精算
|
||||
[¥406.77 已核算] (18) 原木色 1.96米 ← 首批自动精算
|
||||
[点击核算] (4) 原木色 0.9米/高2.2米 ← 用户点击后单独运行
|
||||
[点击核算] ... ← 已算结果缓存,不重跑
|
||||
────────────────────────────────────────
|
||||
商品图片(保留现有形式:主图/SKU图/详情图 分类浏览 + 大图预览)
|
||||
```
|
||||
|
||||
- **首批自动精算**:默认 3 个代表性 SKU(跨度原则:最小/最大/中间,或按材质/结构差异选),2-3 分钟出卡——用户立刻有结果。
|
||||
- **其余 SKU 按需点算**:未核算的 SKU 显示占位按钮,用户点击触发单 SKU agent 运行(复用现有 per-SKU runner,1-2 分钟/个);**结果持久缓存**,重复点击不重跑。
|
||||
- 进度状态只有三个:抓取中 / 分析中 / 出预估(含失败态与重试按钮)。
|
||||
- **渐进展开**:点构成 → BOM 行明细;点行 → 证据(哪张图、哪条镜像参考行)。审计链存在,默认折叠。
|
||||
- **商品图片保留现有展示形式**(主图/SKU 图/详情图分类 + 逐图状态 + 大图预览):用户需要直接查看竞品链接内容,图片是判断依据,不是工程噪声。
|
||||
|
||||
### 4.2 深度模式(保留现有工作台)
|
||||
|
||||
需要全量 27 SKU 拆解、人工核对、证据溯源的用户,从预估卡一键进入现有工作台。v2 验证前工作台不动;验证后它降级为二级入口,不再是默认。
|
||||
|
||||
## 5. 架构:case 级 agent 运行
|
||||
|
||||
### 5.1 运行编排(新,核心增量)
|
||||
|
||||
交付模型:**首批自动精算 + 其余按需点算**。现有 per-SKU runner 是按需点算的执行体,case 级编排负责首批选择与结果汇总。
|
||||
|
||||
```
|
||||
阶段一:runBomCostingAgentEstimateForCase(caseId)(首批)
|
||||
1. 取案例简报(标题/渠道/已解析品类 skill)+ 归档素材 + SKU 清单(含尺寸)
|
||||
2. 代码侧选首批 SKU(默认 3 个:最小/最大/中间,或按材质/结构差异;
|
||||
单 SKU 商品即为它自己)
|
||||
3. 逐个运行现有 per-SKU agent 循环(结构行 + 查价)
|
||||
4. 代码侧汇总出 EstimateCard:卡面五行/区间/置信度 + SKU 列表状态
|
||||
(已核算 3 / 待核算 24)
|
||||
|
||||
阶段二:按需点算(用户驱动)
|
||||
用户点击未核算 SKU → 单 SKU agent 运行(现有 runner)→
|
||||
结果持久缓存 → 卡面/SKU 列表实时更新
|
||||
```
|
||||
|
||||
- 首批预算:≤24 轮 / ≤32 次模型调用 / ≤8 分钟(per-SKU 预算 × 3 + 编排开销)。
|
||||
- **结果缓存**:per-SKU 结果按 (caseId, skuId, 引擎版本) 持久化;点击已核算 SKU 直接展示缓存,不重跑。SKU 重算 = 显式刷新动作(换引擎版本或用户主动重跑)。
|
||||
- 失败语义:首批中单个 SKU 失败 → 该 SKU 标记失败可单独重试,不拖垮整卡;结构成功但价格饥饿 → 出结果但区间放宽 + 不确定点说明(PARTIAL 是合法产物)。
|
||||
- 并发上限与幂等:同一 SKU 的点算请求可去重合并(进行中重复点击返回同一运行)。
|
||||
|
||||
### 5.2 输出契约:EstimateCard(case 级)
|
||||
|
||||
```ts
|
||||
interface BomEstimateCard {
|
||||
caseId: string
|
||||
title: string
|
||||
skus: Array<{ // 全量 SKU 清单 + 状态
|
||||
externalSkuId: string
|
||||
name: string
|
||||
status: 'ESTIMATED' | 'PENDING' | 'FAILED' | 'RUNNING'
|
||||
estimate?: BomCostingAgentEstimate // 已核算时:行/区间/置信度/evidence
|
||||
cachedAt?: string
|
||||
}>
|
||||
firstBatchSkuIds: string[] // 首批自动精算的 SKU
|
||||
breakdown: Array<{ // 卡面五行(由已核算 SKU 的行汇总;随点算增长)
|
||||
category: 'BOARD'|'STEEL'|'ACCESSORY'|'PACKAGING'|'PROCESSING'|'LABOR'
|
||||
total: number | null // null = 该组无价,卡面显示"待补"
|
||||
}>
|
||||
totalRange: { min: number; max: number } // 当前已核算 SKU 的区间包络
|
||||
confidence: number // 已核算 SKU 的最小置信度
|
||||
openQuestions: string[] // 卡面"不确定"(已核算 SKU 的并集)
|
||||
usage: { rounds; modelCalls; elapsedMs }
|
||||
}
|
||||
```
|
||||
|
||||
原则:明细(行级)保留在各 SKU 的 estimate 里供展开;卡面只消费 breakdown/totalRange/confidence/openQuestions,并随按需点算增量更新。
|
||||
|
||||
### 5.3 价格供给(核心依赖,最大缺口)
|
||||
|
||||
首测"定价饥饿"的根治项——把 v1 场景服务的回退链包装成 agent 工具:
|
||||
|
||||
| 优先级 | 工具 | 包装自 |
|
||||
|---|---|---|
|
||||
| P0 | 尺寸感知镜像检索(分类/材质词/尺寸/参考 SKU 多级过滤) | `findReferenceCandidates` |
|
||||
| P0 | 分类画像中位价 | `fillCategoryProfileFallbacks` |
|
||||
| P1 | 同类 cohort 报价 | `resolveCohortQuote` |
|
||||
| P1 | 历史损耗率 | `applyHistoricalLossFactorFallbacks` |
|
||||
| P2 | AI 补估(置信度上限 0.75 + 标记复核) | 现有 estimator |
|
||||
|
||||
工具输出纪律:摘要级(无供应商/店铺/链接),带 sourceRowId 供 evidence 引用,单位明确。**1688 工具维持现状**(只透出状态/参考价/区间/可比数四个中性字段)。
|
||||
|
||||
### 5.4 数据与知识资产接入(全部保留,形态不变)
|
||||
|
||||
| 资产 | v2 中的角色 | 接入形态 |
|
||||
|---|---|---|
|
||||
| 历史 BOM 镜像(每日 MSSQL 同步) | 唯一价格依据 | 工具(5.3) |
|
||||
| 品类 Skill(5 个)+ 结构先验 | agent 脑中的专家清单:必备组件、证据纪律、校验规则 | 提示词注入(`renderBomCostingAgentSkillKnowledge`,已建成) |
|
||||
| 材质目录 | 材质身份唯一权威 | 工具(已接线) |
|
||||
| 商品抓取/归档/抠图 | 看图取证的素材 | 工具 + mission 组装(已接线;抠图模型待切 remove-background) |
|
||||
| 1688 行情 | 后台交叉验证信号 | 工具(已接线,中性摘要) |
|
||||
|
||||
### 5.5 校准闭环(±10% "尽量"的实现方式)
|
||||
|
||||
- **回测**:历史 BOM 镜像抽 20-50 个真实 SKU,同一规则/agent 重算 vs 真实成本,产出误差分布(复用 `backtest:bom-cost-reference` 基建)。
|
||||
- **shadow 对比**:agent 结果与现行引擎结果对同一案例并排落库(工程域),持续记录偏差(`report:bom-cost-shadow-divergence` 同款模式)。
|
||||
- **阈值语义**:±10% 是回测循环追的校准目标(按品类分段看分布),不是单案例门禁。偏差大 → 调工具/知识/提示词 → 再回测。
|
||||
- 未过门槛 → agent 流程保持旁路,线上仍是现行方案。
|
||||
|
||||
## 6. 前端形态
|
||||
|
||||
- **新入口**:`/bom-costing` 默认视图替换为预估卡流(粘贴框 → 卡 → SKU 列表 → 图片浏览)。
|
||||
- **商品图片**:保留现有展示形式(主图/SKU 图/详情图分类 tab + 大图预览 + 逐图状态),用户需要直接查看竞品内容;它从"处理进度件"回归"内容主体"。
|
||||
- **SKU 列表**:已核算显示价格,未核算显示"点击核算"按钮(运行中显示进度态);点击结果写缓存并即时更新卡面。
|
||||
- **组件**:新 `BomEstimateCard`(平台组件体系 + 白曜石规范),状态只有 加载/成功/失败(可重试)。
|
||||
- **现有工作台**:不动,作为深度模式从卡进入。
|
||||
- **移动端**:卡、SKU 列表与图片浏览响应式。
|
||||
- 红线复检:卡面文案经过 `publicEstimateWarningText` 清洗;evidence 展开层同样不得出现来源敏感词。
|
||||
|
||||
## 7. 落地顺序(每步可独立验证)
|
||||
|
||||
| 步骤 | 内容 | 验收 |
|
||||
|---|---|---|
|
||||
| ① | 价格回退工具(P0 两个) | 对已知案例重跑,未定价行显著下降 |
|
||||
| ② | 首批编排 + EstimateCard 契约 + 按需点算与结果缓存 | 单案例 2-8 分钟出首批卡;点击未核算 SKU 增量出结果且缓存生效 |
|
||||
| ③ | 预估卡前端(含 SKU 列表与现有图片浏览)+ 三态进度 | 真人走通 粘贴→出卡→点算其他 SKU→看图核对 全流程 |
|
||||
| ④ | 回测 + shadow 接入,按品类出基线报告 | 误差分布报告(对照 ±10% 目标) |
|
||||
| ⑤ | 实测样本扩到 5-10 案例,按品类定门槛 | 达标 → 翻转默认入口;未达标 → 继续迭代 |
|
||||
|
||||
## 8. 红线(重申)
|
||||
|
||||
1. 前端永不出现 1688 相关内容;一切文本出口过脱敏契约。
|
||||
2. agent 引擎在过实测闸门前保持旁路(不替换线上行为)。
|
||||
3. 区间 + 置信度 + 不确定点是答案的一部分,任何展示形态不得省略。
|
||||
4. 单价只来自工具结果;总额只由代码汇总。
|
||||
5. 现有工作台在 v2 过门槛前不删不改语义。
|
||||
|
||||
## 9. 当前资产清单(截至本文日期)
|
||||
|
||||
已建成:agent 契约/工具层/循环/网关适配/skill 知识渲染/per-SKU runner(27 测试)、镜像与材质只读仓储、1688 中性摘要工具、板材开料工具、shadow 报表、trial 试跑脚本、生产真实案例(源氏木语 27 SKU)对照数据。
|
||||
|
||||
待建:价格回退工具(5.3 表)、case 级编排、EstimateCard 契约与前端、回测接入。
|
||||
|
||||
外部依赖:1688 新 key(当前过期,影响搜图与 A/B 实测,不影响 ①② 开发)。
|
||||
43
furniture-3d-system/docs/架构设计/BOM成本预估产品口径.md
Normal file
43
furniture-3d-system/docs/架构设计/BOM成本预估产品口径.md
Normal file
@ -0,0 +1,43 @@
|
||||
# BOM 成本预估产品口径
|
||||
|
||||
本文是 BOM 成本功能的产品口径裁决入口。代码、界面、测试或历史文档与本文冲突时,应先按本文区分“成本预估”与旧链路内部一致性检查,再决定是否修改实现。
|
||||
|
||||
## 产品承诺
|
||||
|
||||
- 本功能提供带区间、置信度和待确认项的成本预估,不提供报价级精确核算。
|
||||
- 预估结果允许与实际采购价、旧规则结果或其他参考信号存在明显差异;差异本身不构成失败。
|
||||
- 缺少依据的单价可以为空,未解决行应进入待确认项,不得为了提高覆盖率编造价格。
|
||||
- Agent 决定查看什么资料、调用什么工具以及引用什么证据;总额、损耗、开料、分位数和行小计由确定性代码计算。
|
||||
- 正式 Agent 引擎以公司历史 BOM 镜像、材质目录和损耗经验为主要价格依据;临时交付阶段按下节的整件九折方案展示单一预估结果。
|
||||
|
||||
## 临时整件九折方案
|
||||
|
||||
- 正式 Agent 引擎完善前,相似度合格的实时同款报价取中位数并固定乘以 `0.90`,作为前端唯一整件预估价。
|
||||
- 没有可用同款、图片不可用或服务失败时,沿用结构预估,页面仍只显示一套结果。
|
||||
- 原始报价、候选商品、九折过程和原结构价只保留在后台审计字段;普通接口和前端不得出现来源名称、商品链接、供应商或采购系数。
|
||||
- 临时整件价生效时,前端不得同时显示结构分类金额、行小计、贡献占比或旧规则价格;BOM 部件、材质、尺寸和数量仍可在详情中查看。
|
||||
- 正式 Agent 引擎通过产品验收后,在保持同一前端单结果契约的前提下替换临时价格来源。
|
||||
|
||||
## 验收口径
|
||||
|
||||
Agent 旁路试验主要验证:
|
||||
|
||||
1. 能否稳定完成工具调用并返回符合契约的结果。
|
||||
2. 区间、置信度、证据和待确认项是否完整且容易理解。
|
||||
3. 单价是否来自工具结果,总额是否由代码汇总。
|
||||
4. 未定价行是否明确保留,而不是被虚构金额掩盖。
|
||||
5. 前端是否保持来源中性,不泄露供应商、店铺、商品链接或实时市场来源。
|
||||
|
||||
Agent 与旧引擎、实际采购价或市场信号的差异仅作为观察数据,不设全局固定百分比通过门槛。按品类统计差异可以帮助理解结果分布,但不能自动判定预估失败。
|
||||
|
||||
## 旧 10% 门槛的边界
|
||||
|
||||
`backtest:bom-cost-reference` 中的 10% 只用于验证同一历史参考镜像内“已存总额”和“逐行确定性重建”是否一致,属于数据与重建逻辑检查。它不评价页面成本预估或 Agent 预估准确度,不得显示为产品目标,也不得复用于 Agent 灰度验收。
|
||||
|
||||
数据库中的 `targetErrorPercent` 暂时保留用于旧数据和接口兼容;新界面不得展示为成本预估承诺。后续清理旧模型时再通过正式 migration 移除。
|
||||
|
||||
## 术语
|
||||
|
||||
- 用户可见名称统一使用“BOM 成本预估”“预估结果”“重新预估”。
|
||||
- `bom-costing` 等技术目录、路由和历史模型名暂时保留,避免为改名引入无关迁移。
|
||||
- “核算”仅可用于描述已经确认的历史采购/生产成本,不能描述本功能输出。
|
||||
310
furniture-3d-system/docs/架构设计/OA看板-钉钉待办接入方案-20260729.md
Normal file
310
furniture-3d-system/docs/架构设计/OA看板-钉钉待办接入方案-20260729.md
Normal file
@ -0,0 +1,310 @@
|
||||
# OA 看板钉钉待办接入方案
|
||||
|
||||
## 结论
|
||||
|
||||
正式系统**不能只靠新增环境变量**就直接发送钉钉待办。
|
||||
|
||||
原因:
|
||||
|
||||
1. 仓库当前只实现了钉钉登录与用户识别,没有“创建 / 更新 / 删除钉钉待办”的服务代码。
|
||||
2. 当前没有“业务流程记录 <-> 钉钉待办 ID”的持久化表。
|
||||
3. 当前没有发送幂等、失败重试、关闭待办、升级提醒等基础能力。
|
||||
|
||||
但从现有代码基础看,**补一层服务实现后,再由正式环境配置环境变量即可上线**。
|
||||
|
||||
## 现有基础
|
||||
|
||||
### 已具备
|
||||
|
||||
1. 钉钉 OAuth 登录能力
|
||||
- 文件:`src/server/services/dingtalk-auth.service.ts`
|
||||
- 已能完成:
|
||||
- 构建授权地址
|
||||
- 获取用户 access token
|
||||
- 获取 unionId
|
||||
- 获取 app access token
|
||||
- 用 unionId 换取 userId
|
||||
- 获取用户详情
|
||||
|
||||
2. 用户钉钉身份字段
|
||||
- 系统已保存并使用:
|
||||
- `dingtalkUserId`
|
||||
- `dingtalkUnionId`
|
||||
|
||||
3. OA 看板责任人映射
|
||||
- OA 看板已经能把不同阶段映射到默认责任角色 / 人员字段。
|
||||
- 例如首单下单待发起时可兜底为 `采购/跟单`。
|
||||
|
||||
4. 责任与跟进界面基础
|
||||
- 页面已有“责任与跟进”区域,适合作为钉钉待办的业务入口。
|
||||
|
||||
### 尚缺失
|
||||
|
||||
1. 钉钉待办发送 service
|
||||
2. 待办投递日志表
|
||||
3. 待办幂等规则
|
||||
4. 待办关闭 / 更新规则
|
||||
5. 定时升级提醒任务
|
||||
6. 正式环境待办配置项
|
||||
|
||||
## 官方能力判断
|
||||
|
||||
根据钉钉开放平台官方文档,待办能力当前支持:
|
||||
|
||||
1. 创建工作待办
|
||||
2. 更新待办
|
||||
3. 删除待办
|
||||
4. 查询用户待办
|
||||
|
||||
与本项目相关的关键点:
|
||||
|
||||
1. 创建待办时需要应用身份和用户身份,不能只知道姓名。
|
||||
2. 待办通常需要可回跳的 `detailUrl`,跳回本系统详情页或看板页。
|
||||
3. 如果要做闭环,流程状态变更后还要同步更新或关闭钉钉侧待办。
|
||||
|
||||
官方文档参考:
|
||||
|
||||
- [待办概述](https://open.dingtalk.com/document/development/dingtalk-todo-task-overview)
|
||||
- [创建钉钉待办任务](https://open.dingtalk.com/document/development/add-dingtalk-to-do-task)
|
||||
- [更新钉钉待办任务](https://open.dingtalk.com/document/development/updates-dingtalk-to-do-tasks)
|
||||
- [删除钉钉待办任务](https://open.dingtalk.com/document/development/delete-dingtalk-to-do-tasks)
|
||||
- [更新钉钉待办执行者状态](https://open.dingtalk.com/document/development/update-dingtalk-to-do-status)
|
||||
|
||||
## 正式环境建议新增的环境变量
|
||||
|
||||
下面这些变量建议作为**待办功能正式接入专用配置**。当前代码尚未消费它们。
|
||||
|
||||
```env
|
||||
# 是否启用 OA 看板钉钉待办
|
||||
DINGTALK_TODO_ENABLED=false
|
||||
|
||||
# 钉钉开放平台 API 应用配置(待办、审批等服务端调用)
|
||||
DINGTALK_API_CLIENT_ID=
|
||||
DINGTALK_API_CLIENT_SECRET=
|
||||
|
||||
# 钉钉网页登录使用独立 OAuth 应用配置,不与 API 应用混用
|
||||
DINGTALK_LOGIN_CLIENT_ID=
|
||||
DINGTALK_LOGIN_CLIENT_SECRET=
|
||||
DINGTALK_CALLBACK_ORIGIN=
|
||||
|
||||
# 待办跳转回系统的基础地址
|
||||
DINGTALK_TODO_DETAIL_BASE_URL=
|
||||
|
||||
# 用于构造钉钉待办来源标识 / 业务空间
|
||||
DINGTALK_TODO_SOURCE_APP=oa-board
|
||||
|
||||
# 是否开启超时升级提醒
|
||||
DINGTALK_TODO_ESCALATION_ENABLED=false
|
||||
|
||||
# 超时升级阈值(天)
|
||||
DINGTALK_TODO_ESCALATION_DAYS=3
|
||||
```
|
||||
|
||||
如果最终接入方案要求额外的应用标识,还可能需要:
|
||||
|
||||
```env
|
||||
DINGTALK_AGENT_ID=
|
||||
```
|
||||
|
||||
是否必须取决于最终选用的钉钉待办接口版本与应用类型。
|
||||
|
||||
## 建议新增的数据表
|
||||
|
||||
建议新增一张专门的待办投递表,避免把钉钉待办状态散落在 JSON 里。
|
||||
|
||||
建议表名:
|
||||
|
||||
- `oa_board_dingtalk_todo_dispatch`
|
||||
|
||||
用途:
|
||||
|
||||
1. 记录某个业务流程 / 阶段是否已创建钉钉待办
|
||||
2. 保存钉钉待办 ID / 执行人 / 状态
|
||||
3. 支持去重、重试、关闭、审计
|
||||
|
||||
SQL 草案见:
|
||||
|
||||
- `scripts/sql/create-oa-board-dingtalk-todo-dispatch-table.sql`
|
||||
|
||||
## 建议的发送时机
|
||||
|
||||
### 第一阶段:最小闭环
|
||||
|
||||
先只做一类待办:
|
||||
|
||||
1. 当流程进入新的“当前审批阶段”
|
||||
2. 且解析出了唯一责任人
|
||||
3. 且该阶段还没有有效待办
|
||||
4. 则创建一条钉钉待办
|
||||
|
||||
建议首批接入阶段:
|
||||
|
||||
1. `产品设计-外观`
|
||||
2. `产品设计-结构`
|
||||
3. `产品生产-首单下单`
|
||||
4. `限价-发起`
|
||||
|
||||
原因:
|
||||
|
||||
1. 这些阶段责任人相对清晰
|
||||
2. 都已经有字段或规则可映射负责人
|
||||
3. 业务价值高,适合先验证
|
||||
|
||||
### 第二阶段:关闭与更新
|
||||
|
||||
当以下任一情况发生时,同步钉钉待办:
|
||||
|
||||
1. 当前阶段变化
|
||||
- 关闭旧待办
|
||||
- 为新阶段责任人创建新待办
|
||||
|
||||
2. 流程完成 / 驳回 / 终止
|
||||
- 关闭对应待办
|
||||
|
||||
3. 负责人变更
|
||||
- 更新执行人,或关闭旧待办后重建
|
||||
|
||||
### 第三阶段:升级提醒
|
||||
|
||||
当流程满足以下条件时再发升级提醒:
|
||||
|
||||
1. 阶段超时
|
||||
2. 原待办已创建
|
||||
3. 距离上次提醒超过冷却时间
|
||||
4. 升级接收人可解析
|
||||
|
||||
升级对象建议:
|
||||
|
||||
1. 当前负责人
|
||||
2. 当前负责人上级
|
||||
3. 商品企划 / 荆川 / 管理员可选兜底
|
||||
|
||||
## 负责人解析策略
|
||||
|
||||
待办发送目标不能只靠中文名,必须最终落到钉钉身份。
|
||||
|
||||
建议解析顺序:
|
||||
|
||||
1. 当前运行任务的 `userId`
|
||||
- 直接映射 `dingtalkUserId`
|
||||
|
||||
2. 表单中责任人字段
|
||||
- 如 `appearanceDesigner`、`structureDesigner`、`merchandiserOwner`、`pricingOwner`
|
||||
- 用姓名匹配本地用户 / 员工目录,取 `dingtalkUserId`
|
||||
|
||||
3. 阶段默认角色兜底
|
||||
- 例如 `采购/跟单`
|
||||
- 通过角色到成员名单的映射表进一步解析
|
||||
|
||||
如果一个阶段只能解析到“角色名”而不能解析到唯一员工,建议:
|
||||
|
||||
1. 不直接发待办
|
||||
2. 记录 `NEEDS_OWNER_MAPPING`
|
||||
3. 在看板中提示“负责人映射未完成”
|
||||
|
||||
## 需要新增的服务模块
|
||||
|
||||
建议至少拆成这几个文件:
|
||||
|
||||
1. `src/server/services/dingtalk-todo.service.ts`
|
||||
- 创建 / 更新 / 删除待办
|
||||
- 管理 app token
|
||||
|
||||
2. `src/server/services/oa-board-dingtalk-todo-dispatch.service.ts`
|
||||
- 负责业务规则
|
||||
- 判断是否该发待办
|
||||
- 生成 payload
|
||||
- 落库记录
|
||||
|
||||
3. `src/server/services/oa-board-owner-resolution.service.ts`
|
||||
- 负责将阶段负责人解析成钉钉用户
|
||||
|
||||
4. `scripts/sync-oa-board-todos.ts`
|
||||
- 用于定时补发、关闭、升级
|
||||
|
||||
## 需要新增的 API / 任务
|
||||
|
||||
### 后台任务
|
||||
|
||||
建议新增定时任务:
|
||||
|
||||
1. `sync:oa-board:todos`
|
||||
- 扫描当前有效流程
|
||||
- 创建缺失待办
|
||||
- 关闭失效待办
|
||||
- 触发升级提醒
|
||||
|
||||
### 管理接口
|
||||
|
||||
如有需要,可新增:
|
||||
|
||||
1. `POST /api/intake/oa-board/todo-dispatch/retry`
|
||||
- 手工重试失败待办
|
||||
|
||||
2. `GET /api/intake/oa-board/todo-dispatch`
|
||||
- 查看待办发送日志
|
||||
|
||||
## 幂等与重试建议
|
||||
|
||||
待办系统必须做幂等。
|
||||
|
||||
建议唯一业务键:
|
||||
|
||||
```text
|
||||
board_key + stage_key + receiver_dingtalk_user_id
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. 同一个业务键,只允许存在 1 条 `OPEN / PENDING / RETRYING` 记录
|
||||
2. 接口超时后不立即重发,先查日志状态
|
||||
3. 网络失败按指数退避重试
|
||||
|
||||
## 正式环境能否“只配置就开”
|
||||
|
||||
最终判断:
|
||||
|
||||
1. **现在不能**
|
||||
- 因为缺少待办发送代码和日志表
|
||||
|
||||
2. **补完代码后可以**
|
||||
- 正式系统只需配置环境变量
|
||||
- 再开启定时任务即可
|
||||
|
||||
## 推荐落地顺序
|
||||
|
||||
### P1
|
||||
|
||||
1. 新增待办投递表
|
||||
2. 新增 `dingtalk-todo.service.ts`
|
||||
3. 先打通“创建待办”
|
||||
4. 先只接 `产品生产-首单下单`
|
||||
|
||||
### P2
|
||||
|
||||
1. 接入更多阶段
|
||||
2. 做关闭 / 更新逻辑
|
||||
3. 做负责人映射异常日志
|
||||
|
||||
### P3
|
||||
|
||||
1. 做超时升级提醒
|
||||
2. 做看板内待办发送状态可视化
|
||||
3. 做手工重试与运维面板
|
||||
|
||||
## 建议的产品口径
|
||||
|
||||
为了降低误发风险,建议待办文案统一包含:
|
||||
|
||||
1. 开款编号
|
||||
2. 当前阶段
|
||||
3. 目标动作
|
||||
4. 超时状态
|
||||
5. 跳转链接
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
【OA 新品流程看板】XP2026070900000291 已进入“产品生产-首单下单”,请尽快处理。
|
||||
```
|
||||
|
||||
121
furniture-3d-system/docs/架构设计/供应链产品全量档案字段来源对照表.md
Normal file
121
furniture-3d-system/docs/架构设计/供应链产品全量档案字段来源对照表.md
Normal file
@ -0,0 +1,121 @@
|
||||
# 供应链产品全量档案字段来源对照表
|
||||
|
||||
更新时间:2026-10-16
|
||||
|
||||
## 1. 合并与追踪规则
|
||||
|
||||
- 汇总表:`SupplyChainProductArchive`
|
||||
- 唯一合并键:`barcode`
|
||||
- 来源记录:`SupplyChainProductArchiveSource`
|
||||
- 字段级来源:`SupplyChainProductArchive.fieldSourceMap`
|
||||
- 产品全量档案页面使用的来源数据必须先写入 `SupplyChainProductArchive` 或图片缓存;页面列表不再临时联查来源库。
|
||||
- 新品图片先用 `external.product_catalog_wide_v1.archive_image` 判断当前 SKU/条码行是否真的有图;确认有图后,保存同一行标准化的绝对 `image_url`,绝对地址为空时才保留 `archive_image`。当 `archive_image` 为空时禁止读取 `image_url`,因为该列此时可能回退到产品级图片。宽表当前行无图时,再按当前条码/SKU精确查询素材库;命中的链接和来源信息保存在 `SupplyChainProductArchiveImageCache`。图片二进制不写入 PostgreSQL,图片变化不进入商品修改日志。
|
||||
- 首次建立商品档案只记录一条“商品档案导入”;后续来源值实际变化时,按条码、字段、旧值、新值和来源写入 `OperationLog`。
|
||||
- 扩展字段通过 `sync:supply-chain-product-archive-enrichment` 分批同步;重复执行时只有值发生变化的字段才更新主表并写日志。
|
||||
- 生产定时入口为 `scripts/install-supply-chain-product-archive-sync-timer.sh`,默认每 15 分钟先同步 `rpt.jst_sku` 基础档案,再同步供应商、仓库、生产周期、日销和价格等扩展字段。
|
||||
- 聚水潭来源只读取 `JSReportReplica.rpt.jst_sku` 中 `item_type = '成品'` 的行。
|
||||
- `jst_sku` 只更新本表明确列出的字段;空值不覆盖其他来源的已有值。
|
||||
- 已由 `JST_SKU` 提供且有值的字段,不再被产品开发同步覆盖。
|
||||
- 一级类目由 `SupplyChainProductArchiveCategoryConfig` 按 JST 二级类目匹配,未配置时保持空值。
|
||||
- 创建时间优先级固定为:`云杉商品资料.xlsx` 的“创建时间” > `jst_sku.created` > 产品开发创建时间。
|
||||
- 云杉文件只在部署或补数时通过导入脚本读取一次,不进入 Git,页面运行时不读取 Excel。
|
||||
- 体积统一由同一来源记录的长、宽、高相乘计算;任一尺寸缺失、不是有效数字或不大于 0 时保持空值。
|
||||
- 未列出的字段不猜测、不兜底,保持原来源或空值。
|
||||
|
||||
## 2. 当前字段对照
|
||||
|
||||
| 全量档案字段 | 页面表头 | JST_SKU 来源 | 产品开发来源 | 其他来源 | 转换规则 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `barcode` | 条码 | `rpt.jst_sku.sku_id` | `external.product_catalog_wide_v1.barcode_business_key` | | 去空格并转大写;档案唯一键 |
|
||||
| `spu` | SPU | `rpt.jst_sku.i_id` | `archive_spu` | | 去空格并转大写 |
|
||||
| `sku` | SKU | `rpt.jst_sku.other_7` | `archive_sku` | | 去空格并转大写;JST 空值不兜底 |
|
||||
| `image` | 图片 | | `external.product_catalog_wide_v1.archive_image` + `image_url` | 素材库接口 + `SupplyChainProductArchiveImageCache.previewUrl` | 新品以当前 SKU/条码行 `archive_image` 非空为前提,保存同一行标准化 `image_url`;`archive_image` 为空时不得使用可能回退到产品级图片的 `image_url`;当前行无图时再按当前条码/SKU精确查询素材库,命中链接长期复用,未命中则留空 |
|
||||
| `businessCreatedAt` | 创建时间 | `rpt.jst_sku.created` | `product_development_created_at`,空时取 `source_created_at` | 云杉文件“创建时间”按条码匹配 | 按固定优先级覆盖 |
|
||||
| `categoryL1` | 一级类目 | | `sales_category_name` | `SupplyChainProductArchiveCategoryConfig.categoryL1` | JST 行按完整二级类目匹配配置表 |
|
||||
| `categoryL2` | 二级类目 | `rpt.jst_sku.category` | `archive_product_category_name` | | JST 原样保留完整类目文本 |
|
||||
| `productName` | 商品名称 | `rpt.jst_sku.name` | `archive_product_name` | | 原样保留 |
|
||||
| `designer` | 设计师 | 暂不接入 | `archive_designer` | | |
|
||||
| `productStatus` | 商品状态 | `rpt.jst_sku.other_5` | `sku_sales_status` | | 只接受“销售、停产、退市” |
|
||||
| `material` | 材质 | `rpt.jst_sku.other_10` | `archive_material` | | 原样保留 |
|
||||
| `color` | 颜色 | `rpt.jst_sku.properties_value` | `archive_color` | | 原样保留 |
|
||||
| `weight` | 重量 | `rpt.jst_sku.weight` | `sample_weight_kg` | | 转十进制数;空值不覆盖 |
|
||||
| `length` | 长 | `rpt.jst_sku.l` | `package_length_cm` | | 转十进制数;空值不覆盖 |
|
||||
| `width` | 宽 | `rpt.jst_sku.w` | `package_width_cm` | | 转十进制数;空值不覆盖 |
|
||||
| `height` | 高 | `rpt.jst_sku.h` | `package_height_cm` | | 转十进制数;空值不覆盖 |
|
||||
| `volume` | 体积 | `rpt.jst_sku.l * w * h` | `package_length_cm * package_width_cm * package_height_cm` | | 不读取来源体积列,统一按长宽高计算 |
|
||||
| `packType` | 单双包 | `rpt.jst_sku.other_8` | `package_set_count` | | 1-9 转为非双包至九包;已是标准文字时原样保留 |
|
||||
| `productTag` | 商品标签 | `rpt.jst_sku.labels` | `product_tag` | | 原样保留 |
|
||||
| `installDocId` | 安装图纸/视频 ID | `rpt.jst_sku.other_9` | `drawing_id` | | 原样保留 |
|
||||
| `spec` | 产品尺寸 | 从 `rpt.jst_sku.name` 提取 | `archive_spec` | | 提取三轴尺寸,统一 `/` 分隔,保留小数和范围 |
|
||||
| `brand` | 品牌 | `rpt.jst_sku.brand` | `brand_name` | | 原样保留 |
|
||||
| `orderType` | 叫货类型 | `rpt.jst_sku.other_price_2` | `purchase_order_method` | | 按现有枚举归一 |
|
||||
| `storageType` | 入库/代发 | | | `SupplyChainProductArchiveWarehouseConfig.storageType` | 按出入库仓库名称匹配配置表 |
|
||||
| `isSeasonal` | 是否季节性产品 | 暂无来源 | 暂无来源 | | 保持空值 |
|
||||
| `replaceCount` | 替换款数量 | 暂无来源 | 暂无来源 | | 保持空值 |
|
||||
| `region` | 区域 | | | `rpt.js_supplier.region` | 按供应商名称匹配后写主表 |
|
||||
| `supplierName` | 供应商 | `rpt.jst_sku.supplier_name` | `archive_supplier_name` | | 原样保留 |
|
||||
| `supplierType` | 供应商类型 | | | `rpt.js_supplier.scmtype` | 按供应商名称匹配后写主表 |
|
||||
| `supplierStatus` | 供应商状态 | | | `rpt.js_supplier.coopersgn` | `1=合作`、`2=不合作` |
|
||||
| `purchaseOwner` | 采购 | | `archive_purchase_owner` | `rpt.js_supplier.buyer` | 供应商采购非空时覆盖产品开发采购 |
|
||||
| `settlementMethod` | 结算方式 | | | `rpt.js_supplier.paycyclesgn` | 按供应商名称匹配 |
|
||||
| `boardProvider` | 板材提供方 | | | `rpt.js_supplier.undefined2` | 按供应商名称匹配 |
|
||||
| `penaltyStatus` | 违约金赔付状态 | | | `rpt.js_supplier.undefined1` | 按供应商名称匹配 |
|
||||
| `warehouseType` | 仓库属性 | | | `rpt.jst_warehouseInfo.remark2` | 按出入库仓库名称匹配 |
|
||||
| `warehouseHead` | 仓库负责人 | | | `SupplyChainProductArchiveWarehouseConfig.warehouseHead` | 按出入库仓库名称匹配配置表 |
|
||||
| `minimumOrderQuantity` | 产品起订量 | | `archive_minimum_order_quantity` | `rpt.pm_stockMonitor.minimum` | stock monitor 非空时覆盖产品开发起订量 |
|
||||
| `firstLeadTime` | 首单生产周期 | | | `rpt.pm_stockMonitor.produceCycle` | 条码优先、SKU 唯一匹配兜底 |
|
||||
| `palletSpec` | 托盘参数 | | | `rpt.pm_stockMonitor.trayParameter` | 条码优先、SKU 唯一匹配兜底 |
|
||||
| `dailySales` | 采购日销 | | | `rpt.pm_stockMonitor.purchaseSale` | 按条码匹配最新记录 |
|
||||
| `sales3d` | 3日日销 | | | `rpt.pm_stockMonitor.threeAvgSale` | 按条码匹配最新记录 |
|
||||
| `sales7d` | 7日日销 | | | `rpt.pm_stockMonitor.sevenAvgSale` | 按条码匹配最新记录 |
|
||||
| `sales15d` | 15日日销 | | | `rpt.pm_stockMonitor.fifteenAvgSale` | 按条码匹配最新记录 |
|
||||
| `purchasePrice` | 采购价 | `rpt.jst_sku.purchase_price` | | | 按档案条码精确匹配 `jst_sku.sku_id`;匹配不到时留空 |
|
||||
| `costPrice` | 成本价(聚水潭) | `rpt.jst_sku.cost_price` | | | 按档案条码精确匹配 `jst_sku.sku_id`;匹配不到时留空。DF 条码自 2026-10-16 起同样取本来源(原 `rpt.dws_sn_sales_full_info_di.goods_cost` 为金额口径,已停用) |
|
||||
| `costPriceSecondary` | 成本价(新品) | | `external.product_catalog_wide_v1.cost_price` | | 按档案条码精确匹配 `barcode_business_key`,只读取 `catalog_source = INTAKE_READY` 的新品行;匹配不到或来源为空时留空 |
|
||||
|
||||
## 3. JST_SKU 第一批字段覆盖率
|
||||
|
||||
统计范围:`item_type = '成品'`,共 65,164 行。
|
||||
|
||||
| 来源字段 | 对应档案字段 | 非空行数 |
|
||||
| --- | --- | ---: |
|
||||
| `sku_id` | 条码 | 65,164 |
|
||||
| `i_id` | SPU | 65,164 |
|
||||
| `name` | 商品名称、规格解析 | 65,164 |
|
||||
| `other_7` | SKU | 63,537 |
|
||||
| `other_5` | 商品状态 | 63,771 |
|
||||
| `other_9` | 安装图纸/视频 ID | 24,792 |
|
||||
| `other_10` | 材质 | 31,394 |
|
||||
| `properties_value` | 颜色 | 63,855 |
|
||||
| `category` | 二级类目 | 63,539 |
|
||||
| `weight` | 重量 | 35,754 |
|
||||
| `l` | 长 | 38,224 |
|
||||
| `w` | 宽 | 38,083 |
|
||||
| `h` | 高 | 38,124 |
|
||||
| `other_8` | 单双包 | 61,908 |
|
||||
| `labels` | 商品标签 | 以本次来源库实时数据为准 |
|
||||
| `brand` | 品牌 | 以本次来源库实时数据为准 |
|
||||
| `supplier_name` | 供应商 | 以本次来源库实时数据为准 |
|
||||
|
||||
## 4. 来源代码
|
||||
|
||||
| 来源代码 | 来源系统 | Schema | 对象 |
|
||||
| --- | --- | --- | --- |
|
||||
| `JST_SKU` | 聚水潭 JSReportReplica | `rpt` | `jst_sku` |
|
||||
| `PRODUCT_DEVELOPMENT` | 家具产品开发系统 | `external` | `product_catalog_wide_v1` |
|
||||
| `CATEGORY_CONFIG` | 供应链中心类目配置 | `public` | `SupplyChainProductArchiveCategoryConfig` |
|
||||
| `JST_SUPPLIER` | 聚水潭 JSReportReplica | `rpt` | `js_supplier` |
|
||||
| `JST_STOCK_MONITOR` | 聚水潭 JSReportReplica | `rpt` | `pm_stockMonitor` |
|
||||
| `JST_WAREHOUSE_INFO` | 聚水潭 JSReportReplica | `rpt` | `jst_warehouseInfo` |
|
||||
| `JST_SKU_PRICE` | 聚水潭 JSReportReplica | `rpt` | `jst_sku` |
|
||||
| `PRODUCT_CATALOG_COST_PRICE` | 家具产品开发系统 | `external` | `product_catalog_wide_v1` |
|
||||
| `WAREHOUSE_CONFIG` | 供应链中心 | `public` | `SupplyChainProductArchiveWarehouseConfig` |
|
||||
| `YUNSHAN_PRODUCT_EXPORT` | 云杉商品资料 | Excel 工作表名 | Excel 文件名 |
|
||||
|
||||
## 5. 生产同步入口
|
||||
|
||||
- 产品开发页面保存成功后,会按当前产品即时同步 `external.product_catalog_wide_v1` 的档案字段。
|
||||
- 聚水潭及其关联表由 systemd timer 周期检查。同步过程可以重复执行,主表只写实际变化,商品修改日志只记录后续真实变化。
|
||||
- 定时任务安装前必须在生产服务器的 `shared/supply-chain-product-archive-sync.env` 填写目标 PostgreSQL 和来源 SQL Server 的身份校验值;文件权限固定为 `600`,其中不保存数据库密码。
|
||||
- 安装命令只在发布完成并获得生产操作授权后执行:`bash scripts/install-supply-chain-product-archive-sync-timer.sh`。
|
||||
- 新品即时同步以宽表当前 SKU/条码行的 `archive_image` 是否存在为准,并把同一行标准化的绝对 `image_url` 写入档案和 `SupplyChainProductArchiveImageCache`。宽表当前行无图时,页面才按当前条码/SKU调用素材库接口并回写链接;存量图片规则不在本次新品图片调整范围内。
|
||||
@ -29,6 +29,7 @@
|
||||
- `scripts/print-master-data-foundation-decision-package.js`
|
||||
- `npm run db:verify-master-data:all`
|
||||
- `npm run db:verify-master-data:audit`
|
||||
- `npm run db:verify-master-data:production-readonly`
|
||||
- `npm run db:verify-master-data:readiness`
|
||||
- `npm run db:verify-master-data:readiness:audit`
|
||||
- `node scripts/verify-master-data-code-boundaries.js --mode reference-estimator-cost-boundary`
|
||||
@ -46,6 +47,8 @@
|
||||
- `npm run db:foundation:apply-structure-repair`
|
||||
- `npm run db:foundation:apply-structure-repair:audit`
|
||||
- `npm run db:foundation:audit`
|
||||
- `npm run db:foundation:audit:production-readonly`
|
||||
- `npm run db:foundation:verify-production-readonly`
|
||||
- `npm run db:foundation:audit:cleanup-template`
|
||||
- `npm run db:foundation:completion-checklist`
|
||||
- `npm run db:foundation:decision-package`
|
||||
@ -72,9 +75,13 @@
|
||||
|
||||
完整地基审计批次使用 `npm run db:foundation:audit`。它会在 `.master-data-audits/foundation-audit-<timestamp>/` 下依次写出 `readiness.json`、`foundation-hard-ready-gate.json`、`foundation-preflight.json`、`foundation-migrate-plan.json` 和 `manifest.json`;即使其中任一门禁失败,也会继续收集后续证据并在 manifest 中汇总 blocker。这个入口只做只读/干跑证据收集,不生成备份、不执行迁移、不清洗数据;它的用途是给每次治理形成同一时刻的可复核证据包。`manifest.json` 会写入脱敏 `readinessSummary` 和 `foundationHardReadyGateSummary`,包含 `hardReady`、blocker 数、blocker key、数量值和迁移应用状态摘要,但不会复制历史 SKU、产品头或准 SKU 的样本行。`manifest.hardReady` 必须同时满足 readiness 总就绪度和 foundationHardReadyGate 总门禁都为 true,任何一个缺失或失败都不能宣称地基已十分稳固。
|
||||
|
||||
最终完成判断必须看 `manifest.foundationCompletionChecklist`,不能把某个局部命令的绿色结果当成全目标完成。该清单把本目标拆成 8 个机器可判定项:数据库结构与 Prisma 迁移历史、核心对象唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史主数据字段清洗、PreSku 正式化生命周期和授权结构修复施工链。`npm run db:foundation:completion-checklist` 默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,输出脱敏清单;只要任一项 `blocked / missing`,命令就以非 0 状态退出。它不重新审计数据库,只是把同批证据的完成判定单独拎出来,给协作、验收和 goal completion audit 使用。
|
||||
生产或目标库只读巡检优先使用 `npm run db:foundation:audit:production-readonly`。该入口会让 readiness 和 hard-ready gate 都走 `--read-only-quality`,迁移状态只通过只读查询确认;如果治理迁移还不是 `fullyReady`,脚本会直接把迁移缺口写入报告,不会在生产库事务内回放迁移 SQL。单项快速确认可用 `npm run db:verify-master-data:production-readonly`,它只输出脱敏 summary。生产只读证据包仍可能包含真实表结构、数量和阻断键,应留在生产本地或受控交接中,不提交 Git。拿到生产/目标库 manifest 后,再运行 `npm run db:foundation:verify-production-readonly -- --manifest <manifest.json>`,它只接受 `auditProfile = production-read-only`、`migrationApplicationStatus.source = read-only-migration-status`、`hardReady = true`、blocker 归零、步骤全通过且证据未过期的报告;本地 full audit、shadow 演练或混入写库计划的 manifest 不能冒充生产只读收口证据。
|
||||
|
||||
如果完成清单没有 8/8 通过,下一步施工判断看 `npm run db:foundation:decision-package`。它同样默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只从 manifest 的脱敏摘要和完成清单生成“决策包”,把剩余阻断拆成结构修复授权、治理迁移落库、历史字段业务确认、PreSku 生命周期专用决策和最终验证五段。这个入口不写数据库、不复制敏感候选行、不输出绝对路径;用途是防止后续把 `apply-structure`、`resolve-baseline`、`apply-required`、历史清洗和 PreSku 专用修复混成一条危险捷径。决策包的 `nextGate` 只表示下一道施工门禁,不表示该门禁已经被授权或已完成。
|
||||
最终完成判断必须看 `manifest.foundationCompletionChecklist`,不能把某个局部命令的绿色结果当成全目标完成。该清单把本目标拆成 9 个机器可判定项:数据库结构与 Prisma 迁移历史、核心对象唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史主数据字段清洗、历史对象存储引用迁移、PreSku 正式化生命周期和授权结构修复施工链。`npm run db:foundation:completion-checklist` 默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,输出脱敏清单;只要任一项 `blocked / missing`,命令就以非 0 状态退出。它不重新审计数据库,只是把同批证据的完成判定单独拎出来,给协作、验收和 goal completion audit 使用。
|
||||
|
||||
历史对象存储引用迁移要用 `npm run storage:migrate-historical-assets:gate` 或在批次审计里显式追加 `--include-storage-migration-gate`。该 gate 只做 dry-run,但会要求旧本地素材、旧 S3/七牛链接的源文件可读取,且目标对象不存在时不能继续放行;失败报告只作为本地敏感证据包保存,不应提交 Git。
|
||||
|
||||
如果完成清单没有 9/9 通过,下一步施工判断看 `npm run db:foundation:decision-package`。它同样默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只从 manifest 的脱敏摘要和完成清单生成“决策包”,把剩余阻断拆成结构修复授权、治理迁移落库、历史字段业务确认、历史对象存储引用迁移、PreSku 生命周期专用决策和最终验证六段。这个入口不写数据库、不复制敏感候选行、不输出绝对路径;用途是防止后续把 `apply-structure`、`resolve-baseline`、`apply-required`、历史清洗、对象存储迁移和 PreSku 专用修复混成一条危险捷径。决策包的 `nextGate` 只表示下一道施工门禁,不表示该门禁已经被授权或已完成。
|
||||
|
||||
决策包还必须输出 `executionChecklist`,把真实施工拆成“授权前刷新证据 -> 真实执行结构修复 SQL -> 结构修复后重新预检 -> 逐项补齐 Prisma 迁移历史 -> 应用 bootstrap-repair 结构/回填修复 -> 应用最终治理迁移/护栏 -> 业务确认后清洗历史质量债 -> 最终 hardReady 审计”。其中所有 `writeDatabase=true` 的阶段都必须有明确用户授权;`apply-structure` 成功前禁止进入 `resolve-baseline / apply-required / 历史清洗`,`post-repair preflight` 未证明结构 diff 归零前禁止 baseline resolve,治理迁移未 `fullyReady` 前禁止历史清洗。这个顺序是地基施工门禁,不是建议步骤。
|
||||
|
||||
@ -295,6 +302,8 @@ baseline resolve 完成后,治理迁移、外部只读视图、约束、触发
|
||||
- 后端写入入口也必须和数据库护栏同口径,不能把数据库约束当成第一道业务校验。当前已收紧 `PlanningTemplate.code`、`WarehouseLocation.code`、`Substrate.code`、`Material.code`、`Hardware.code`、`CommonComponent.code`、正式 `Product.modelNumber`、正式 `ProductSKU.skuCode`、`IntakeNonIntegratedSkuItem.skuCode / barcode`、SKU CAD 导入、传统产品上传、准 SKU 包装方案保存、品牌物料准备项、正式入库承接链路、协作者角色和产品上传可见范围:进入仓储层前先规范化身份字段,空编码/空身份、游离状态、负数数量、重复物理库位、非法权限角色和可提前发现的冲突由服务层直接阻断。
|
||||
- 仓储查询入口不能再把“调用方已经规范化”当隐含前提。资产中心、正式产品、正式 SKU、基础物料、库位和规划模板等核心 `findByCode / findBySkuCode / findByModelNumber / codeExists` 查询会先做 `trim + uppercase`;资产 CAD 导入也会在查找已有资产族、创建规格款和生成模型存储路径前归一手填编码,避免同一编码因大小写或首尾空格在服务层表现不一致。
|
||||
- 正式 SKU 资产同步副本的身份字段必须在快照构建层二次规范化:`ProductSkuAssetProfile.model / specCode / barcode` 以及 `sourceVariants` 中对应字段进入结构化副本前统一 `trim + uppercase`,人工锁定字段也不能绕过数据库 `CHECK` 护栏。
|
||||
- `ProductSkuAssetProfile.lockedFields / lockedReasons` 只能保存登记过的锁定字段名,快照构建会过滤空白、拼错和重复字段;readiness 会把历史 profile 中无效锁定字段或无效锁定原因列为 hard blocker,避免人工锁定优先级变成不可审计的自由 JSON。
|
||||
- `AssetSkuSyncEvent` 不只检查字段形态,还要作为资产中心到正式 SKU 同步是否真实闭环的运行证据。readiness 会把 `FAILED`、超过 15 分钟仍停留在 `PENDING / PROCESSING`、以及完成态仍保留失败计数的事件列为 hard blocker,避免源资产变更、缩略图重生成或删除事件没有真正刷新正式 SKU 资产资料副本。
|
||||
- `PackagedItem.code` 由正式 `ProductSKU.skuCode` 和包装箱编号生成;正式入库前必须校验包装箱编号是唯一正整数,且包装箱尺寸必须大于 0、预估重量不能为负数,避免用重复箱号、小数箱号或脏箱规生成冲突、不稳定的包装成品和轻仓库数据。
|
||||
- `SpuNumberCounter.productCategory / prefix` 是正式 SPU 号段身份,不只要求非空和大写,还必须数据库级唯一;产品品类还要按去空格和大小写归一后唯一。不同产品品类不能共用同一个前缀,同一产品品类也不能因为空格差异形成两个号池,否则并发占号会变成不可追溯的双计数器。
|
||||
- 正式入库前会校验 `PreSku.variantSelection` 引用的 `AssetVariant` 仍存在、未删除、状态可用、资产族和资产类型未漂移;正式生成板件、孔位和五金时读取当前 `AssetVariant` 结构化数据,不再信任旧快照里的尺寸和孔位。
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
地基批次审计:`npm run db:foundation:audit` 用于把 `readiness`、`foundationHardReadyGate` 总门禁、结构变更前置预检、baseline/结构修复施工图和迁移 dry-run 计划放进同一个本地证据目录,并生成 `manifest.json` 汇总 blocker。它不写数据库,不生成备份,也不替代迁移授权;用途是让每次治理前后的证据可对比、可复核。`manifest.hardReady` 必须同时满足 `readiness.hardReady = true`、`foundationHardReadyGate.hardReady = true` 且没有 blockers;总门禁原始报告会标记为敏感文件,manifest 只暴露脱敏的 `foundationHardReadyGateSummary`。
|
||||
|
||||
最终完成清单:完整批次审计会在 `manifest.foundationCompletionChecklist` 中把“数据库结构与迁移历史、唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史字段清洗、PreSku 生命周期、授权施工链”拆成逐项 `passed / blocked / missing`。需要把这份清单单独输出给协作或验收时,运行 `npm run db:foundation:completion-checklist`;该命令默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只打印脱敏清单,并且只在所有项都 passed 时以 0 状态退出。它不是新的审计来源,只是防止把某个局部绿色结果误判为“数据地基已完成”。
|
||||
最终完成清单:完整批次审计会在 `manifest.foundationCompletionChecklist` 中把“数据库结构与迁移历史、唯一真相源矩阵、后端写入边界、JSON/快照边界、文件系统边界、历史字段清洗、历史对象存储引用迁移、PreSku 生命周期、授权施工链”拆成逐项 `passed / blocked / missing`。需要把这份清单单独输出给协作或验收时,运行 `npm run db:foundation:completion-checklist`;该命令默认读取 `.master-data-audits/foundation-audit-current-goal/manifest.json`,只打印脱敏清单,并且只在所有项都 passed 时以 0 状态退出。它不是新的审计来源,只是防止把某个局部绿色结果误判为“数据地基已完成”。对象存储引用迁移证据通过 `--include-storage-migration-gate` 纳入批次审计;未纳入时该项为 `missing`,源文件缺失或上传验证失败时该项为 `blocked`。
|
||||
|
||||
批次审计需要 shadow diff 留证时,显式使用 `--use-docker-shadow`。该模式会临时启动 Docker PostgreSQL shadow 库,把 `SHADOW_DATABASE_URL` 传给 preflight 和迁移计划步骤,结束后清理容器,并在 manifest 中写入 `shadowDiffEvidence`。它只证明迁移链和当前库/当前 schema 的差异,不代表目标数据库已经迁移完成。批次审计也可以显式追加 `--include-migration-rehearsal`,把从备份恢复到 Docker 克隆库并执行 `migrate deploy` 的演练报告写入同一目录的 `migration-rehearsal.json`。
|
||||
|
||||
|
||||
@ -12,14 +12,18 @@
|
||||
|
||||
- `external.*_v1` 视图是否齐全。
|
||||
- `external.intake_*_ready_v1` 是否存在,外部稳定消费是否能避开不完整草稿资料。
|
||||
- 仓库库位、SKU 库存、包装库存项和 SKU 质检记录是否已经进入外部契约层。
|
||||
- 非集成产品限价是否已经进入结构化限价视图和 SKU 聚合宽表。
|
||||
- 关键字段是否齐全。
|
||||
- 正式 SKU、非集成产品和非集成 SKU 是否都暴露稳定业务键,避免外部系统误用内部技术 ID 做同步身份。
|
||||
- 仓库视图是否暴露稳定库位编码,质检视图是否暴露稳定记录键。
|
||||
- `external_reader` 是否被误授权到 `public` schema。
|
||||
- `external_reader` 在 `external` schema 下是否只有 `SELECT` 权限。
|
||||
- 对外字段里是否混入 `payload`、权限、内部路径、密钥类字段名。
|
||||
- `external` 视图定义内部是否仍依赖 `payload / attributes / businessData / displaySummary / familySelection` 等 JSON 快照源。
|
||||
- 正式 SKU、资产规格款、非集成 SKU 的关键字段是否存在明显空值。
|
||||
- 正式 SKU 和非集成 ready 视图的业务键是否缺失或重复。
|
||||
- 产品限价视图是否存在适用但未确认的限价记录。
|
||||
- 正式产品素材和 BOM 明细视图是否携带可关联的稳定业务键。
|
||||
- 素材 URL 是否仍有不适合长期对外同步的 `data:` 形态。
|
||||
- 非集成 SKU 明细是否已经达到 `READY`,还是仍有 `INCOMPLETE` 草稿数据。
|
||||
@ -43,22 +47,39 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
|
||||
- `missing_external_column`
|
||||
- `external_contract_private_column_risk`
|
||||
- `external_view_json_source_dependency_risk`
|
||||
- `external_reader_public_schema_grant_risk`
|
||||
- `external_reader_non_select_grant_risk`
|
||||
- `external_readonly_role_public_schema_grant_risk`
|
||||
- `external_readonly_role_non_select_grant_risk`
|
||||
- `external_asset_url_unstable_or_internal_risk`
|
||||
- `product_assets_data_url_risk`
|
||||
- `product_sku_business_key_duplicate_risk`
|
||||
- `intake_product_business_key_duplicate_risk`
|
||||
- `intake_sku_business_key_duplicate_risk`
|
||||
- `product_assets_business_key_missing_risk`
|
||||
- `product_bom_business_key_missing_risk`
|
||||
- `sku_warehouse_stock_business_key_missing_risk`
|
||||
- `packaged_items_business_key_missing_risk`
|
||||
- `sku_quality_inspections_business_key_missing_risk`
|
||||
- `intake_product_limits_business_key_missing_risk`
|
||||
- `intake_product_limits_sku_business_key_duplicate_risk`
|
||||
- `warehouse_location_code_duplicate_risk`
|
||||
- `packaged_item_code_duplicate_risk`
|
||||
- `sku_quality_record_key_duplicate_risk`
|
||||
|
||||
下面这些检查结果是计数项,不一定要求马上为 0,但要进入数据质量治理台账:
|
||||
|
||||
- `product_skus_blank_key_fields`
|
||||
- `warehouse_locations_blank_key_fields`
|
||||
- `sku_warehouse_stock_blank_key_fields`
|
||||
- `packaged_items_blank_key_fields`
|
||||
- `sku_quality_inspections_blank_key_fields`
|
||||
- `asset_variants_blank_key_fields`
|
||||
- `intake_sku_items_blank_key_fields`
|
||||
- `intake_sku_items_quality_status_count`
|
||||
- `intake_ready_business_key_missing_risk`
|
||||
- `intake_product_limits_state_count`
|
||||
- `intake_product_limits_applicable_unconfirmed_risk`
|
||||
- `product_catalog_wide_limit_state_count`
|
||||
- `product_catalog_wide_applicable_unconfirmed_limit_risk`
|
||||
|
||||
如果关键编码、名称、型号大量为空,说明外部系统虽然能读数据,但读到的是不完整资料,不能直接进入稳定对接阶段。
|
||||
|
||||
@ -72,7 +93,7 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
|
||||
- 授权越界:撤回 `public` schema 或非 `SELECT` 权限,再重新执行授权脚本。
|
||||
- 私有字段风险:从视图里移除字段,必要时新增白名单字段。
|
||||
- 视图 JSON 源依赖风险:先判断该字段是否应该结构化;不能只改外部列名来绕过巡检。
|
||||
- 业务键缺失或重复:回到对应真相源修复 `Product.modelNumber / ProductSKU.skuCode / IntakeNonIntegratedProduct.brand+spu / IntakeNonIntegratedSkuItem.skuCode`,不能让外部系统临时用技术 ID 兜底;如果素材或 BOM 明细缺业务键,优先修视图关联链,不要让外部系统自行跨表补。
|
||||
- 业务键缺失或重复:回到对应真相源修复 `Product.modelNumber / ProductSKU.skuCode / IntakeNonIntegratedProduct.brand+spu / IntakeNonIntegratedSkuItem.skuCode / WarehouseLocation.code / ProductSkuQualityInspection.recordKey`,不能让外部系统临时用技术 ID 兜底;如果素材、BOM、仓库或质检明细缺业务键,优先修视图关联链,不要让外部系统自行跨表补。
|
||||
- 关键字段为空:回到资产中心、产品资料或非集成录入链路修源数据。
|
||||
- `data:` 素材:逐步迁移到文件 URL 或受控下载 API。
|
||||
- `INCOMPLETE` 非集成 SKU:外部系统不能自动入库,先由产品资料链路补齐或确认废弃;如果业务确认分阶段补齐,原因只写入 `MasterDataQualityDecision` 做审计,不改变 ready 视图过滤规则。
|
||||
@ -86,7 +107,7 @@ npx prisma db execute --file scripts/sql/check-external-readonly-contract.sql
|
||||
|
||||
`MasterDataQualityDecision` 不对外作为商品资料视图开放;它只是内部审计谁确认了暂不完整、缺哪些字段、为什么允许分阶段清洗。外部系统仍只能把 `READY` 当自动消费前提。
|
||||
|
||||
`npm run db:verify-master-data:all` 已把核心外部契约风险纳入硬失败:外部核心视图缺失、外部视图依赖内部 JSON、非集成产品负责人兼容列未优先读取结构化 profile、正式 SKU 视图业务键缺失或重复、正式产品素材/BOM 明细视图业务键缺失、ready SKU 视图混入非 READY 明细、ready 视图业务键缺失或重复时,命令必须失败。独立 SQL 巡检仍保留给人工查库和外部审计使用。
|
||||
`npm run db:verify-master-data:all` 已把核心外部契约风险纳入硬失败:外部核心视图缺失、外部视图依赖内部 JSON、非集成产品负责人兼容列未优先读取结构化 profile、正式 SKU 视图业务键缺失或重复、正式产品素材/BOM 明细视图业务键缺失、仓库/质检视图业务键缺失、ready SKU 视图混入非 READY 明细、ready 视图业务键缺失或重复时,命令必须失败。独立 SQL 巡检仍保留给人工查库和外部审计使用。
|
||||
|
||||
## 5. 规范审查
|
||||
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
- `prisma/migrations/20260530143000_tighten_external_readonly_view_sources/migration.sql`
|
||||
- `prisma/migrations/20260530150000_add_intake_non_integrated_product_profile/migration.sql`
|
||||
- `prisma/migrations/20260531103000_tighten_external_intake_views_to_structured_sources/migration.sql`
|
||||
- `prisma/migrations/20260608143000_add_external_product_catalog_wide_view/migration.sql`
|
||||
- `prisma/migrations/20260608150000_add_external_warehouse_quality_views/migration.sql`
|
||||
- `prisma/migrations/20260608162000_add_intake_product_limit_rows/migration.sql`
|
||||
关联巡检:`scripts/sql/check-external-readonly-contract.sql`
|
||||
|
||||
## 1. 使用原则
|
||||
@ -23,6 +26,12 @@
|
||||
| 视图 | 用途 | 主要来源 |
|
||||
| --- | --- | --- |
|
||||
| `external.product_skus_v1` | 正式产品 SKU 主数据 | `Product`、`ProductSKU`、`ProductSkuAssetProfile`、`ProductCategory`、`WarehouseLocation` |
|
||||
| `external.product_catalog_wide_v1` | 商品目录聚合宽表 | `external.product_skus_v1`、`external.intake_products_ready_v1`、`external.intake_sku_items_ready_v1`、`external.intake_product_limits_v1`、正式素材/BOM/仓库/质检外部视图 |
|
||||
| `external.intake_product_limits_v1` | 非集成产品限价白名单 | `IntakeNonIntegratedProductLimitRow`、`IntakeNonIntegratedSkuItem`、`IntakeNonIntegratedProductProfile` |
|
||||
| `external.warehouse_locations_v1` | 仓库库位资料 | `WarehouseLocation` |
|
||||
| `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 | `external.product_skus_v1`、`external.warehouse_locations_v1` |
|
||||
| `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 | `PackagedItem`、`ProductSKU`、`Product`、`WarehouseLocation` |
|
||||
| `external.sku_quality_inspections_v1` | SKU 仓库/首批质检记录 | `ProductSkuQualityInspection`、`external.product_skus_v1` |
|
||||
| `external.product_assets_v1` | 正式 SKU 图片/模型/缩略图 URL | `ProductSKU.images`、`ProductSKU.modelUrl`、`ProductSkuAssetProfile.imageUrl`、`Product.thumbnailUrl` |
|
||||
| `external.product_bom_v1` | 正式产品板件和五金摘要 | `Panel`、`PanelMaterial`、`ProductHardware`、`Hardware` |
|
||||
| `external.intake_products_v1` | 非集成开品主信息白名单 | `IntakeNonIntegratedProduct`、`IntakeNonIntegratedProductProfile` |
|
||||
@ -62,6 +71,7 @@
|
||||
| `asset_family_code` | 来源资产族编码 |
|
||||
| `spec_code` | 来源规格款编码 |
|
||||
| `barcode` | 条码 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
| `size_json` | 尺寸 JSON |
|
||||
| `color` | 颜色/花色 |
|
||||
| `material` | 材质 |
|
||||
@ -72,7 +82,8 @@
|
||||
|
||||
- 该视图过滤已软删除的 `Product` 和 `ProductSKU`。
|
||||
- 外部系统读取正式商品资料时优先使用该视图。
|
||||
- `product_id / sku_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新必须优先使用 `product_business_key / sku_business_key`。
|
||||
- `product_id / sku_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新优先使用 `barcode_business_key`。
|
||||
- `product_business_key` 用于 SPU/型号归组;`sku_business_key` 用于 SKU 编码兼容关联。
|
||||
- 资产资料字段来自 `ProductSkuAssetProfile`;`assetSkuSnapshot` 只保留为内部兼容快照,不作为该视图主读来源。
|
||||
|
||||
## 4. 正式素材:`external.product_assets_v1`
|
||||
@ -88,6 +99,7 @@
|
||||
| `sku_id` | SKU ID,内部追踪用技术键,不作为外部业务身份 |
|
||||
| `product_business_key` | 外部稳定产品业务键,规则为正式产品型号/SPU 的规范化大写值 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键,规则为 SKU 编码的规范化大写值 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
| `asset_url` | 可访问素材 URL |
|
||||
| `file_name` | 文件名,当前为空,后续可补 |
|
||||
| `mime_type` | MIME 类型,当前为空,后续可补 |
|
||||
@ -96,7 +108,7 @@
|
||||
注意:
|
||||
|
||||
- 该视图只输出 URL,不输出磁盘路径。
|
||||
- 外部系统关联素材时必须优先使用 `product_business_key / sku_business_key`,`product_id / sku_id` 仅用于内部追踪。
|
||||
- 外部系统关联 SKU 素材时优先使用 `barcode_business_key`,`product_business_key / sku_business_key` 用于归组和兼容关联,`product_id / sku_id` 仅用于内部追踪。
|
||||
- 后续如需要签名 URL,应在 API 层做,不直接改原表。
|
||||
|
||||
## 5. 正式 BOM 摘要:`external.product_bom_v1`
|
||||
@ -112,6 +124,7 @@
|
||||
| `sku_id` | SKU ID,可为空,内部追踪用技术键,不作为外部业务身份 |
|
||||
| `product_business_key` | 外部稳定产品业务键,规则为正式产品型号/SPU 的规范化大写值 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键;当 BOM 行不绑定 SKU 时可为空 |
|
||||
| `barcode_business_key` | 外部最小商品身份键;当 BOM 行不绑定 SKU 时可为空 |
|
||||
| `item_id` | 物料行 ID |
|
||||
| `item_code` | 物料编码 |
|
||||
| `item_name` | 物料名称 |
|
||||
@ -126,7 +139,7 @@
|
||||
注意:
|
||||
|
||||
- 第一版重点是结构稳定,成本口径后续单独治理。
|
||||
- 外部系统关联 BOM 时必须优先使用 `product_business_key / sku_business_key`,`product_id / sku_id` 仅用于内部追踪。
|
||||
- 外部系统关联 SKU 级 BOM 时优先使用 `barcode_business_key`;产品级 BOM 行可使用 `product_business_key` 归组,`product_id / sku_id` 仅用于内部追踪。
|
||||
- 孔位数据暂不进入普通 BOM 视图,避免把制造/3D 数据混进商品资料。
|
||||
|
||||
## 6. 非集成开品主信息:`external.intake_products_v1`
|
||||
@ -155,6 +168,10 @@
|
||||
| `visual_planning` | 空间/视觉负责人 |
|
||||
| `structure_designer` | 结构设计负责人 |
|
||||
| `purchase_owner` | 采购负责人结构化字段 |
|
||||
| `appearance_designer` | 外观设计师明确字段;来自非集成产品基础列 |
|
||||
| `spu_owner` | SPU 负责人明确字段;优先来自结构化 profile,旧运营负责人只作兜底 |
|
||||
| `planning_owner` | 商品企划明确字段;来自结构化 profile |
|
||||
| `space_designer` | 空间设计师明确字段;优先来自结构化 profile 的空间/视觉负责人 |
|
||||
| `product_development_kind` | 开发类型 |
|
||||
| `goods_type` | 商品类型 |
|
||||
| `material_type` | 材料类型 |
|
||||
@ -164,11 +181,269 @@
|
||||
|
||||
- 该视图过滤已软删除记录。
|
||||
- 产品名称、图片、品类、年份、状态、开发类型、货品类型、材质类型、生产类型和结构化负责人字段来自 `IntakeNonIntegratedProductProfile`,不再直接读取 `payload`。
|
||||
- `designer / operator / purchaser` 是兼容列,优先输出 profile 中的结构设计、商品企划、采购负责人;旧产品行列只作为兼容兜底。
|
||||
- `designer / operator / purchaser` 是兼容列,优先输出 profile 中的结构设计、商品企划、采购负责人;旧产品行列只作为兼容兜底。新接入优先读取 `appearance_designer / structure_designer / spu_owner / planning_owner / space_designer / purchase_owner` 这些明确字段。
|
||||
- `intake_product_id` 只用于内部追踪和问题定位;外部系统同步、去重和幂等更新必须优先使用 `product_business_key`。
|
||||
- 不开放 `visibility`、`departmentId`、`teamId`、审批状态、内部确认状态。
|
||||
- `external.intake_products_ready_v1` 使用同一字段结构,但只保留产品名称、品类、开发类型和 `product_business_key` 都已补齐的记录。外部系统做稳定同步时优先读取 ready 视图;`intake_products_v1` 用于治理对账和人工补齐。
|
||||
|
||||
## 6A. 商品目录聚合宽表:`external.product_catalog_wide_v1`
|
||||
|
||||
核心用途:给外部系统一张表同步商品目录。正式 `Product / ProductSKU` 已存在时优先输出正式 SKU 行;正式商品尚未落库但 non-integrated 已进入 ready 状态时,补输出可消费的开发阶段 SKU 行。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `product_business_key` | 外部稳定产品业务键 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键 |
|
||||
| `product_model_number` | 产品型号/SPU;正式行来自 `Product.modelNumber`,non-integrated ready 行来自 `spu` |
|
||||
| `product_name` | 产品名称 |
|
||||
| `product_category_name` | 产品分类 |
|
||||
| `sku_code` | SKU 编码 |
|
||||
| `sku_name` | SKU 名称 |
|
||||
| `price` | 售价;non-integrated ready 行暂为空 |
|
||||
| `cost_price` | 成本价;non-integrated ready 行暂为空 |
|
||||
| `stock` | 库存;non-integrated ready 行默认 `0` |
|
||||
| `record_status` | 外部统一状态:`ACTIVE` / `INACTIVE`;non-integrated ready 行默认 `ACTIVE` |
|
||||
| `model_url` | SKU 或产品模型 URL |
|
||||
| `image_url` | SKU 主图 URL |
|
||||
| `image_urls` | SKU 图片 URL 数组 |
|
||||
| `asset_family_code` | 来源资产族编码 |
|
||||
| `spec_code` | 来源规格款编码 |
|
||||
| `barcode` | 条码 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
| `size_json` | 尺寸 JSON |
|
||||
| `color` | 颜色/花色 |
|
||||
| `material` | 材质 |
|
||||
| `appearance_designer` | 产品开发明细的外观设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `structure_designer` | 产品开发明细的结构设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `spu_owner` | 产品开发明细的 SPU 负责人;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `planning_owner` | 产品开发明细的商品企划;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `space_designer` | 产品开发明细的空间设计师;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `purchase_owner` | 产品开发明细的采购负责人;开发阶段 ready 行来自产品资料负责人字段,正式商品行暂为空 |
|
||||
| `supplier_name` | 产品定厂供应商名称;开发阶段 ready 行来自结构化 SKU 定厂资料,正式商品行暂为空 |
|
||||
| `supplier_code` | 产品定厂供应商编码;开发阶段 ready 行来自结构化 SKU 定厂资料,正式商品行暂为空 |
|
||||
| `factory_warehouse` | 产品定厂发货仓;与库存库位 `warehouse_location_name` 是两个不同业务字段 |
|
||||
| `shipping_method` | 产品定厂发货方式 |
|
||||
| `minimum_order_quantity` | 产品定厂最低起订量 |
|
||||
| `tax_included_unit_price` | 产品定厂含税单价 |
|
||||
| `lead_time` | 产品定厂交期 |
|
||||
| `install_method` | 产品定厂安装方式 |
|
||||
| `jst_product_pushed_at` | 商品资料成功推送聚水潭的时间;未成功推送时为空 |
|
||||
| `is_external_intake` | 是否由外部录入人员创建 |
|
||||
| `supply_chain_archive_eligible` | 是否满足供应链产品全量档案当前入档规则 |
|
||||
| `source_pricing_row_id` | 产品开发核价记录 ID,用于追踪原始业务记录 |
|
||||
| `source_product_id` | 产品开发产品 ID,用于定向增量同步和排查 |
|
||||
| `source_sku_item_id` | 产品开发结构化 SKU 行 ID |
|
||||
| `product_development_created_at` | 产品首次录入产品开发系统的时间,作为档案创建时间口径 |
|
||||
| `product_development_updated_at` | 产品开发系统最近更新时间 |
|
||||
| `archive_spu / archive_sku` | 产品全量档案使用的 SPU、SKU 口径 |
|
||||
| `archive_product_name` | 产品全量档案商品名称,来自条码对应规格名称 |
|
||||
| `archive_image` | 产品开发条码/SKU 图片原值 |
|
||||
| `sales_category_name` | 一级类目,例如桌类、柜类 |
|
||||
| `archive_product_category_name` | 二级类目原值,档案同步时补住宅家具/商业办公/其他前缀 |
|
||||
| `sku_sales_status` | 销售状态原值;档案只接受销售、停产、退市 |
|
||||
| `brand_name` | 产品开发品牌 |
|
||||
| `sample_weight_kg` | 样包测量重量(千克) |
|
||||
| `package_length_cm / package_width_cm / package_height_cm` | 样包长宽高(厘米) |
|
||||
| `package_volume_cm3` | 样包体积(立方厘米) |
|
||||
| `package_set_count` | 套件量;档案转换为非双包、双包至九包 |
|
||||
| `purchase_order_method` | 叫货/采购方式 |
|
||||
| `archive_shipping_method` | 产品全量档案的入仓/代发口径 |
|
||||
| `warehouse_location_code` | 仓库库位编码 |
|
||||
| `warehouse_zone / warehouse_aisle / warehouse_shelf / warehouse_bin` | 库位物理位置 |
|
||||
| `warehouse_location_status` | 库位状态:`ACTIVE` / `INACTIVE` |
|
||||
| `limit_price_applicable` | 是否适用限价;存量商品明细默认 `false`,产品开发明细和模块化产品明细默认 `true` |
|
||||
| `limit_price_confirmed` | 产品限价节点是否已确认;只有适用限价时才有业务意义 |
|
||||
| `limit_price_status` | 限价状态:`NOT_APPLICABLE` / `PENDING` / `CONFIRMED` |
|
||||
| `expected_daily_limit_price` | 期望日常限价;来自结构化产品限价行 |
|
||||
| `expected_limit_gross_margin_rate` | 期望限价毛利率 |
|
||||
| `non_spu_owner_limit_price` | 非 SPU 负责人限价 |
|
||||
| `limit_confirmed_at` | 产品限价确认时间 |
|
||||
| `limit_source_product_business_key` | 限价来源非集成产品业务键 |
|
||||
| `limit_source_sku_business_key` | 限价来源非集成 SKU 业务键 |
|
||||
| `image_asset_count` | SKU 图片素材数量 |
|
||||
| `model_asset_count` | SKU 模型素材数量 |
|
||||
| `thumbnail_asset_count` | SKU 缩略图素材数量 |
|
||||
| `first_model_asset_url` | 第一条模型素材 URL |
|
||||
| `first_thumbnail_asset_url` | 第一条缩略图素材 URL |
|
||||
| `bom_item_count` | BOM 行数 |
|
||||
| `panel_item_count` | 板件行数 |
|
||||
| `hardware_item_count` | 五金行数 |
|
||||
| `material_codes` | BOM 涉及材质编码数组 |
|
||||
| `material_names` | BOM 涉及材质名称数组 |
|
||||
| `packaged_item_count` | 已关联正式 SKU 的包装库存项数量 |
|
||||
| `packaged_stock_quantity` | 包装库存项库存合计 |
|
||||
| `quality_record_count` | SKU 质检记录数 |
|
||||
| `warehouse_quality_record_count` | 仓库质检记录数 |
|
||||
| `first_batch_quality_record_count` | 首批质检记录数 |
|
||||
| `latest_quality_record_key` | 最新质检记录业务键 |
|
||||
| `latest_quality_source_type` | 最新质检来源:`WAREHOUSE` / `FIRST_BATCH` |
|
||||
| `latest_quality_batch_no` | 最新质检批次号 |
|
||||
| `latest_quality_inspected_at` | 最新质检时间 |
|
||||
| `latest_quality_conclusion` | 最新质检结论 |
|
||||
| `latest_quality_status` | 最新质检状态 |
|
||||
| `created_at` | SKU 创建时间 |
|
||||
| `updated_at` | SKU 更新时间 |
|
||||
|
||||
注意:
|
||||
|
||||
- 产品全量档案只读取本宽表中 `supply_chain_archive_eligible = true` 的记录,不再由同步程序直接关联产品开发业务表或解析 `payload`。
|
||||
- 当前入档规则为:产品开发系统创建、商品资料已成功推送聚水潭、创建人不是外部录入人员。
|
||||
- 同一条码即使最终目录行优先采用正式商品,产品开发的入档字段仍按条码附加在该行末尾,避免来源标记被正式行覆盖。
|
||||
|
||||
- 该视图保持一条码一行,适合外部系统做商品目录同步;同一 SKU 下多个条码会输出多行。
|
||||
- 如果外部系统只想读一张表,优先使用该宽表;它已经覆盖正式 SKU 与可消费的 non-integrated ready SKU,以及产品资料、库存库位摘要、包装库存摘要和最新质检摘要。
|
||||
- 需要逐条素材或逐条 BOM 时,仍读取 `external.product_assets_v1 / external.product_bom_v1`;这两张明细视图是否对外授权,以当前 `external_core_reader` 白名单为准。
|
||||
- 所有 URL 字段都应是完整系统入口;相对的 `/api/...` 或 `/materials/...` 路径会在外部视图层按 `app.external_asset_base_url` 补全,未设置时默认补到 `https://cp.jiansun.vip`。
|
||||
- `appearance_designer / structure_designer / spu_owner / planning_owner / space_designer / purchase_owner` 是产品开发明细页红框负责人字段的外部同步口径;它们只对 non-integrated ready 行有明确来源,正式商品行没有开发阶段上下文时保持空值。
|
||||
- `supplier_name / supplier_code / factory_warehouse / shipping_method / minimum_order_quantity / tax_included_unit_price / lead_time / install_method` 来自结构化非集成 SKU 定厂资料;外部视图不读取 `payload.factoryRows`。其中 `factory_warehouse` 是定厂发货仓,不得与库存库位字段混用。
|
||||
- 需要逐条包装库存或质检明细时,读取 `external.packaged_items_v1 / external.sku_quality_inspections_v1`。
|
||||
- 限价口径:`存量商品明细` 的产品一般没有限价,宽表会输出 `limit_price_status = NOT_APPLICABLE`;`产品开发明细`、`模块化产品明细` 到 `产品限价` 节点确认后,限价视为正式口径并输出为 `CONFIRMED`。
|
||||
- `product_id / sku_id` 只用于内部追踪;正式行会输出正式产品/SKU ID,non-integrated ready 行会输出 `intake_product_id / intake_sku_item_id`;外部主同步、去重和幂等更新必须使用 `barcode_business_key`。
|
||||
- `product_business_key` 用于 SPU/型号归组;宽表里统一按 `SPU / sku_code` 的规范化值输出,不沿用 ready 视图里的 `brand:spu(:sku)` 组合键口径。
|
||||
- 宽表限价优先按 `barcode_business_key` 匹配,只有条码缺失时才回退到 `sku_business_key / sku_code`。
|
||||
- non-integrated ready 行尚未正式化时,正式 BOM、包装库存、仓库库位和 SKU 质检摘要字段默认仍为 `0 / NULL / []`;这些字段只有正式链路落库后才会有正式真值。
|
||||
|
||||
## 6A-1. 非集成产品限价:`external.intake_product_limits_v1`
|
||||
|
||||
核心用途:给外部系统读取非集成产品限价的结构化白名单。该视图一 SKU 一行,不开放整段 `payload`。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `product_business_key` | 非集成产品业务键,规则为 `品牌:SPU` |
|
||||
| `sku_business_key` | 非集成 SKU 业务键,规则为 `品牌:SPU:SKU编码` |
|
||||
| `product_development_kind` | 开发类型:`product` 为产品开发明细,`series` 为模块化产品明细 |
|
||||
| `process_template` | 流程模板;`存量商品` 表示存量商品明细 |
|
||||
| `sku_code` | SKU 编码 |
|
||||
| `barcode` | 条码 |
|
||||
| `limit_price_applicable` | 是否适用限价 |
|
||||
| `limit_price_confirmed` | 产品限价节点是否确认 |
|
||||
| `limit_price_status` | `NOT_APPLICABLE` / `PENDING` / `CONFIRMED` |
|
||||
| `expected_daily_limit_price` | 期望日常限价 |
|
||||
| `expected_limit_gross_margin_rate` | 期望限价毛利率 |
|
||||
| `non_spu_owner_limit_price` | 非 SPU 负责人限价 |
|
||||
| `limit_confirmed_at` | 限价确认时间 |
|
||||
|
||||
注意:
|
||||
|
||||
- `存量商品明细` 默认限价不适用,即使历史工作区存在临时限价行,也不作为外部正式限价口径。
|
||||
- `产品开发明细`、`模块化产品明细` 在 `产品限价` 节点确认后,限价字段就是正式口径。
|
||||
- 外部主同步优先读 `external.product_catalog_wide_v1`;需要核对非集成限价来源时再读本视图。
|
||||
|
||||
## 6B. 仓库库位:`external.warehouse_locations_v1`
|
||||
|
||||
核心用途:给外部系统读取库位资料。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `warehouse_location_id` | 库位 ID,内部追踪用技术键 |
|
||||
| `warehouse_location_code` | 库位编码 |
|
||||
| `warehouse_location_name` | 库位名称 |
|
||||
| `zone` | 分区 |
|
||||
| `aisle` | 巷道 |
|
||||
| `shelf` | 货架 |
|
||||
| `bin` | 箱位 |
|
||||
| `is_active` | 是否启用 |
|
||||
| `record_status` | `ACTIVE` / `INACTIVE` |
|
||||
| `created_at` | 创建时间 |
|
||||
| `updated_at` | 更新时间 |
|
||||
|
||||
注意:
|
||||
|
||||
- 外部系统同步库位时优先使用 `warehouse_location_code`。
|
||||
- 该视图不输出库位备注,避免把内部管理说明带到外部系统。
|
||||
|
||||
## 6C. SKU 仓库库存:`external.sku_warehouse_stock_v1`
|
||||
|
||||
核心用途:给外部系统读取正式 SKU 的库存数量和库位。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `product_business_key` | 外部稳定产品业务键 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键 |
|
||||
| `sku_code` | SKU 编码 |
|
||||
| `sku_name` | SKU 名称 |
|
||||
| `barcode` | 产品条码 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
| `stock_quantity` | SKU 库存数量 |
|
||||
| `sku_record_status` | SKU 状态 |
|
||||
| `warehouse_location_code` | 库位编码 |
|
||||
| `warehouse_location_name` | 库位名称 |
|
||||
| `warehouse_zone / warehouse_aisle / warehouse_shelf / warehouse_bin` | 库位物理位置 |
|
||||
| `warehouse_location_status` | 库位状态 |
|
||||
|
||||
注意:
|
||||
|
||||
- 该视图一 SKU 一行,来源是正式 `ProductSKU.stock` 和 `ProductSKU.warehouseLocationId`。
|
||||
- 外部系统关联库存时优先使用 `barcode_business_key`,不要使用 `sku_id` 做业务主键;`sku_business_key` 只做兼容关联。
|
||||
|
||||
## 6D. 包装库存项:`external.packaged_items_v1`
|
||||
|
||||
核心用途:给外部系统读取已关联正式 SKU 的包装库存项。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `packaged_item_id` | 包装库存项 ID,内部追踪用技术键 |
|
||||
| `packaged_item_code` | 包装库存项编码 |
|
||||
| `packaged_item_name` | 包装库存项名称 |
|
||||
| `product_business_key` | 外部稳定产品业务键 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键 |
|
||||
| `sku_code` | SKU 编码 |
|
||||
| `barcode` | 产品条码 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
| `quantity_per_pack` | 单包数量 |
|
||||
| `stock_quantity` | 包装库存数量 |
|
||||
| `length / width / height` | 包装尺寸 |
|
||||
| `weight` | 包装重量 |
|
||||
| `status` | 包装项状态 |
|
||||
| `warehouse_location_code` | 库位编码 |
|
||||
|
||||
注意:
|
||||
|
||||
- 该视图只输出已关联正式 SKU 的 `PackagedItem`;准 SKU 草稿包装过程数据不在本视图中开放。
|
||||
- 不输出 `PackagedItem.payload`,避免把包装过程 JSON 和成本草稿整段暴露给外部系统。
|
||||
|
||||
## 6E. SKU 质检记录:`external.sku_quality_inspections_v1`
|
||||
|
||||
核心用途:给外部系统读取 SKU 级仓库质检和首批质检记录。
|
||||
|
||||
关键字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `quality_inspection_id` | 质检记录 ID,内部追踪用技术键 |
|
||||
| `record_key` | 质检记录业务键 |
|
||||
| `source_type` | `WAREHOUSE` / `FIRST_BATCH` |
|
||||
| `source_label` | 质检来源中文标签 |
|
||||
| `batch_no` | 批次号 |
|
||||
| `inspected_at` | 质检时间 |
|
||||
| `inspector_name` | 质检人 |
|
||||
| `conclusion` | 质检结论 |
|
||||
| `detail` | 质检说明 |
|
||||
| `inspected_quantity` | 检验数量 |
|
||||
| `qualified_quantity` | 合格数量 |
|
||||
| `qualified_rate` | 合格率 |
|
||||
| `status` | 质检记录状态 |
|
||||
| `product_business_key` | 外部稳定产品业务键 |
|
||||
| `sku_business_key` | 外部稳定 SKU 业务键 |
|
||||
| `sku_code` | SKU 编码 |
|
||||
| `barcode` | 产品条码 |
|
||||
| `barcode_business_key` | 外部最小商品身份键,规则为产品条码的规范化大写值 |
|
||||
|
||||
注意:
|
||||
|
||||
- 外部系统关联质检记录时优先使用 `barcode_business_key`,记录自身去重使用 `record_key`。
|
||||
- 不输出 `metrics / attachments` 内部 JSON;如果外部系统后续确实需要附件,应单独设计附件白名单视图或只读 API。
|
||||
|
||||
## 7. 非集成 SKU 明细:`external.intake_sku_items_v1`
|
||||
|
||||
核心用途:给外部系统读取非集成开品中的 SKU 明细。
|
||||
|
||||
@ -14,6 +14,13 @@
|
||||
|
||||
统一入口是 `src/lib/storage.ts`。
|
||||
|
||||
当前读取真相已明确拆成两层:
|
||||
|
||||
- 稳定客户端入口:数据库和 DTO 只保存系统稳定地址,例如登录态 `/api/storage/object?key=...`。
|
||||
- 服务端直读地址:由 storage adapter 按当前 provider 生成临时可访问地址,例如签名 CDN URL。
|
||||
|
||||
业务页面、服务层和 DTO 只依赖“稳定入口”,不写死 bucket、CDN 域名或七牛 SDK 细节。
|
||||
|
||||
当前支持:
|
||||
|
||||
- `STORAGE_PROVIDER=s3`:MinIO / S3 兼容对象存储。
|
||||
@ -48,6 +55,8 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
|
||||
|
||||
当前 `sucaiku-js` 空间为私有空间。七牛测试域名不支持 HTTPS,测试阶段可先使用 `QINIU_PUBLIC_DOMAIN=http://tevepd6od.hd-bkt.clouddn.com`,并设置 `QINIU_USE_HTTPS=false`。后续换正式 HTTPS 域名时只替换 `QINIU_PUBLIC_DOMAIN` 并恢复 `QINIU_USE_HTTPS=true`,数据库内的稳定系统访问地址不需要批量迁移。
|
||||
|
||||
七牛官方对测试域名的口径是:测试域名只供测试使用,不在 SLA 保障范围内,有效期有限;绑定自定义域名并设为默认域名后,只影响后续新生成外链,历史测试域名链接不会自动替换。因此测试域名、正式 CDN 域名、历史 S3 域名都不能作为数据库长期真相。
|
||||
|
||||
HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应使用已绑定证书的 HTTPS CDN 域名,避免浏览器混合内容拦截和传输层明文风险。
|
||||
|
||||
私有空间不能直接把短期签名 URL 保存成长期资料字段。统一存储入口会在私有模式下保存 `/api/storage/object?key=...` 这类系统内稳定地址;读取时由后端受登录保护的接口临时生成七牛下载签名并跳转。
|
||||
@ -58,6 +67,14 @@ HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应
|
||||
- 七牛 HTTP 测试域名:系统稳定读取接口不把 HTTP 签名 URL 直接暴露给浏览器,而是由后端代理读取并返回,避免 HTTPS 页面出现混合内容拦截。
|
||||
- 后端代理只解决浏览器侧混合内容和签名 URL 暴露问题;HTTP 测试域名在七牛到应用服务器这一段仍不是加密传输,不能作为正式保密链路长期使用。
|
||||
|
||||
当前稳定读取入口分三类:
|
||||
|
||||
- 登录态素材:`GET /api/storage/object?key=...`,先走应用鉴权,再 `307` 到短时签名 CDN URL。
|
||||
- 分享态素材:`GET /api/share/[token]/object?key=...&sku=...`,先校验分享 token 和对象范围,再 `307` 到短时签名 CDN URL。
|
||||
- 审批态素材:审批 DTO 继续输出稳定系统入口,最终仍通过登录态读取链路完成鉴权和 `307` 跳转。
|
||||
|
||||
换 bucket、换 CDN 域名、甚至未来换 provider 时,只允许改环境变量和 `src/lib/storage.ts` 里的 storage adapter。业务页、分享页、审批页和服务层不应该重写链接拼接逻辑。
|
||||
|
||||
七牛侧如果保持“无自定义跨域规则”,空间默认允许跨域访问;如果后续新增自定义 CORS 规则,必须允许本系统正式域名和本地调试域名读取模型、贴图和缩略图,并暴露 `Content-Range`、`Accept-Ranges` 等范围请求相关响应头,否则浏览器中的 3D/WebGL 资源加载可能失败。
|
||||
|
||||
## 后续迁移规则
|
||||
@ -80,3 +97,9 @@ HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应
|
||||
3. 历史 demo / seed 中的本地路径样本。
|
||||
|
||||
每次迁移后必须跑主数据巡检,确认没有新增“文件系统读取 + 主数据写入”的反向造数风险。
|
||||
|
||||
外部只读契约额外要求:
|
||||
|
||||
- `external.*_v1` 视图不能输出七牛测试域名、`data:`、`file:`、`localhost`、内网 IP、相对路径、内部 API 路径、`public/` 或 `uploads/` 这类不可长期外部读取的素材地址。
|
||||
- 外部系统如果需要读取素材,必须拿到正式 HTTPS 可访问 URL,或走专门的后端下载/签名 API;不能依赖本系统登录态 API 路径。
|
||||
- `scripts/sql/check-external-readonly-contract.sql` 和 `npm run db:verify-master-data` 会把上述风险作为外部契约问题检查出来。
|
||||
|
||||
363
furniture-3d-system/docs/架构设计/店铺账号管理取数与筛选逻辑.md
Normal file
363
furniture-3d-system/docs/架构设计/店铺账号管理取数与筛选逻辑.md
Normal file
@ -0,0 +1,363 @@
|
||||
# 店铺账号管理取数与筛选逻辑
|
||||
|
||||
> 用途:给需要接入店铺账号管理数据的开发人员使用。本文描述当前代码和数据库的真实口径。
|
||||
>
|
||||
> 重要:本文不包含数据库连接串、账号、密码、加密密钥或任何业务明文数据。当前数据由本系统人工维护,不直接从外部数据库同步。
|
||||
|
||||
## 1. 数据源与责任边界
|
||||
|
||||
- **权威数据源**:本系统使用 `DATABASE_URL` 指向的 PostgreSQL 数据库,数据通过本系统店铺账号管理页面/API 人工新增、编辑、导入和维护。
|
||||
- **不属于数据源的内容**:聚水潭、京东、京喜等外部系统目前不是本模块的自动同步来源;不能假设外部系统中存在同一份实时数据。
|
||||
- **推荐接入方式**:优先调用本系统鉴权 API。若确实需要数据库直读,应由数据库管理员单独创建只读账号或只读视图,并按本文的字段白名单开放;不要复用应用账号,也不要把应用数据库密码或加密密钥交给外部系统。
|
||||
- **人工维护原则**:不要直接写入业务表绕过 Service。直接写入会跳过页面权限、字段权限、密文加密、唯一键校验和操作日志。
|
||||
|
||||
### 1.1 代码与迁移依据
|
||||
|
||||
当前数据契约以以下文件为准:
|
||||
|
||||
- Prisma 模型:`prisma/schema.prisma` 中 `StoreAccount`、`StoreAccountStoreProfile`、`StoreAccountChangeLog`、`StoreAccountFieldPermission`、`SupplyChainPermissionOverride`。
|
||||
- 建表迁移:`prisma/migrations/20260826120000_add_store_account_management/migration.sql`。
|
||||
- 店铺主体和银行卡字段、平台资料表迁移:`prisma/migrations/20260828100000_extend_store_account_profiles/migration.sql`。
|
||||
- 列表筛选规则:`src/server/repositories/store-account-management.repository.ts`。
|
||||
- 业务校验、脱敏和日志:`src/server/services/store-account-management.service.ts`。
|
||||
- 导出字段:`src/server/services/store-account-management-export.service.ts`。
|
||||
|
||||
## 2. 物理表清单与关联关系
|
||||
|
||||
PostgreSQL 物理表名使用双引号保留大小写,取数时应写成下表中的名称。
|
||||
|
||||
| 物理表 | 用途 | 主要关联键 | 是否主业务数据 |
|
||||
| --- | --- | --- | --- |
|
||||
| `"StoreAccount"` | 店铺账号主记录,一行代表一个店铺下的一种账号 | `id`;业务唯一键为 `storeName + accountType + account` | 是 |
|
||||
| `"StoreAccountStoreProfile"` | 京东/京喜店铺资料,一行代表一个店铺的一个平台资料 | 逻辑关联 `storeName`,平台由 `platform` 区分 | 是(扩展资料) |
|
||||
| `"StoreAccountChangeLog"` | 新增、编辑、导入、查看敏感字段、资料维护的审计日志 | `storeAccountId` 可为空;资料日志用 `storeName` | 否(审计) |
|
||||
| `"StoreAccountFieldPermission"` | 店铺账号字段对业务类型/个人的可见、只读、可编辑配置 | `fieldKey + subjectKey` 唯一 | 否(权限配置) |
|
||||
| `"SupplyChainPermissionOverride"` | 页面级的查看、下载、编辑权限 | `targetKey + subjectKey + action` 唯一;本模块 `targetKey = 'storeAccountManagement'` | 否(权限配置) |
|
||||
|
||||
`StoreAccountStoreProfile` 与 `StoreAccount` 没有数据库外键,应用层按 `storeName` 关联。因此外部查询时要注意:资料表可能没有对应账号行,也可能一个店铺有多个账号行;不要用 `id` 去连接资料表。
|
||||
|
||||
## 3. `"StoreAccount"` 字段字典
|
||||
|
||||
| 字段 | PostgreSQL 类型/约束 | 业务含义 | 空值与敏感性 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT`,主键 | 账号记录 ID | 系统生成,不为空 |
|
||||
| `storeName` | `TEXT NOT NULL` | 店铺名称,也是资料表的逻辑关联键 | 不应为空;支持多选筛选 |
|
||||
| `operationOwner` | `TEXT NOT NULL DEFAULT ''` | 店铺运营负责人 | 空字符串表示未维护 |
|
||||
| `operationDepartmentHead` | `TEXT NOT NULL DEFAULT ''` | 店铺运营部门负责人 | 空字符串表示未维护 |
|
||||
| `operationDirector` | `TEXT NOT NULL DEFAULT ''` | 店铺运营总监 | 空字符串表示未维护 |
|
||||
| `profitCenter` | `TEXT NOT NULL DEFAULT ''` | 店铺所属利润中心 | 空字符串或:`运营中心A`、`运营中心B`、`运营中心C`、`运营中心G` |
|
||||
| `subject` | `TEXT NOT NULL DEFAULT ''` | 店铺主体 | 空字符串表示未维护 |
|
||||
| `accountType` | `TEXT NOT NULL` | 账号类型 | 正常值 `MAIN`(主账号)、`SUB`(子账号);不完整导入允许空字符串或 `/` |
|
||||
| `account` | `TEXT NOT NULL` | 登录账号 | 新增时必填 |
|
||||
| `passwordCiphertext` | `TEXT NOT NULL DEFAULT ''` | 密码密文 | 绝不作为普通取数字段返回;空字符串表示未设置 |
|
||||
| `boundBankAccountNumberCiphertext` | `TEXT NOT NULL DEFAULT ''` | 店铺绑定银行卡号密文 | 与密码同等级敏感;空字符串表示未设置 |
|
||||
| `boundPhone` | `TEXT NOT NULL DEFAULT ''` | 绑定手机号 | 空字符串表示未维护 |
|
||||
| `verificationContact` | `TEXT NOT NULL DEFAULT ''` | 手机号验证码对接人 | 空字符串表示未维护 |
|
||||
| `jstOrderService` | `TEXT NOT NULL DEFAULT ''` | 聚水潭订单服务号 | 空字符串表示未维护 |
|
||||
| `accountGroupName` | `TEXT NOT NULL DEFAULT ''` | 账号分组名称 | 空字符串表示未维护 |
|
||||
| `createdById` | `TEXT` 可空 | 首次创建人 ID | 系统审计字段 |
|
||||
| `updatedById` | `TEXT` 可空 | 最近修改人 ID | 系统审计字段 |
|
||||
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 账号记录创建时间 | 系统生成;列表按此字段倒序 |
|
||||
| `updatedAt` | `TIMESTAMP(3) NOT NULL` | 账号记录最近修改时间 | 系统自动更新时间 |
|
||||
|
||||
约束和索引:
|
||||
|
||||
- 唯一约束:`("storeName", "accountType", "account")`,导入按这三个字段执行 upsert。
|
||||
- 索引:`storeName`、`account`、`accountType`、`updatedAt`。
|
||||
- 本表没有软删除字段;空白字段是空字符串,不使用“系统默认值”兜底。
|
||||
|
||||
## 4. `"StoreAccountStoreProfile"` 字段字典
|
||||
|
||||
该表用于“京东/京喜店铺资料”次页面的后续维护,不要求在账号主表导入时同时填写。
|
||||
|
||||
| 字段 | PostgreSQL 类型/约束 | 业务含义 | 取值规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT`,主键 | 店铺平台资料 ID | 系统生成 |
|
||||
| `storeName` | `TEXT NOT NULL` | 店铺名称 | 与 `StoreAccount.storeName` 逻辑关联 |
|
||||
| `platform` | `TEXT NOT NULL` | 平台代码 | `JD`=京东,`JX`=京喜 |
|
||||
| `merchantNumber` | `TEXT NOT NULL DEFAULT ''` | 商户号(钱包商户号) | 空字符串表示未维护 |
|
||||
| `secondaryMerchantNumber` | `TEXT NOT NULL DEFAULT ''` | 二级商户号 | 空字符串表示未维护 |
|
||||
| `merchantStoreId` | `TEXT NOT NULL DEFAULT ''` | 商家 ID(钱包店铺号) | 空字符串表示未维护 |
|
||||
| `companyId` | `TEXT NOT NULL DEFAULT ''` | 公司 ID(京麦店铺 ID) | 空字符串表示未维护 |
|
||||
| `createdById` | `TEXT` 可空 | 首次创建人 ID | 系统审计字段 |
|
||||
| `updatedById` | `TEXT` 可空 | 最近修改人 ID | 系统审计字段 |
|
||||
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 资料创建时间 | 系统生成 |
|
||||
| `updatedAt` | `TIMESTAMP(3) NOT NULL` | 资料最近修改时间 | 系统自动更新时间 |
|
||||
|
||||
约束和平台规则:
|
||||
|
||||
- 唯一约束:`("storeName", "platform")`,同一店铺同一平台只有一条资料。
|
||||
- 店铺名以 `京东-` 开头时只能使用 `JD`;以 `京喜-` 开头时只能使用 `JX`。
|
||||
- 非 `京东-` 或 `京喜-` 开头的店铺不支持店铺资料页面/API,服务端会拒绝读取或保存。
|
||||
- 资料字段通过 `storeName` 关联账号主表,导出时会按平台展开为京东四列和京喜四列。
|
||||
|
||||
## 5. `"StoreAccountChangeLog"` 字段与日志格式
|
||||
|
||||
| 字段 | PostgreSQL 类型/约束 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `id` | `TEXT`,主键 | 日志 ID |
|
||||
| `storeAccountId` | `TEXT` 可空 | 账号记录 ID;店铺资料变更时为空 |
|
||||
| `storeName` | `TEXT NOT NULL` | 店铺名称 |
|
||||
| `operation` | `TEXT NOT NULL` | 操作类型 |
|
||||
| `actorId` | `TEXT` 可空 | 操作人 ID |
|
||||
| `actorName` | `TEXT NOT NULL` | 操作人名称 |
|
||||
| `changes` | `JSONB NOT NULL` | 字段变更明细数组 |
|
||||
| `createdAt` | `TIMESTAMP(3) NOT NULL` | 日志时间 |
|
||||
|
||||
当前 `operation` 代码:
|
||||
|
||||
| operation | 含义 |
|
||||
| --- | --- |
|
||||
| `CREATE` | 新增账号 |
|
||||
| `UPDATE` | 编辑账号 |
|
||||
| `IMPORT_CREATE` | 导入新增 |
|
||||
| `IMPORT_UPDATE` | 导入更新 |
|
||||
| `BATCH_UPDATE` | 批量修改 |
|
||||
| `VIEW_PASSWORD` | 查看密码 |
|
||||
| `VIEW_BANK_ACCOUNT` | 查看银行卡号 |
|
||||
| `PROFILE_UPDATE` | 修改京东/京喜店铺资料 |
|
||||
|
||||
`changes` 的标准元素为:
|
||||
|
||||
```json
|
||||
{
|
||||
"field": "operationOwner",
|
||||
"previousValue": "原值",
|
||||
"nextValue": "新值"
|
||||
}
|
||||
```
|
||||
|
||||
特殊字段和资料字段:
|
||||
|
||||
- `password`、`boundBankAccountNumber` 只记录“已设置/未设置”,不写入明文或密文。
|
||||
- 查看敏感字段时记录 `nextValue: "已查看"`。
|
||||
- 平台资料字段使用 `JD.merchantNumber`、`JX.companyId` 这样的字段名。
|
||||
- API 返回日志前还会根据当前调用人的字段权限过滤隐藏字段。
|
||||
|
||||
## 6. `"StoreAccountFieldPermission"` 字段字典
|
||||
|
||||
| 字段 | PostgreSQL 类型/约束 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `id` | `TEXT`,主键 | 权限记录 ID |
|
||||
| `fieldKey` | `TEXT NOT NULL` | 店铺账号字段 key,如 `account`、`password`、`boundBankAccountNumber` |
|
||||
| `subjectKey` | `TEXT NOT NULL` | 业务类型或个人主体 |
|
||||
| `visibility` | `TEXT NOT NULL DEFAULT 'READONLY'` | `HIDDEN`、`READONLY` 或 `EDITABLE` |
|
||||
| `updatedById` | `TEXT` 可空 | 最后配置人 ID |
|
||||
| `createdAt` / `updatedAt` | `TIMESTAMP(3)` | 权限记录时间 |
|
||||
|
||||
当前可配置的业务类型 `subjectKey`:
|
||||
|
||||
`design`(设计)、`planning`(企划)、`brandCenter`(策划)、`purchase`(采购)、`merchandiser`(采购-跟单)、`pricing`(采购-核价)、`quality`(采购-质检)、`operation`(运营)、`warehouse`(仓库)、`customerService`(客服)、`management`(经管)、`viewer`(普通用户)。
|
||||
|
||||
个人例外使用 `user@<用户ID>`。唯一约束为 `("fieldKey", "subjectKey")`。`createdAt`、`updatedAt` 是系统字段,不能通过字段权限配置为可编辑。
|
||||
|
||||
## 7. 页面级权限表
|
||||
|
||||
`"SupplyChainPermissionOverride"` 的 `targetKey` 固定为 `storeAccountManagement`,`action` 为 `view`、`download`、`edit`。
|
||||
|
||||
| 场景 | 当前规则 |
|
||||
| --- | --- |
|
||||
| 未登录 | 所有店铺账号接口返回 401 |
|
||||
| 页面查看 | 需要工作台可见且 `storeAccountManagement:view` 生效 |
|
||||
| 新增、编辑、导入、批量修改 | 需要页面 `edit` |
|
||||
| 导出全部或已选 | 需要页面可见,且调用人是管理员或经管 |
|
||||
| 配置字段权限 | 需要页面可见,且调用人是管理员或经管 |
|
||||
| 密码/银行卡查看 | 还要通过对应字段权限;服务端单独记录查看日志 |
|
||||
|
||||
字段可见性解析顺序:
|
||||
|
||||
1. 页面没有 `view` 时,所有字段均为 `HIDDEN`。
|
||||
2. 先取页面编辑能力形成基础值:系统时间字段只读,密码和银行卡默认隐藏,普通字段按页面编辑能力为可编辑或只读。
|
||||
3. 同一字段的业务类型配置按 `EDITABLE > READONLY > HIDDEN` 合并。
|
||||
4. `user@<用户ID>` 的个人配置覆盖业务类型配置。
|
||||
5. 没有页面编辑权时,即使字段配置为 `EDITABLE` 也会降为只读;系统时间字段永远不会变成可编辑。
|
||||
|
||||
列表接口对隐藏字段返回空字符串;密码和银行卡只返回 `passwordConfigured`、`boundBankAccountNumberConfigured` 布尔值,不返回密文。
|
||||
|
||||
## 8. 列表取数与筛选逻辑
|
||||
|
||||
### 8.1 API 参数
|
||||
|
||||
`GET /api/supply-chain/store-account-management`
|
||||
|
||||
| 参数 | 默认/限制 | 逻辑 |
|
||||
| --- | --- | --- |
|
||||
| `page` | 默认 `1`,最小 `1` | 页码 |
|
||||
| `pageSize` | 默认 `50`,范围 `1..200` | 每页行数;页面常用 50/100/200 |
|
||||
| `search` | 最多 120 字符 | 关键词,可用空格、逗号、中文逗号、分号分隔多个 token |
|
||||
| `storeName` | 最多 120 字符 | 店铺名称多选值,同样支持逗号、中文逗号、分号分隔 |
|
||||
| `accountType` | 空、`MAIN`/`SUB` 或显示名 | 包含匹配;页面显示主账号/子账号 |
|
||||
| `operationOwner` | 空或文本 | 店铺运营负责人包含匹配 |
|
||||
| `accountGroupName` | 空或文本 | 账号分组名称包含匹配 |
|
||||
| `createdAtGranularity` | `year`、`month`、`day` | 创建时间筛选粒度 |
|
||||
| `createdAtValue` | 年 `YYYY`;月 `YYYY-MM`;日 `YYYY-MM-DD` | 与粒度配套的值 |
|
||||
|
||||
### 8.2 条件组合
|
||||
|
||||
- 不同筛选项之间是 **AND**。
|
||||
- `search` 内部每个 token 会对以下字段做 **OR**:`storeName`、`account`、`operationOwner`、`operationDepartmentHead`、`operationDirector`、`profitCenter`、`subject`、`boundPhone`、`verificationContact`、`jstOrderService`、`accountGroupName`。
|
||||
- `search` 不搜索密码、银行卡、创建时间、修改时间和京东/京喜资料字段。
|
||||
- `storeName` 会拆分并去重,使用精确 `IN` 匹配;最多取 500 个店铺名。
|
||||
- `accountType`、`operationOwner`、`accountGroupName` 使用不区分大小写的 `contains` 匹配。
|
||||
- 创建时间使用中国时区 `+08:00` 的半开区间:`[开始时间, 结束时间)`。例如 `2026-08` 是 `2026-08-01 00:00:00+08:00`(含)到 `2026-09-01 00:00:00+08:00`(不含)。
|
||||
|
||||
### 8.3 排序与分页
|
||||
|
||||
固定排序:
|
||||
|
||||
```sql
|
||||
ORDER BY "createdAt" DESC, "id" ASC
|
||||
```
|
||||
|
||||
因此最新创建的记录永远在前面;`id ASC` 只用于创建时间相同时稳定排序。分页使用 `OFFSET (page - 1) * pageSize LIMIT pageSize`,同时查询总数。
|
||||
|
||||
成功响应核心结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"rows": [],
|
||||
"storeNames": ["京东-示例店"],
|
||||
"pageInfo": { "page": 1, "pageSize": 50, "total": 0, "totalPages": 1 },
|
||||
"fieldAccess": {}
|
||||
},
|
||||
"meta": { "requestId": "...", "timestamp": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
`storeNames` 是全表去重后按名称升序返回的店铺列表,用于前端多选;它不受当前页面筛选条件限制。
|
||||
|
||||
## 9. 只读 SQL 示例
|
||||
|
||||
以下 SQL 仅用于说明字段和筛选口径,执行账号必须是单独的只读账号。示例不包含密码密文列。
|
||||
|
||||
### 9.1 最新账号列表
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
"id", "storeName", "operationOwner", "operationDepartmentHead",
|
||||
"operationDirector", "profitCenter", "subject", "accountType",
|
||||
"account", "boundPhone", "verificationContact", "jstOrderService",
|
||||
"accountGroupName", "createdAt", "updatedAt"
|
||||
FROM "StoreAccount"
|
||||
ORDER BY "createdAt" DESC, "id" ASC
|
||||
LIMIT 50 OFFSET 0;
|
||||
```
|
||||
|
||||
### 9.2 店铺 + 账号类型 + 月份筛选
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
"id", "storeName", "accountType", "account", "createdAt", "updatedAt"
|
||||
FROM "StoreAccount"
|
||||
WHERE "storeName" IN ('京东-示例店', '京喜-示例店')
|
||||
AND "accountType" = 'MAIN'
|
||||
AND "createdAt" >= TIMESTAMPTZ '2026-08-01 00:00:00+08:00'
|
||||
AND "createdAt" < TIMESTAMPTZ '2026-09-01 00:00:00+08:00'
|
||||
ORDER BY "createdAt" DESC, "id" ASC;
|
||||
```
|
||||
|
||||
### 9.3 关键词筛选的 SQL 等价写法
|
||||
|
||||
```sql
|
||||
WHERE (
|
||||
"storeName" ILIKE '%' || $1 || '%'
|
||||
OR "account" ILIKE '%' || $1 || '%'
|
||||
OR "operationOwner" ILIKE '%' || $1 || '%'
|
||||
OR "operationDepartmentHead" ILIKE '%' || $1 || '%'
|
||||
OR "operationDirector" ILIKE '%' || $1 || '%'
|
||||
OR "profitCenter" ILIKE '%' || $1 || '%'
|
||||
OR "subject" ILIKE '%' || $1 || '%'
|
||||
OR "boundPhone" ILIKE '%' || $1 || '%'
|
||||
OR "verificationContact" ILIKE '%' || $1 || '%'
|
||||
OR "jstOrderService" ILIKE '%' || $1 || '%'
|
||||
OR "accountGroupName" ILIKE '%' || $1 || '%'
|
||||
)
|
||||
```
|
||||
|
||||
多个 token 时,把每个 token 的上述括号用 `AND` 连接。
|
||||
|
||||
### 9.4 查询平台资料
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
"storeName", "platform", "merchantNumber", "secondaryMerchantNumber",
|
||||
"merchantStoreId", "companyId", "createdAt", "updatedAt"
|
||||
FROM "StoreAccountStoreProfile"
|
||||
WHERE "storeName" = $1
|
||||
AND "platform" IN ('JD', 'JX')
|
||||
ORDER BY "platform" ASC;
|
||||
```
|
||||
|
||||
资料缺失时不要补默认值;没有行或字段为空都表示尚未维护。
|
||||
|
||||
### 9.5 查询审计日志
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
"id", "storeAccountId", "storeName", "operation",
|
||||
"actorId", "actorName", "changes", "createdAt"
|
||||
FROM "StoreAccountChangeLog"
|
||||
WHERE "storeName" = $1
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT 200;
|
||||
```
|
||||
|
||||
## 10. 当前 API 接口清单
|
||||
|
||||
所有接口都要求登录,并返回统一的 `{ success, data/error, meta }` 包装。接口权限还会检查工作台权限和 `storeAccountManagement` 页面权限。
|
||||
|
||||
| 方法 | 路径 | 用途 | 主要限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/supply-chain/store-account-management` | 分页列表、筛选、店铺名称选项 | 页面 `view` |
|
||||
| `POST` | `/api/supply-chain/store-account-management` | 新增账号 | 页面 `edit`;店铺名、账号类型、账号必填 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/{id}` | 单条账号 | 页面 `view` |
|
||||
| `PATCH` | `/api/supply-chain/store-account-management/{id}` | 编辑账号 | 页面 `edit`;只更新有权限的字段 |
|
||||
| `POST` | `/api/supply-chain/store-account-management/import` | Excel/CSV 导入账号 | 页面 `edit`;10 MB、5000 行;NDJSON 进度 |
|
||||
| `POST` | `/api/supply-chain/store-account-management/batch` | 批量修改前预览 | 页面 `edit` + 目标字段 `EDITABLE`;最多 500 条 |
|
||||
| `PATCH` | `/api/supply-chain/store-account-management/batch` | 批量修改 | 同上;密码、银行卡和系统时间不可批量修改 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/export` | 导出全部账号 | 仅管理员或经管;包含明文密码/银行卡,必须严格控制调用方 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/export?scope=selected&ids=id1,id2` | 导出已选账号 | 仅管理员或经管;最多 200 个 ID |
|
||||
| `GET` | `/api/supply-chain/store-account-management/logs?id=...` | 查询单账号日志 | 页面 `view`;返回内容按字段权限过滤 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/logs?storeName=...` | 查询店铺日志 | 页面 `view`;最多返回 200 条 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/{id}/password` | 查看密码明文 | 字段可见;响应不缓存,并写查看日志 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/{id}/bank-account` | 查看银行卡号明文 | 字段可见,并写查看日志 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/permissions` | 读取字段权限配置 | 仅管理员或经管 |
|
||||
| `PUT` | `/api/supply-chain/store-account-management/permissions` | 保存字段权限配置 | 仅管理员或经管;最多 500 条更新 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/profiles?storeName=...` | 读取京东/京喜店铺资料 | 页面 `view`;店铺名前缀必须匹配 |
|
||||
| `PATCH` | `/api/supply-chain/store-account-management/profiles` | 保存平台资料 | 页面 `edit`;平台必须与店铺名前缀一致 |
|
||||
| `GET` | `/api/supply-chain/store-account-management/profiles/template?platform=JD\|JX` | 下载平台资料模板 | 页面 `edit` |
|
||||
| `POST` | `/api/supply-chain/store-account-management/profiles/import` | 导入平台资料 | 页面 `edit`;表单字段 `file` + `platform`,10 MB、5000 行 |
|
||||
|
||||
导出文件为 XLSX,包含账号主表字段、解密后的密码和绑定银行卡号,以及按 `JD`/`JX` 展开的八个店铺资料字段。外部系统若不需要敏感字段,不应调用导出接口,应该调用列表接口或使用只读白名单视图。
|
||||
|
||||
## 11. 导入、更新和空白值规则
|
||||
|
||||
- 主表导入支持中文表头和内部英文 key;未知列会被忽略。
|
||||
- 行数上限 5000,文件上限 10 MB;每一行按 `storeName + accountType + account` upsert,并写入 `IMPORT_CREATE` 或 `IMPORT_UPDATE` 日志。
|
||||
- 普通文本字段导入空白会保存为空字符串,表示“未维护”;不会套用系统默认值。
|
||||
- 密码和绑定银行卡号为空时不会覆盖已有密文;要清除敏感值需由具备相应字段编辑权限的业务流程明确处理,不能通过普通空白导入误清除。
|
||||
- 新增账号时 `storeName`、`accountType`、`account` 必填,`accountType` 正常应为 `MAIN` 或 `SUB`。
|
||||
- 批量修改会先返回所选记录当前值预览,再统一更新;每条实际发生变化的记录写一条 `BATCH_UPDATE` 日志。
|
||||
- 平台资料导入只识别“店铺名称”和四个资料字段,按店铺名和平台 upsert;空白资料仍然允许保存。
|
||||
|
||||
## 12. 敏感字段与外部接入注意事项
|
||||
|
||||
1. `passwordCiphertext` 和 `boundBankAccountNumberCiphertext` 使用 AES-256-GCM 存储,密钥来自服务端 `STORE_ACCOUNT_ENCRYPTION_KEY`(生产必须配置至少 32 个字符)。
|
||||
2. 外部系统不能自行解密,也不能索要或复制该密钥;密文列不应进入普通数据同步任务、日志或缓存。
|
||||
3. 普通列表接口只返回“是否已设置”布尔值;明文只通过受权限保护的查看接口或管理员/经管导出得到,并且会留下审计日志。
|
||||
4. 字段权限和页面权限不是同一层:先满足页面 `view/edit`,再应用业务类型和个人字段权限。
|
||||
5. 直连数据库时建议只开放不敏感白名单字段,至少排除两个 `*Ciphertext` 字段、权限表和日志中的敏感上下文;如需日志,应单独授予审计读取权限。
|
||||
6. 当前没有外部只读视图或公开 Token 契约。外部接入前应由系统负责人确认鉴权方式、限流、字段白名单、数据刷新频率和脱敏要求。
|
||||
|
||||
## 13. 接入前检查清单
|
||||
|
||||
- [ ] 确认读取的是当前 `DATABASE_URL` 对应的 PostgreSQL,而不是历史数据库或外部平台库。
|
||||
- [ ] 使用本文中的双引号物理表名和字段名,确认迁移已执行。
|
||||
- [ ] 明确只读范围:主表、平台资料、日志、权限配置是否都需要。
|
||||
- [ ] 明确是否需要京东/京喜资料,按 `storeName + platform` 读取,不能按账号 `id` 连接。
|
||||
- [ ] 明确筛选时区为中国 `+08:00`,创建时间使用半开区间。
|
||||
- [ ] 按 `createdAt DESC, id ASC` 保持与页面相同的排序。
|
||||
- [ ] 不读取或传播密码、银行卡密文;需要明文时走授权接口并保留审计记录。
|
||||
- [ ] 不直接写表;新增、编辑、导入和权限变更必须走系统 API 或经负责人批准的维护流程。
|
||||
@ -25,7 +25,7 @@
|
||||
- PostgreSQL 是本系统主业务数据库。
|
||||
- 数据库保存业务数据、文件 URL、对象 key、元数据;图片、模型、附件等素材本体不直接存入数据库。
|
||||
- 外部系统只能读专用只读契约层,不能直接读 `ProductSKU`、`IntakeNonIntegratedProduct.payload` 等业务原表。
|
||||
- 外部系统同步、去重和幂等更新必须优先使用契约层里的稳定业务键,不能把内部 `id / product_id / sku_id / intake_product_id / intake_sku_item_id` 当业务身份。
|
||||
- 外部系统主同步、去重和幂等更新必须优先使用 `barcode_business_key`;内部 `id / product_id / sku_id / intake_product_id / intake_sku_item_id` 只能用于问题追踪。
|
||||
- 内部业务表允许随系统演进调整;外部契约必须版本化、稳定、可追溯。
|
||||
- 对外开放字段必须白名单化,默认不开放内部流程、权限、草稿、审批、临时 UI 状态。
|
||||
|
||||
@ -109,7 +109,9 @@
|
||||
|
||||
- `external.product_skus_v1.product_business_key` 来自 `Product.modelNumber` 的规范化大写值。
|
||||
- `external.product_skus_v1.sku_business_key` 来自 `ProductSKU.skuCode` 的规范化大写值。
|
||||
- `external.product_skus_v1.barcode_business_key` 来自产品条码的规范化大写值,是外部系统最小商品身份。
|
||||
- `external.product_assets_v1` 和 `external.product_bom_v1` 也必须携带同一套业务键,避免外部系统为了关联素材或 BOM 被迫使用内部技术 ID。
|
||||
- `external.product_catalog_wide_v1` 在同一业务键口径上聚合正式 SKU,以及已进入 ready 状态但尚未正式落库的 non-integrated SKU;素材数量、BOM 摘要、仓库、质检和限价摘要优先按 `barcode_business_key` 关联。
|
||||
|
||||
### 3.4 非集成开品资料
|
||||
|
||||
@ -131,9 +133,12 @@
|
||||
- `IntakeNonIntegratedSkuItem` 是非集成 SKU 明细的结构化承接表。
|
||||
- `payload.skuItems` 作为历史过程快照保留,用于同步差异巡检,不作为对外主数据来源。
|
||||
- `IntakeNonIntegratedProductProfile` 承接产品名称、主图、品类、销售分类、年份、状态、开发类型、货品类型、材质类型、生产类型、主要负责人等产品头主数据。
|
||||
- `IntakeNonIntegratedProductLimitRow` 承接产品限价行,来源是 `productLimitRows` 的结构化同步副本,不允许外部系统直接读取 `payload` 推断限价。
|
||||
- 产品定厂的供应商、发货仓、发货方式、最低起订量、含税单价、交期和安装方式由 `IntakeNonIntegratedSkuItem` 结构化列承接;`payload.factoryRows` 只保留兼容快照,不作为外部主读来源。
|
||||
- `external.intake_products_v1` 读取 `IntakeNonIntegratedProductProfile`,不再直接从 `payload` / `openingReference` 读取产品头主数据。
|
||||
- `external.intake_products_v1 / external.intake_sku_items_v1` 显式输出稳定业务键:`product_business_key = 品牌:SPU`,`sku_business_key = 品牌:SPU:SKU编码`;ready 视图会要求业务键非空。
|
||||
- 非空 `skuCode / barcode` 由数据库唯一索引兜底。
|
||||
- `external.intake_product_limits_v1` 读取 `IntakeNonIntegratedProductLimitRow`;`存量商品明细` 默认限价不适用,`产品开发明细` 和 `模块化产品明细` 到 `产品限价` 节点确认后才视为正式限价。
|
||||
- `external.intake_products_v1 / external.intake_sku_items_v1` 显式输出稳定业务键:`product_business_key = 品牌:SPU`,`sku_business_key = 品牌:SPU:SKU编码`;这些键用于产品归组和兼容关联。
|
||||
- 非空 `skuCode / barcode` 由数据库唯一索引兜底;外部正式商品最小身份以条码规范化后的 `barcode_business_key` 为准。
|
||||
- 软删除非集成产品时,后端和数据库触发器都会清理结构化 SKU 明细,避免已删除记录占用编码池。
|
||||
|
||||
非集成对外只能做白名单投影,例如:
|
||||
@ -210,6 +215,12 @@ CREATE SCHEMA IF NOT EXISTS external;
|
||||
| 视图 | 用途 |
|
||||
| --- | --- |
|
||||
| `external.product_skus_v1` | 正式产品 SKU 主数据 |
|
||||
| `external.product_catalog_wide_v1` | 一条码一行的商品目录聚合宽表,覆盖正式条码与可消费的 non-integrated ready 条码,以及产品资料、开发阶段负责人、限价、库存库位摘要、包装库存摘要和最新质检摘要 |
|
||||
| `external.intake_product_limits_v1` | 非集成产品限价白名单 |
|
||||
| `external.warehouse_locations_v1` | 仓库库位资料 |
|
||||
| `external.sku_warehouse_stock_v1` | 正式 SKU 库存和库位 |
|
||||
| `external.packaged_items_v1` | 已关联正式 SKU 的包装库存项 |
|
||||
| `external.sku_quality_inspections_v1` | SKU 仓库/首批质检记录 |
|
||||
| `external.product_assets_v1` | SKU 图片、模型、资产来源、素材引用 |
|
||||
| `external.product_bom_v1` | 正式 SKU 的板材、组件、五金摘要 |
|
||||
| `external.intake_products_v1` | 非集成开品主信息白名单 |
|
||||
@ -217,17 +228,22 @@ CREATE SCHEMA IF NOT EXISTS external;
|
||||
| `external.materials_v1` | 花色/材质基础资料 |
|
||||
| `external.asset_variants_v1` | 可被正式产品引用的资产规格款 |
|
||||
|
||||
契约层允许保留内部技术 ID 作为追踪字段,但外部业务关联必须优先使用 `product_business_key / sku_business_key` 等稳定业务键。独立 SQL 巡检和 `npm run db:verify-master-data:all` 都会检查核心外部视图的业务键缺失或重复风险。
|
||||
契约层允许保留内部技术 ID 作为追踪字段,但外部主同步必须优先使用 `barcode_business_key`;`product_business_key` 用于 SPU/型号归组,`sku_business_key` 用于 SKU 编码兼容关联。仓库库位优先使用 `warehouse_location_code`,质检记录自身去重使用 `record_key`。独立 SQL 巡检和 `npm run db:verify-master-data:all` 都会检查核心外部视图的业务键缺失或重复风险。
|
||||
|
||||
只读账号授权原则:
|
||||
|
||||
```sql
|
||||
GRANT USAGE ON SCHEMA external TO external_reader;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA external TO external_reader;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA external
|
||||
GRANT SELECT ON TABLES TO external_reader;
|
||||
GRANT USAGE ON SCHEMA external TO external_core_reader;
|
||||
GRANT SELECT ON external.product_catalog_wide_v1 TO external_core_reader;
|
||||
GRANT SELECT ON external.intake_product_limits_v1 TO external_core_reader;
|
||||
GRANT SELECT ON external.warehouse_locations_v1 TO external_core_reader;
|
||||
GRANT SELECT ON external.sku_warehouse_stock_v1 TO external_core_reader;
|
||||
GRANT SELECT ON external.packaged_items_v1 TO external_core_reader;
|
||||
GRANT SELECT ON external.sku_quality_inspections_v1 TO external_core_reader;
|
||||
```
|
||||
|
||||
不要给外部账号使用 `SELECT ON ALL TABLES IN SCHEMA external` 这类全量授权;新增外部视图必须单独评审后追加白名单。
|
||||
|
||||
禁止:
|
||||
|
||||
- 给外部账号授权 `public` schema 全表读取。
|
||||
|
||||
@ -160,6 +160,7 @@
|
||||
- 代码巡检已增加“未归类本地存储”硬护栏:非测试源码里的 `localStorage/sessionStorage` 直接读写和 `createJSONStorage` 封装持久化用途必须被归类为主数据风险、兼容缓存、旧缓存,或明确列入非主数据 UI/登录态缓存;未归类时命令失败,避免新的本地主数据真相源漏进系统。
|
||||
- 代码巡检已增加“应用层本地 JSON 文件主数据源”硬护栏:`src` 下不得用同步或异步 `readFile/writeFile`、`require/import` 读写或导入本地 `.json` 文件作为产品、资料库、权限或外部同步真相;导入载荷、迁移计划、备份和测试夹具必须留在明确的工具/测试边界内。
|
||||
- 代码巡检已增加非集成产品 `payload` 查询护栏:开发类型、负责人、待办和列表筛选不得直接把 `payload` JSON 当作并列真相源;历史兜底必须通过 `payloadJsonWhenProfileMissing(...)` 明确绑定到 profile 缺失场景。
|
||||
- 非集成产品定厂字段已结构化到 `IntakeNonIntegratedSkuItem`:供应商、供应商编码、工厂仓库、发货方式、最低起订量、含税单价、交期和安装方式均以 SKU 明细列为真相;`payload.factoryRows` 只保留迁移兼容快照和流程型字段,运行时回显与 `external.product_catalog_wide_v1` 不得再从该 JSON 兜底取定厂字段。`factoryWarehouse` 表示供应商发货仓,不得和库存仓位 `warehouse_location_name` 混用。
|
||||
- 代码巡检已增加关键地基表写入边界护栏:`src/app` 下的 Route/Page 不得直接写 `SystemConfig / ProviderConfig / ThemeConfig / IntakeNodePermissionOverride / AssistedAssemblyAiRender / Feedback / ActivityLog`,必须进入 service/repository 的规范化写入口;回收站产品/SKU 日志写入已改走统一 `logActivity`。
|
||||
- 代码巡检已继续扩展到核心主数据写入边界:`src/app` 下的 Route/Page 不得直接写 `Product / ProductSKU / ProductSkuAssetProfile / DraftGroup / PreSku / PreSkuBasicInfo / AssetFamily / AssetVariant / IntakeNonIntegratedProduct / IntakeNonIntegratedProductProfile / IntakeNonIntegratedSkuItem / ProductPatentRecord / SpuNumberCounter / IntakeReferenceLibraryItem`,防止页面接口绕开服务层的编码规范化、快照同步、质量状态和冲突检测;`src/server` 写入口会同时识别 Prisma client 别名和 relation 嵌套写入,子模型结构化副本不能藏在父模型 `create/update` 里绕过分类;资料库条目身份也禁止恢复 `id` 兜底 `itemKey`、直接 `itemKey: item.id`、`code || item.id` 或 `` `item:${item.id}` `` 的写法,有稳定编码的资料库必须用 `code` 作为条目身份,缺显式编码的资料库必须使用业务规格派生的稳定 key,避免浏览器临时 id 或数据库主键变成业务身份;草稿组编号禁止恢复纯 `Date.now()` 生成,服务层必须使用可读前缀和足够熵,数据库唯一索引只做最后兜底。
|
||||
- 文件系统输入也纳入代码巡检:`filesystem-master-data-source` 会识别读取 `public/uploads`、上传临时文件、ZIP 解包目录等文件来源后又写 `Product / ProductSKU / Panel / ProductHardware / Hardware` 的组合。只有登记过的旧上传/导入入口可保留,新增未登记入口会阻断,防止把素材目录或本地文件扫描当成第二主数据源。
|
||||
|
||||
289
furniture-3d-system/docs/架构设计/物流到货签收时长看板-订单明细快照.md
Normal file
289
furniture-3d-system/docs/架构设计/物流到货签收时长看板-订单明细快照.md
Normal file
@ -0,0 +1,289 @@
|
||||
# 物流到货签收时长看板 · 订单明细每日快照
|
||||
|
||||
## 为什么要做
|
||||
|
||||
看板的全部数据来自远端只读报表视图 `rpt.jst_order_express_details`(外加 `jst_qmcust_orderlist_item` 条码明细和 `SupplyChainProductArchive` 产品档案富化)。
|
||||
该视图是远端对象,一次扫描通常以分钟计,页面每次刷新、翻页、下钻、导出都要重新扫一遍,看板因此很难"高效运转"。
|
||||
|
||||
做法:每天把看板口径的订单明细物化到本地 Postgres,看板优先读本地快照,只有快照覆盖不到时才回落实时数据源。
|
||||
|
||||
## 数据与表
|
||||
|
||||
### 条码/供应商/采购的匹配来源(两套互补的订单表)
|
||||
|
||||
报表库把订单明细拆成了两套**互不重叠**的表,只看一套会漏掉近一半订单的条码:
|
||||
|
||||
| 表 | 覆盖平台 | 规模(实测) |
|
||||
| --- | --- | --- |
|
||||
| `rpt.jst_qmcust_orderlist` / `rpt.jst_qmcust_orderlist_item` | 淘宝天猫、阿里巴巴(1688)、淘工厂、天猫供销 | 408 万 / 576 万行 |
|
||||
| `rpt.jst_Orders` / `rpt.jst_OrderItem` | 京东商城、京东厂家直送、拼多多、头条放心购(抖音)、得物、唯品会、线下、微信视频号、小红书 | 124 万 / 175 万行 |
|
||||
|
||||
看板取条码时会**同时查两张明细表并用 `INNER_ORDER_ID`(即 `o_id`)合并**(`listItemRows`,两表按订单号各发一批查询后合并结果)。修复前只查 `jst_qmcust_orderlist_item`,导致京东/拼多多/抖音/得物/唯品会的订单条码全空;修复后新生成的快照条码匹配率为 **100%**(此前整体 52.3%)。
|
||||
|
||||
> 注:`rpt.jst_salesOutboundInfo`(销售出库单头)可以读取,但只有 `o_id / so_id / l_id / shop_name / io_id / qty` 等出库单头字段,没有 SKU 列,不能单独提供条码。
|
||||
|
||||
纯新增两张表(迁移 `prisma/migrations/20260928120000_add_logistics_arrival_signing_duration_snapshot`):
|
||||
|
||||
| 表 | 作用 |
|
||||
| --- | --- |
|
||||
| `LogisticsArrivalSigningDurationSnapshotRun` | 批次元数据:`snapshotDate`、`windowFrom`、`windowTo`、`status`(RUNNING/COMPLETED/FAILED)、`rowCount`、`message` |
|
||||
| `LogisticsArrivalSigningDurationSnapshot` | 明细行:与看板 09 订单明细一一对应,含条码/供应商/采购/`productItems` 等快照时点已固化的富化结果 |
|
||||
|
||||
要点:
|
||||
|
||||
- 只有 `status = COMPLETED` 的批次对读路径可见,写入中途或失败的批次不会被读到。
|
||||
- 行唯一键 `(snapshotDate, rowKey)`,`rowKey` 与下钻键一致(内部单号 + 线上订单号 + 物流单号 + 签收时间),重复执行用 `skipDuplicates` 幂等。
|
||||
- `(snapshotDate, signTime)` 有索引,按签收日期区间取数走索引。
|
||||
|
||||
## 生成任务
|
||||
|
||||
```bash
|
||||
# 默认:快照日期=当天(Asia/Shanghai),窗口=最近 62 天(覆盖本月与上月),保留最近 2 批
|
||||
npm run sync:logistics-arrival-signing-duration-snapshot -- \
|
||||
--apply --confirm=sync-logistics-arrival-signing-duration-snapshot \
|
||||
--expected-database-identity="<本地库身份>" \
|
||||
--expected-source-database-identity="<远端报表身份>"
|
||||
```
|
||||
|
||||
可调参数:`--snapshot-date=YYYY-MM-DD`、`--window-days`、`--chunk-days`(默认 7,按天分批抓取以控制内存)、`--batch-size`(默认 2000)、`--retention`、`--force`(当天批次已完成时强制重建)。
|
||||
|
||||
编排逻辑在 `src/server/services/logistics-arrival-signing-duration-snapshot.service.ts`:
|
||||
|
||||
1. 当天批次已 `COMPLETED` 且未 `--force` → 直接跳过;
|
||||
2. 标记 `RUNNING` 并清空当天旧行;
|
||||
3. 按 `chunk-days` 分片读远端并写入本地,同时记下真正抓到的最新签收时间 `dataThrough`;
|
||||
4. 全部成功才标记 `COMPLETED`(连同 `dataThrough`),随后按 `retention` 清理更早批次;任一分片失败则标记 `FAILED` 并保留错误信息,读路径会自动继续用上一批快照;
|
||||
5. 收尾关闭远端连接池(一次性任务必须关闭,否则进程挂在事件循环上)。
|
||||
|
||||
生产定时(systemd timer,默认北京时间 03:00):
|
||||
|
||||
```bash
|
||||
sudo bash scripts/install-logistics-arrival-signing-duration-snapshot-timer.sh
|
||||
```
|
||||
|
||||
首次安装会生成 `$BASE/shared/logistics-arrival-signing-duration-snapshot.env` 模板,需要填两个身份校验值(缺省为空会拒绝写入)。可用环境变量覆盖:`LOGISTICS_SNAPSHOT_CALENDAR`、`LOGISTICS_SNAPSHOT_WINDOW_DAYS`、`LOGISTICS_SNAPSHOT_RETENTION`。
|
||||
|
||||
## 读路径规则
|
||||
|
||||
`src/server/repositories/logistics-arrival-signing-duration.repository.ts` 的每个读方法都先判断快照是否可用:
|
||||
|
||||
- 取最近一批 `COMPLETED` 快照;请求区间**完全落在快照的实际覆盖范围**内才读快照(否则回落实时视图,避免返回不完整结果)。覆盖上限 = `min(windowTo - 1, dataThrough 当天)`:
|
||||
- `windowTo` 是跑批当天,当天还没签收完,所以只能信任到它前一天;
|
||||
- 再用批次记录的 `dataThrough`(该批真实最大签收时间)兜底,避免数据源滞后时把不完整的一天当成完整。
|
||||
- 例:一批 10-08 生成、数据只到 10-08 05:30 的快照,对 `10-01~10-08` 不再命中,看板/导出自动改用实时源,不会少算 10-08。
|
||||
- 命中快照时返回值带 `source: 'snapshot'` 与 `snapshotDate`,页面头部显示"快照数据 · 日期";未命中为 `source: 'live'`,显示原来的"数据更新 · 时间";
|
||||
- 快照表还没迁移或还没有任何批次时,`findLatestCompletedRun()` 返回 null(并吞掉异常),整条链路自动等价于改造前行为。
|
||||
|
||||
因此在以下情况仍会走实时数据源(较慢,但结果正确):请求区间超出窗口、任务当天尚未跑完或失败、本地还没生成过快照。
|
||||
|
||||
## 指标口径(整区间精确 vs 样本)
|
||||
|
||||
命中快照时,**整块看板都是整区间精确值**,与 2,000 条样本彻底解耦:
|
||||
|
||||
| 看板块 | 精确口径实现 |
|
||||
| --- | --- |
|
||||
| 经营总览(6 个 KPI) | `aggregateDashboard()` 一次扫描聚合 |
|
||||
| 履约环节拆解(5 环节平均/中位/P90) | 同上,`percentile_cont` 直接给出分位数 |
|
||||
| 发货→签收时效分布 | 同上,4 个区间计数 |
|
||||
| 付款→签收 / 付款→发货 多维经营分析(01–08) | `aggregatePayToSignAnalysis()` / `aggregatePayToSendAnalysis()`,按签收日期 + 采购/供应商/条码(`jsonb_array_elements` 展开 `productItems`)+ 物流公司/发货仓/平台/店铺分组 |
|
||||
| 发货仓 / 物流公司 超时排名 | `aggregateThresholdRankings()` |
|
||||
| 09 订单明细下钻 | `listDrilldownRows()`:按维度条件下钻,**列出全量匹配订单**并给出真实总数与页数 |
|
||||
|
||||
口径一致性由三点保证:筛选条件统一由 `snapshotFilterConditions()` 派生(Prisma 与原始 SQL 共用);比率/取整统一走 lib 的 `buildLogisticsArrivalSigningDurationOverview`、`buildPayToSignMetrics`、`buildPayToSendMetrics`;排序统一走 `comparePayToSignGroups` / `comparePayToSendGroups` / `compareLogisticsArrivalSigningDurationThresholdRows`。每个聚合块都用"把整区间明细喂给样本路径同样函数"的方式做过逐字段比对(MATCH)。
|
||||
|
||||
### 看板顺序与联动链路
|
||||
|
||||
### 履约环节拆解:目标值与看板宽度
|
||||
|
||||
### 经营总览:平均到货时长卡片与异常下钻
|
||||
|
||||
- 签收订单后面新增 **付款→签收平均到货时长** 卡片:主值是该环节平均时长,附行给出 **本月目标**(上月平均 × 98%)与 **目标达成率**,与履约环节拆解同一套口径。
|
||||
- **缺少关键时间 / 时间异常** 两张卡片可点击:点击后用 `{ kind: 'anomaly', value: 'missing' | 'invalid' }` 条件下钻,09 订单明细列出对应订单;再点一次取消。判定口径与总览一致——缺少关键时间 = 四个时间任一为空;时间异常 = 四个时间齐全但至少一个环节时长算不出来(时间顺序异常)。
|
||||
- **自动滚动只保留给经营总览的异常卡片**(卡片上写着"点击看明细"):点它们会把 09 订单明细滚到视口顶部(`detailBoardRef` + `scrollIntoView`,按"下钻签名"去重,不会在翻页时反复滚动)。**经营对象维度(平台/店铺/发货仓/物流/采购/供应商/商品/签收日期)的点选不再自动滚动**——那些看板是连着看的,一滚到底就没法接着看其他维度、还得手动往上拉;下钻筛选本身照常生效,09 明细就在原位置更新。
|
||||
- 为保证"卡片数字 = 点击后的明细条数",**命中快照时经营总览始终按整区间精确统计**(`aggregateDashboard(..., includeStages=false)`,只算总览不算分位数);超出精确上限的大区间里,只有履约环节拆解/时效分布/经营分析/超时排名退回样本,页面脚注会写明。
|
||||
|
||||
### 经营总览视觉层次与看板工具条
|
||||
|
||||
- 经营总览从"7 张卡一张网格"改为**两组网格**:5 张履约指标卡一排(`--sc-kpi-cols`,标准 5 / 紧凑 3 / 展开 2),2 张数据质量卡单独成排(`--sc-kpi-alert-cols`,标准 2 / 展开 1)。7 张卡在常见宽度下不再是 5+2、第二排右侧大片留白。
|
||||
- 卡片排版统一:标签 12px 用 `--sc-muted`,数值 24px 且 `font-variant-numeric: tabular-nums`,用 `margin-top: auto` 贴底、副标题保留 2 行最小高度(`min-height: 2.9em`)。同一行内标签换行不影响数值对齐,卡片等高、数字列对齐。
|
||||
- 绿色强调卡(付款→签收平均到货时长、付款→签收履约率)改为"浅绿底 + 左侧 2px 竖条"(`inset 2px 0 0`),不再整卡大面积着色;点击态的竖条与边框跟着状态色走。
|
||||
- 缺关键时间 / 时间异常改为横向"告警条":左侧图标 + 名称/说明,数值贴右,用低饱和暖色表达"异常"语义;点击下钻、`is-active` 选中态、`aria-pressed` 与 `aria-label`(含条数)都保留。
|
||||
- 修复看板工具条(导出 / 尺寸切换)样式不生效的根因:`BoardTools` 是本文件内的**独立组件**,styled-jsx 按组件注入作用域类,主组件里的 `.sc-logistics-icon-button` / `.sc-logistics-board-tools` / `.sc-logistics-board-size-label` 规则永远匹配不到它的 DOM,四个看板右上角一直是"裸图标"。现在样式放进 `BoardTools` 自己的 `<style jsx>`(深色排名表头用 `:global(.sc-logistics-ranking-board-header)` 适配),并删除主样式块里的死规则。
|
||||
|
||||
- 每个环节多出一列 **目标**:`本月目标 = 上月同环节平均时长 × 98%`(`LOGISTICS_ARRIVAL_SIGNING_DURATION_TARGET_IMPROVEMENT_RATIO`)。基准月取所选区间结束日期所在月的上一个月(跨年正确),基准值由 `aggregateStageAverages()` 用同一套筛选条件跑上个月区间得到;当前平均不高于目标时目标值标绿表示达标,鼠标悬停可看到"目标 = 上月平均 × 98%"的算式。
|
||||
- 目标后面还有 **目标达成率**:该环节时长不超过目标的订单占比。精确口径由 `aggregateStageTargetHits()` 按目标值统计命中订单数,样本口径由 `buildLogisticsArrivalSigningDurationStageAchievement()` 在样本上计算;无目标或无可计算订单时显示 `-`,鼠标悬停显示"命中 / 可计算"明细。环节行的平均/中位/P90/目标/达成率都只显示数值,单位(小时)只在表头语义里体现。
|
||||
- 环节行按 `128px / 弹性条 / 74 / 74 / 86 / 92` 收紧列宽,列表支持横向滚动,避免 P90、目标被截断。
|
||||
- 履约环节拆解与发货→签收时效分布之间加了**可拖动的分隔条**(`role="separator"`,支持左右方向键微调),比例默认 62% 并记在本地,可随时左右拖动调整两个看板的宽度。
|
||||
|
||||
经营对象问题排名各看板按"上级 → 下级"顺序排列,并逐级联动:
|
||||
|
||||
```
|
||||
01 签收日期维度 → 02 平台维度 → 03 店铺维度 → 04 发货仓维度 → 05 物流维度
|
||||
→ 06 采购维度 → 07 供应商维度 → 08 商品维度 → 09 订单明细
|
||||
```
|
||||
|
||||
点击任意一级对象,都会保留更上级条件、丢弃更下级条件,并把这条链路作为筛选条件重新取数:
|
||||
|
||||
- 整区间口径:链路以 `cascade`(JSON 数组,`LogisticsArrivalSigningDurationDrilldownFilter[]`)随 dashboard 请求下发,服务端在 `aggregatePayToSignAnalysis` / `aggregatePayToSendAnalysis` / `aggregateThresholdRankings` 的 WHERE 上追加 `cascadeSqlWhere`,因此下级看板与 09 明细都只展示对应数据;
|
||||
- 样本口径(实时源或超出精确上限):前端用 `logisticsArrivalSigningDurationRowMatchesCascade` 收窄样本后重算;
|
||||
- 店铺维度分组键是"平台 + 店铺",因此平台级联动能精确到该平台下的店铺;
|
||||
- 联动刷新是原位刷新(只有页面筛选变化才显示整块加载态),排名看板标题会显示当前链路与"联动加载中…"提示;
|
||||
- 经营总览、履约环节拆解、时效分布属于页级口径,不随联动链路变化。
|
||||
|
||||
### 经营对象的目标与目标达成率
|
||||
|
||||
02–08 经营对象表(平台 / 店铺 / 发货仓 / 物流 / 采购 / 供应商 / 商品)在原有指标后面追加 **目标** 与 **目标达成率** 两列;01 签收日期维度的「指标 × 日期」矩阵与总计不追加(按日算"上月同日目标"没有业务含义)。
|
||||
|
||||
- **目标 = 该对象上月同环节平均时长 × 98%**,达成率 = `100% − (平均 − 目标) / 目标 × 100%`,与履约环节拆解同一套口径,只是精确到每个经营对象——所以每个对象的目标值不同(例如 08 月各采购平均不同,9 月目标 52.7 / 56.5 / 60.5 …),达成的判断是"相对自己上个月是否改善 2%"。
|
||||
- 数值口径与展示一致:平均、目标都按 1 位小数取值后再算达成率,便于和表里看到的数字对账;没有上月基线的对象(新采购、新店铺、当月新条码)显示 `-`。
|
||||
- 取数:`aggregateObjectBaselines()`(快照仓库)在上月区间一次扫描算出 7 个维度 × 付款→签收 / 付款→发货 两个环节的平均时长,随 dashboard 载荷以 `objectBaselines` 下发;前端用 `withObjectAnalysisTargets()` 按分组键(`JSON.stringify(keyParts)`,与样本路径同一套键)合并,样本口径与整区间口径共用。
|
||||
- 这条聚合约 2–4 秒,因此按「快照日期 + 基准月 + 关键词/排除词」在服务端缓存 10 分钟(这些平均值只在每日快照重建时变化),并与看板其余查询并行执行;查询失败只让这两列显示 `-`,不会把整块看板拖回实时源。
|
||||
- 「经营对象问题排名」导出的 CSV 同步带上这两列。
|
||||
|
||||
### 看板分页
|
||||
|
||||
> 页面顶部的「经营管理 / 物流时效 + 看板标题 + 物流签收明细口径 + 明细条数/更新时间」页头已去掉(用户口径:无需展示,标签页标题已经说明是哪个看板);快照/缓存状态在数据源异常时的黄色提示条里仍会给出。
|
||||
|
||||
### 批量关键词查询
|
||||
|
||||
筛选区的「关键词」支持批量输入(与排除关键词同一套拆分规则):
|
||||
|
||||
- 按中文逗号 `,`、英文逗号 `,`、换行拆分,自动去空、去重,最多 100 个、单个最长 120 字符(原始输入上限 4000 字符);
|
||||
- **多个关键词之间是"或"**:命中任一字段即算匹配。整区间(快照)口径覆盖:内部单号、线上订单号、物流单号、物流公司、店铺、平台、发货仓、收货地址,以及快照时点固化的**条码、商品名称、供应商、采购**(`KEYWORD_FIELDS`)——所以能直接按采购/供应商/条码检索;
|
||||
- 走实时源时(区间超出快照窗口)只覆盖前 8 个报表字段,条码/采购/供应商这三类需要本地快照支持,这是两套口径的已知差异;
|
||||
- 解析规则由 `splitSearchKeywords()` 统一提供,服务端缓存键按关键词集合(排序后)计算,输入顺序不同不会重复缓存。
|
||||
- 输入框与其它筛选控件**等高**(34px 单行高度,多行时框内滚动),"已识别 N 个关键词"显示在标签行右侧并在空间不足时省略,不再把这一格撑成两行高。
|
||||
|
||||
筛选区的三个按钮各管一件事,点下去**立即按新条件查询**(互不重复):
|
||||
|
||||
| 按钮 | 签收日期 | 关键词 | 排除关键词 | 结果 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **重置** | 清空(输入框留空) | 清空 | 清空 | 没有任何筛选条件;空日期由服务端兜底为本月 1 号~昨日(`dateRangeCleared` 让输入框保持空白,不被兜底值回填) |
|
||||
| **恢复默认** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 恢复系统默认配置(全部勾选) | 默认口径 |
|
||||
| **清空排除** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 置空 | 默认区间 + 不排除任何关键词 |
|
||||
|
||||
三者过去都只是"改输入、等用户点查询"(重置只清日期与关键词、另两个只改排除词),结果看起来一样容易混;现在各自的语义、界面状态与结果都区分开了,按钮 hover 有对应说明,筛选栏底部也有一行图例。空日期提交时服务端仍按默认区间查询(跨天且当日快照未生成时可能落实时源,较慢但结果正确)。
|
||||
|
||||
**默认签收日期区间是「本月 1 号 ~ 昨日」**(由 `normalizeLogisticsArrivalSigningDurationFilters()` 生成):当日签收数据仍在滚动,且每日快照只覆盖到昨日,用"到昨日"作默认值可以让首屏直接命中快照(整区间精确 + 秒级),不再因为区间含当天而回落到实时源。每月 1 号时会自动退化为"昨日当天"(本月 1 号还没到昨日)。
|
||||
|
||||
筛选控件(签收日期、关键词输入框,以及排除关键词选择器的摘要)都是**受控并与服务端下发的筛选条件同步**:从菜单重新进入(无参数 URL)、重置后查询、浏览器前进/后退时,控件会刷成当前真正生效的条件。这一点很重要——否则会出现"数据已经是默认区间,输入框还停在上次条件"的错觉。
|
||||
|
||||
### 排除关键词选择器
|
||||
|
||||
筛选区的「排除关键词」是**按字段分组**的勾选式选择器(`ExcludeKeywordPicker.tsx`)。关键词只能配置在它要匹配的那个字段上,**不跨字段混用**——这是这一版最重要的口径修正。
|
||||
|
||||
**字段与匹配规则**
|
||||
|
||||
| 字段(配置分组) | 数据列 | 匹配方式 |
|
||||
| --- | --- | --- |
|
||||
| 发货仓 | `warehouseName` | 该字段值包含关键词(不区分大小写),命中即排除 |
|
||||
| 物流公司 | `logisticCompanyName` | 同上 |
|
||||
| 订单标签 | `orderLabels`(JSON 数组文本) | 同上 |
|
||||
| 买家留言 | `buyerMessage` | 同上 |
|
||||
| 卖家备注 | `remark` | 同上 |
|
||||
|
||||
配置与筛选都以「字段 → 关键词」存储(`LogisticsArrivalSigningDurationExcludeKeywords`)。SQL 侧对每个关键词只生成一条 `NOT (该字段 ILIKE '%词%')`;旧版"命中 5 个字段里任意一个就排除"的口径已废弃——那正是「物流」会连带命中订单标签里的 `商家指定物流`、`物流探查` 和买家留言里"物流单号"的根因。
|
||||
|
||||
**选择器交互**
|
||||
|
||||
- **按字段分组**:发货仓 / 物流公司 / 订单标签 / 买家留言 / 卖家备注,每组标题显示"共 N 项 · 已勾选 M 项",右侧索引条按字段跳转(字段没有候选词时不显示该组);
|
||||
- **默认全选**:候选词 = 该字段的默认排除词 ∪ 当前已排除词(旧链接里带的自定义词不会丢),进入看板时全部处于"排除"状态;取消勾选 = 本次不排除该项,确定后立即以当前表单条件重新查询(`requestSubmit()` 在受控值落到 DOM 后再触发);一个都不勾时以 `excludeKeywordsMode=clear` 提交,避免回落到默认排除词;
|
||||
- 选择器内含:搜索、`全部 / 已选项(n)` 页签、`全选 / 反选 / 清空`(作用于当前可见列表,全选带半选态)、`紧凑模式`(四列更密);
|
||||
- **新增候选词仅系统管理员可操作**:底部先选字段、再输入关键词(回车或点「添加」),新词并入**该字段**的默认排除词并立即落库(`PUT …/settings`,服务端 `assertCanManage` 同样校验管理员身份),提示"已保存到「物流公司」默认排除关键词:…";非管理员只看到"需要新增请联系系统管理员";
|
||||
- 页面原先独立的「默认排除关键词」设置卡片已删除,维护入口统一收敛到选择器;`清空排除`(不排除任何词)与 `恢复默认`(回到全部默认排除词)两个按钮保留,只改输入、点查询生效。
|
||||
|
||||
**编码与兼容**
|
||||
|
||||
- 筛选栏摘要按字段给数量(如"已排除 14 项:发货仓 3 · 物流公司 5 · 订单标签 2 · 买家留言 2 · 卖家备注 2"),URL 用 `excludeKeywords=字段:词,词;字段:词` 的紧凑写法;
|
||||
- **老链接的平铺写法按当前配置归位**:`excludeKeywords=词1,词2` 会被逐个对应到配置里它所属的字段(大小写不敏感,`bbff` 会归一到配置里的 `BBFF`),配置里已经没有的词说明它没有字段、直接忽略——**不会再把一个词摊到 5 个字段上**(那会让选择器每组都列出全部关键词)。只有当配置为空、任何词都对应不上时才退回旧口径,避免老链接突然变成"不排除任何词"。
|
||||
- 历史配置(数组)在 `normalizeExcludeKeywords()` 里仍按旧口径挂到全部 5 个字段(那是当年真实的匹配语义);但配置一旦以新结构保存,后续解析都会走"按字段归位"。
|
||||
|
||||
**当前初设配置**(2026-09-30 落库):发货仓=`自动发货仓 / 常州简尚家居有限公司 / 京东云仓`;物流公司=`京喜 / 物流 / 安能 / 自提 / 中铁`;订单标签=`配件 / 1688一件代发`;买家留言=`BBFF / SSDD`;卖家备注=`BBFF / SSDD`。2026-09-01~09-02 实测:不排除 26,296 行 → 按字段排除后 17,506 行(旧口径同样 14 个词是 16,552 行,多出的 954 行正是"标签里带物流、物流公司里没有"的那些)。
|
||||
|
||||
经营对象问题排名里的每个看板都有独立翻页,不再用"显示更多"或无限滚动,也不会因为对象太多被截断:
|
||||
|
||||
- 01 签收日期维度:按日期列翻页(默认 10 列/页,可切 10/20/50/100);
|
||||
- 02–08 采购/供应商/条码/物流公司/发货仓/平台/店铺:按对象行翻页(默认 20 行/页);
|
||||
- 发货仓 / 物流公司 超时排名:按对象行翻页(默认 20 行/页)。
|
||||
- 09 订单明细:翻页控件就在看板内部(不再挂在页面底部);未下钻时按 URL 的 `page/pageSize` 走服务端分页(每页 25/50/100/200,命中快照时显示真实总数"共 N 条筛选结果"),下钻时用本地分页翻全量匹配明细。
|
||||
|
||||
分页条统一由 `BoardPagination.tsx` 渲染(含"共 N 条 · 第 x / y 页"、上/下一页与每页条数),行集合变化(例如切换采购后重新筛选供应商)时自动回到第 1 页。
|
||||
|
||||
**每页条数切换的坑(已修)**:下拉框原先在 `onChange` 里同时调了 `onPageSizeChange(新值)` 和 `onPageChange(1)`。服务端分页(09 订单明细未下钻时)这两次都会触发 `router.push`,而后一次带着**旧的** `pageSize` 构造 URL 并覆盖前一次,表现为"订单明细每页条数选了没反应、又跳回 50"。现在 `BoardPagination` 只通知"每页条数变了",回到第 1 页由调用方负责:服务端分页用 `href(1, { ...filters, pageSize })`,本地分页(各经营对象表、发货仓/物流公司排名、下钻后的 09 明细)在 `setState` 里带 `page: 1`。
|
||||
|
||||
店铺维度表在"店铺"列前额外展示"平台"列:点击平台即把该表筛成该平台的店铺(标题变成"店铺维度 · 天猫",再次点击取消),点击店铺名仍走 09 明细下钻。店铺分组的业务键是"平台 + 店铺",避免同名店铺跨平台互相覆盖;平台列在样本路径(JS 归组)与整区间路径(SQL `GROUPING SETS ((platform, shop))`)都做了同样的处理,并逐字段比对过一致性。
|
||||
|
||||
### 规模上限与样本回落
|
||||
|
||||
SQL 聚合成本与区间行数近似线性(约 5 万行/秒)。当**区间原始行数超过 `LOGISTICS_ARRIVAL_SIGNING_DURATION_EXACT_SCOPE_MAX_ROWS`(11 万条,约 11 天)**时,`getDashboard` 会退回区间内均匀抽取的 2,000 条样本(`listDashboardSampleRows`,5 层并行、按签收时间分层,不再是"取最近一段"),并在底部注释写明原因与"缩小签收日期范围即可得到精确值"。
|
||||
|
||||
返回值上的 `dataScope`(`exact` / `sample`)与 `dataScopeReason`(`live-source` / `range-too-large`)驱动卡片副标题、底部注释与 CSV 导出的口径说明。
|
||||
|
||||
实测(663k 行快照,含 13 个默认排除关键词):
|
||||
|
||||
| 区间 | 原始行数 | 口径 | 首屏耗时 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 天 | 约 1.3 万 | 整区间精确 | 0.55s |
|
||||
| 2 天 | 约 2.6 万 | 整区间精确 | 1.7s |
|
||||
| 5 天 | 约 6.5 万 | 整区间精确 | 3.1s |
|
||||
| 本月 | 约 30 万 | 样本(超上限) | 3.8–5.0s |
|
||||
|
||||
服务端 60s 内重复访问直接命中缓存。要把大区间也做成亚秒精确,方向是在快照写入时就把 4 个时间戳、5 个环节时长落成列,并把多维指标的计数改为位掩码 `GROUP BY`(一次扫描产出全部桶计数),现有文档结论不变、只替换实现。
|
||||
|
||||
### 查询性能优化(2026-10)
|
||||
|
||||
上线后"每次查询生成太慢"的根因有三个,都在快照读路径上:
|
||||
|
||||
1. **整表顺序扫描**:聚合查询原先 `SELECT` 全列(`rowKey`、`productItems`、十几个文本列),
|
||||
Postgres 只能对 900MB 的堆表做 `Seq Scan`(28 天区间实测 `read=106409` 页、6.3 秒)。
|
||||
现在按用途收窄列集合——`FILTERED_TIMELINE_SELECT`(只取 4 个时间戳)用于整区间总览/环节目标,
|
||||
`FILTERED_DIMENSION_SELECT`(时间戳 + 4 个维度列)用于签收日期/平台/店铺/发货仓/物流/超时排名——
|
||||
并新增覆盖索引 `LogisticsArrivalSigningDurationSnapshot_cover_idx`
|
||||
(`(snapshotDate, signTime) INCLUDE (payTime, sendTime, fetchTime, warehouseName, platformName, shopName, logisticCompanyName)`),
|
||||
扫描变成 **Index Only Scan**(同样的 28 天区间 26ms、`Heap Fetches: 0`)。
|
||||
2. **有序集聚合把整行拖进排序**:`percentile_cont(0.5/0.9) WITHIN GROUP` 原先和十几个计数写在同一条语句里,
|
||||
输入行 14 列宽,PG 18 会把它整行送进排序(两个分位数单独就占 4.3 秒,环节分位数叠在一起甚至到分钟级)。
|
||||
现在分位数**按列拆成独立语句**、只投影一个浮点列,并与计数语句并行下发(`Promise.all`)。
|
||||
3. **CTE 表达式被重复内联**:`measured`/`durations` 的 `CASE + EXTRACT` 会在每一处引用被重新计算,
|
||||
同一条聚合里重复十几次。改为 `AS MATERIALIZED` 后只算一次(计数语句从 2.3s 降到 ~0.5s)。
|
||||
|
||||
另外把 **环节目标(上月平均 × 98%)** 也接入 10 分钟内存缓存(此前每次刷新都重跑上月聚合)。
|
||||
|
||||
优化前后(本机 Postgres 18、快照 648,689 行、冷启动、无缓存):
|
||||
|
||||
| 场景 | 优化前 | 优化后 |
|
||||
| --- | --- | --- |
|
||||
| 仓库层 `getDashboard`(28 天,样本口径) | 7.3s | **2.3s** |
|
||||
| 仓库层 `getDashboard`(10 天) | 4.4s | **0.4s** |
|
||||
| 仓库层 `getDashboard`(2 天,精确口径) | 1.7s | **0.3s** |
|
||||
| 浏览器首屏出数(冷启动,含 5.6MB 载荷 + 渲染) | ~7–10s | **2.7s** |
|
||||
|
||||
正确性用 2 天区间做过逐项对账:SQL 聚合与"取全量明细在 JS 里现算"在订单数、平均/中位/P90、
|
||||
异常计数、达成率、时效分布、5 个环节的 可计算单量/平均/中位/P90 上完全一致。
|
||||
|
||||
时间戳在 SQL 里用 `NULLIF(BTRIM(col),'')::timestamp` 直接转型:快照写入的就是远端 `CONVERT(varchar(19), …, 120)` 结果,格式固定;早期版本用正则逐行校验,整区间聚合约慢一个数量级。
|
||||
|
||||
## 运维排查
|
||||
|
||||
```sql
|
||||
-- 批次状态
|
||||
SELECT "snapshotDate", status, "windowFrom", "windowTo", "rowCount", message, "finishedAt"
|
||||
FROM "LogisticsArrivalSigningDurationSnapshotRun" ORDER BY "snapshotDate" DESC;
|
||||
|
||||
-- 当天行数
|
||||
SELECT count(*) FROM "LogisticsArrivalSigningDurationSnapshot" WHERE "snapshotDate" = CURRENT_DATE;
|
||||
```
|
||||
|
||||
- 页面长时间显示"数据更新"而不是"快照数据" → 先看批次是不是 `FAILED`,或窗口是否覆盖了页面的签收日期区间。
|
||||
- 批次 `RUNNING` 卡住(进程被杀)→ 该批次不会发布;直接重跑或加 `--force` 即可(重跑会先清空当天行)。
|
||||
- 需要重建历史窗口 → `--window-days=` 调大 + `--force`。
|
||||
|
||||
## 部署步骤(生产)
|
||||
|
||||
1. 部署新版本代码;
|
||||
2. 执行迁移 `prisma/migrations/20260928120000_add_logistics_arrival_signing_duration_snapshot`(需按生产迁移流程授权、先备份);
|
||||
3. 跑一次手工同步生成第一批快照(`--apply`),确认批次为 `COMPLETED`、行数与窗口符合预期;
|
||||
4. 安装 timer 并核对 `systemctl list-timers`;
|
||||
5. 打开看板确认头部显示"快照数据 · 日期"。
|
||||
|
||||
注意:Prisma Client 重新生成过,运行中的 dev/prod 进程需要重启才能拿到新模型。
|
||||
@ -1,6 +1,6 @@
|
||||
# White Obsidian Atelier(白曜石工坊)统一视觉标准
|
||||
|
||||
适用范围:资产中心、辅助装配、草稿组、正式入库、轻仓库等新平台主链页面。
|
||||
适用范围:资产中心、辅助装配、草稿组、产品资料/正式入库、仓库管理/轻仓库、仓库质检、AI 出图工作台,以及 `/admin`、`/users` 等新平台后台管理页面。
|
||||
|
||||
目标:统一“白曜石工坊 / White Obsidian Atelier”视觉方向。默认基调是浅色工作台,不再做深浅色主题切换;同时避免把资产中心做成普通 SaaS 卡片列表,也避免把毛玻璃效果滥用成低可读性的装饰。
|
||||
|
||||
@ -12,7 +12,7 @@ White Obsidian Atelier 的关键词是:浅色精密、克制通透、资产工
|
||||
|
||||
- `White Obsidian(白曜石)`:整体基调偏浅、安静、低干扰,用冷白、浅灰、石墨灰、金属中性色建立专业感。
|
||||
- `Atelier(工坊)`:页面不是营销展示页,而是给人长期处理资产、装配、审核、入库的工作空间。
|
||||
- `Unified(统一)`:资产中心、辅助装配、草稿组、正式入库、轻仓库要像同一套产品,不允许每个页面各自发明一套视觉语言。
|
||||
- `Unified(统一)`:资产中心、辅助装配、草稿组、产品资料/正式入库、仓库管理、仓库质检和 AI 工作台要像同一套产品,不允许每个页面各自发明一套视觉语言。
|
||||
- `3D Workbench(3D 工作台)`:新平台的第一感知应是“可操作的资产工作台”,而不是“管理后台卡片墙”。
|
||||
|
||||
视觉上应保持:
|
||||
@ -58,6 +58,14 @@ White Obsidian Atelier 的关键词是:浅色精密、克制通透、资产工
|
||||
|
||||
在新平台里,板材、五金、组件、包装等不应被当作普通卡片处理,而应被当作 `Meta Component(元组件)`。
|
||||
|
||||
落地时优先使用项目现有组件库和平台组件:
|
||||
|
||||
- `src/components/platform/`
|
||||
- 资产中心现有工作台组件
|
||||
- 已有元组件、Inspector、工作台、HUD 和平台导航结构
|
||||
|
||||
Stitch 设计稿是视觉规格和验收标准,但不是绕开组件库重写静态页面的理由。实现时应在项目组件体系内对齐 Stitch 的密度、层级、控件形态、留白、字号、圆角、阴影和低噪音表达。
|
||||
|
||||
元组件的含义是:它是可以被系统理解、检视、规格化、装配、审核、材质配置和后续入库承接的资产元件。
|
||||
|
||||
元组件至少应具备这些表达能力:
|
||||
@ -156,7 +164,9 @@ className="bg-white divide-y divide-zinc-100"
|
||||
- 毛玻璃是否只出现在顶栏、HUD、浮动工具条和轻量状态浮层。
|
||||
- 板材、五金、组件、包装是否按元组件表达,而不是普通展示卡片。
|
||||
- 3D 工作台、Inspector、数据列表之间的主次关系是否明确。
|
||||
- 当前页面是否仍符合项目主链:资产中心 -> 辅助装配 -> 草稿组 -> 正式入库 -> 轻仓库。
|
||||
- 当前页面是否仍符合项目主链:资产中心 -> 辅助装配 -> 草稿组 -> 产品资料/正式入库 -> 仓库管理/轻仓库。
|
||||
- 是否优先复用平台组件和元组件,而不是另起一套页面皮肤。
|
||||
- 如果有 Stitch 设计稿,是否按设计稿控制密度、控件状态和低噪音表达。
|
||||
|
||||
结论口径:
|
||||
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
# 苹果风格视觉设计标准 (Apple-Style Visual Design Standards)
|
||||
|
||||
> **版本**: 2.0 (2026-01-29)
|
||||
> **状态**: 补充参考标准;当前新平台第一视觉标准以 `白曜石工坊统一视觉标准.md` 为准
|
||||
> **优先级**: 如本文件与白曜石工坊标准冲突,优先执行白曜石工坊标准
|
||||
> **目标**: 打造优雅、统一、沉浸式的用户体验,完全去除"模板化"和"AI味",接近苹果官网的视觉水准。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [核心设计哲学](#1-核心设计哲学)
|
||||
2. [详细视觉规范](#2-详细视觉规范)
|
||||
* [色彩系统](#21-色彩系统)
|
||||
* [排版系统](#22-排版系统)
|
||||
* [布局与间距](#23-布局与间距)
|
||||
* [材质与光影](#24-材质与光影)
|
||||
3. [交互与动画](#3-交互与动画)
|
||||
4. [去AI味专项指南](#4-去ai味专项指南)
|
||||
5. [技术架构与文件结构](#5-技术架构与文件结构)
|
||||
6. [组件开发规范](#6-组件开发规范)
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心设计哲学
|
||||
|
||||
### 1.1 内容至上 (Content is King)
|
||||
* **原则**:界面不仅是容器,更是内容的舞台。UI 应该"退后",让 3D 模型、产品图片和关键数据"向前"。
|
||||
* **执行**:
|
||||
* 背景色主要使用白色或极淡的灰色;当前产品不再提供深浅色主题切换。
|
||||
* **拒绝**无意义的装饰性边框、分割线和阴影。只有在区分层级必要时才使用。
|
||||
* **拒绝**均等分割的网格布局("AI味"典型特征)。使用非对称布局(如:左侧 2/3 展示 3D 模型,右侧 1/3 展示控制面板)。
|
||||
|
||||
### 1.2 物理感与材质 (Materiality & Physics)
|
||||
* **原则**:数字界面应模拟真实世界的物理属性。
|
||||
* **执行**:
|
||||
* **Liquid Glass (毛玻璃)**:仅在覆盖内容时使用(如导航栏、侧边栏、浮动面板)。标准:`backdrop-filter: blur(20px) saturate(180%)`。
|
||||
* **Spring Animation (弹簧动画)**:交互元素(按钮按压、弹窗出现)必须使用弹簧物理模拟,而不是简单的线性过渡。
|
||||
* **光影**:光源应统一来自正上方。阴影应柔和、弥散,模拟环境光遮蔽 (Ambient Occlusion)。
|
||||
|
||||
### 1.3 优雅的排版 (Elegant Typography)
|
||||
* **原则**:排版是信息层级的核心。
|
||||
* **执行**:
|
||||
* **动态字间距 (Dynamic Tracking)**:大标题收紧 (-0.4px),正文标准,小字加宽 (+0.06px)。
|
||||
* **层次分明**:使用字重 (Weight) 和颜色 (Color) 区分层级,而不是单纯靠字号。
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细视觉规范
|
||||
|
||||
### 2.1 色彩系统
|
||||
|
||||
**设计令牌位置**: `src/lib/design-tokens/colors.ts`
|
||||
|
||||
不要使用纯灰 (`#808080`),总是混入微量的蓝色或暖色调。
|
||||
|
||||
| 用途 | Light Mode | Dark Mode | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **背景** | `#FFFFFF` | `#000000` | 纯净背景,最大化对比度 |
|
||||
| **次级背景** | `#F5F5F7` | `#1C1C1E` | 用于分组区块,**比系统灰(#F2F2F7)更淡、更优雅** |
|
||||
| **主文本** | `#1D1D1F` | `#F5F5F7` | 接近纯黑但不是纯黑 (Off-black) |
|
||||
| **次文本** | `#86868b` | `#86868b` | 经典的苹果灰,用于说明文字 |
|
||||
| **强调色** | `#008D74` | `#00B896` | 品牌绿,仅用于链接和主按钮 |
|
||||
| **分割线** | `rgba(0,0,0,0.1)` | `rgba(255,255,255,0.1)` | 极细,极淡 |
|
||||
|
||||
### 2.2 排版系统
|
||||
|
||||
**设计令牌位置**: `src/lib/design-tokens/typography.ts`
|
||||
|
||||
我们使用**合法的开源/系统字体栈**,严格避免版权风险:
|
||||
* **西文**: `-apple-system`, `BlinkMacSystemFont`, `Inter`, `Segoe UI`
|
||||
* **中文**: `-apple-system`, `Noto Sans CJK SC`, `Source Han Sans SC` (思源黑体), `Microsoft YaHei`
|
||||
* **代码**: `JetBrains Mono`, `Fira Code`
|
||||
|
||||
| 样式 | 字号 (px) | 行高 (px) | 字间距 (px) | 用途 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Large Title** | 34 | 41 | -0.4 | 页面主标题 |
|
||||
| **Title 1** | 28 | 34 | -0.36 | 章节标题 |
|
||||
| **Title 2** | 22 | 28 | -0.26 | 子章节标题 |
|
||||
| **Headline** | 17 | 22 | -0.41 | 强调文本 (Semibold) |
|
||||
| **Body** | 17 | 25 | -0.41 | 正文内容 |
|
||||
| **Caption 1** | 12 | 16 | 0 | 说明文字 |
|
||||
|
||||
### 2.3 布局与间距
|
||||
|
||||
**设计令牌位置**: `src/lib/design-tokens/spacing.ts`
|
||||
|
||||
* **基础网格**: 4px
|
||||
* **大留白原则**:
|
||||
* 模块间距至少 `48px` 或 `64px`。
|
||||
* 卡片内部 Padding 至少 `24px`。
|
||||
* **流体布局**:
|
||||
* 容器宽度随视口变化,最大宽度限制 (Max-width: 1440px)。
|
||||
* **全宽突破**: Hero Banner 等关键元素应占据全宽。
|
||||
|
||||
### 2.4 材质与光影
|
||||
|
||||
* **Liquid Glass**:
|
||||
* Light: `rgba(255, 255, 255, 0.72)` + Blur 20px
|
||||
* Dark: `rgba(22, 22, 23, 0.72)` + Blur 20px
|
||||
* **阴影 (Shadows)**:
|
||||
* Subtle: `0 2px 8px rgba(0, 0, 0, 0.04)` (静态元素)
|
||||
* Standard: `0 4px 12px rgba(0, 0, 0, 0.08)` (悬浮/交互)
|
||||
* Elevated: `0 8px 24px rgba(0, 0, 0, 0.12)` (模态框)
|
||||
|
||||
---
|
||||
|
||||
## 3. 交互与动画
|
||||
|
||||
**设计令牌位置**: `src/lib/design-tokens/animations.ts`
|
||||
|
||||
所有交互必须有物理反馈。
|
||||
|
||||
* **按钮按压**: Scale 0.98, Duration 100ms
|
||||
* **微交互 (Hover)**: Scale 1.02, Duration 200ms
|
||||
* **页面过渡**: Duration 500ms, Curve `cubic-bezier(0.16, 1, 0.3, 1)`
|
||||
* **Spring 参数**: 使用 `spring(mass: 1, stiffness: 100, damping: 15)` 模拟真实回弹。
|
||||
|
||||
---
|
||||
|
||||
## 4. 去AI味专项指南
|
||||
|
||||
"AI味" = 死板、重复、缺乏人情味、过度依赖默认组件。
|
||||
|
||||
### 4.1 拒绝"Lorem Ipsum"
|
||||
* ❌ "Product Name 1", "This is a description."
|
||||
* ✅ "Eames Lounge Chair", "由模压胶合板和皮革制成,经典的设计符号。"
|
||||
* **要求**:所有占位文本必须是**真实场景数据**。
|
||||
|
||||
### 4.2 拒绝"均等分布"
|
||||
* ❌ 4个功能点,每个占 25% 宽度,图标+标题+描述,完全一样。
|
||||
* ✅ 1个核心功能占 60% 宽度配大图,其余 3 个功能以列表形式在侧边展示。打破对称。
|
||||
|
||||
### 4.3 拒绝"默认图标"
|
||||
* ❌ 使用实心块状图标。
|
||||
* ✅ 优先使用文字。如果必须用图标,使用细线条风格 (Stroke width 1.5px-2px)。
|
||||
|
||||
### 4.4 拒绝"彩虹渐变"
|
||||
* ❌ 高饱和度彩虹渐变背景。
|
||||
* ✅ 纯色、微渐变或高质量实拍图/3D渲染图。
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术架构与文件结构
|
||||
|
||||
本项目基于 **Next.js 16 + Tailwind CSS + React 19** 构建。
|
||||
|
||||
### 5.1 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ └── design-tokens/ # 核心视觉配置
|
||||
│ ├── typography.ts # 字体定义
|
||||
│ ├── colors.ts # 颜色定义
|
||||
│ ├── spacing.ts # 间距定义
|
||||
│ └── animations.ts # 动画定义
|
||||
├── components/
|
||||
│ └── ui/ # 基础原子组件
|
||||
│ ├── Button.tsx # 遵循规范的按钮
|
||||
│ ├── Card.tsx # 遵循规范的卡片
|
||||
│ ├── Typography.tsx # 文本组件封装
|
||||
│ └── ...
|
||||
└── app/
|
||||
└── globals.css # CSS 变量映射 (自动从 tokens 生成)
|
||||
```
|
||||
|
||||
### 5.2 迁移指南
|
||||
|
||||
如果你在代码中看到旧的 CSS 类(如 `text-gray-500`),请替换为语义化的设计令牌类:
|
||||
* ❌ `text-gray-500` -> ✅ `text-(--color-label-secondary)` (或封装好的 Tailwind 类)
|
||||
* ❌ `bg-white` -> ✅ `bg-(--color-background-primary)`
|
||||
* ❌ `shadow-lg` -> ✅ `shadow-apple-lg`
|
||||
|
||||
---
|
||||
|
||||
## 6. 组件开发规范
|
||||
|
||||
### 6.1 Button
|
||||
* **形状**: 高度 36px/44px,Pill shape (999px) 或 Rounded (8px)。
|
||||
* **质感**: Primary (纯色无边框), Secondary (浅灰背景)。禁止复杂渐变。
|
||||
|
||||
### 6.2 Card
|
||||
* **圆角**: 统一 `20px` 或 `24px`。
|
||||
* **边框**: Light Mode 下通常无边框或极淡 `1px` 边框。
|
||||
* **内容**: 内部 Padding 至少 `24px`。
|
||||
|
||||
### 6.3 Input
|
||||
* **样式**: 移除默认边框,使用浅灰背景 (`#F5F5F7`),聚焦时出现 Focus Ring。
|
||||
* **高度**: 至少 `44px`。
|
||||
|
||||
### 6.4 Slider
|
||||
* **组件来源**: 统一使用 `src/components/ui/Slider.tsx`,禁止直接使用原生 `input[type="range"]`。
|
||||
* **轨道样式**: 高度 `6px`,圆角 `999px`,未填充色 `#D4DCE6`,填充色使用品牌色 `#008D74`(深色场景可切换 `#00B896`)。
|
||||
* **滑块样式**: 尺寸 `16px`,白色描边 `2px`,投影 `0 1px 3px rgba(0, 0, 0, 0.25)`。
|
||||
* **状态规范**: 禁用态降低不透明度并禁用交互光效;拖动态允许轻微放大(建议 `scale(1.08)`)。
|
||||
* **可访问性**: 必须提供 `aria-label`,并保持键盘可调节。
|
||||
|
||||
### 6.5 响应式
|
||||
* 使用 `useBreakpoint` 或 Tailwind 的响应式前缀 (`md:`, `lg:`)。
|
||||
* 支持 iPhone 刘海屏安全区域 (`safe-area-inset`).
|
||||
|
||||
---
|
||||
*遵循此标准,我们将构建出一个不仅好用,而且令人愉悦的家具展示系统。*
|
||||
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ STORAGE_PROVIDER=qiniu
|
||||
QINIU_ACCESS_KEY=<只放在服务器环境变量里>
|
||||
QINIU_SECRET_KEY=<只放在服务器环境变量里>
|
||||
QINIU_BUCKET=<七牛空间名称>
|
||||
QINIU_PUBLIC_DOMAIN=https://<七牛绑定域名>
|
||||
QINIU_PUBLIC_DOMAIN=https://<七牛 CDN 域名>
|
||||
QINIU_FILE_PATH=prod
|
||||
QINIU_USE_HTTPS=true
|
||||
QINIU_PRIVATE_BUCKET=true
|
||||
@ -126,6 +126,12 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
|
||||
- HTTP 测试域名只适合临时验证。素材需要保密时,正式环境应使用已绑定证书的 HTTPS CDN 域名,避免浏览器混合内容拦截和传输层明文风险。
|
||||
- 系统稳定读取接口会自动处理私有桶读取:HTTPS 正式域名走 307 签名跳转;HTTP 测试域名由后端代理读取,避免浏览器直接访问 HTTP 签名 URL。
|
||||
- 私有空间模式下,数据库保存系统内稳定访问地址,由后端按请求生成七牛临时签名 URL,不能把短期签名 URL 当作长期资料字段保存。
|
||||
- 当前稳定入口约定:
|
||||
- 登录态素材走 `/api/storage/object?key=...`
|
||||
- 分享态素材走 `/api/share/[token]/object?key=...`
|
||||
- 审批态素材继续输出系统稳定入口,再复用登录态鉴权读取链路
|
||||
- 新增写入链路默认也应回写系统稳定入口,而不是把七牛外链或短时签名 URL 直接落库。
|
||||
- 后续如果更换 bucket、CDN 域名或对象存储 provider,只改环境变量和 storage adapter,不改业务页链接逻辑。
|
||||
- 七牛侧如果保持“无自定义跨域规则”,空间默认允许跨域访问;如果后续新增自定义 CORS 规则,必须至少包含:
|
||||
- `Allowed Origins`:正式域名 `https://cp.jiansun.vip`,本地调试 `http://localhost:3000`,必要时加生产内网调试地址。
|
||||
- `Allowed Methods`:`GET`、`HEAD`、`OPTIONS`。
|
||||
@ -135,6 +141,14 @@ QINIU_PRIVATE_URL_EXPIRES_SECONDS=3600
|
||||
- 生产服务器填完对象存储环境变量后,先运行 `npm run storage:verify-config` 做只读配置校验。该命令不会上传文件,也不会打印 AK/SK 或签名 token。
|
||||
- 原有函数名 `uploadToS3`、`uploadBufferToS3` 仍保留为兼容入口,但内部已按 `STORAGE_PROVIDER` 分发到统一对象存储。
|
||||
|
||||
当前正式环境口径(2026-06-06):
|
||||
|
||||
- 七牛空间:`sucaiku-js`
|
||||
- 正式 CDN 域名:`https://qnycdn.jiansun.vip`
|
||||
- 正式环境对象读取:业务页继续使用系统稳定入口,服务端鉴权后统一 `307` 跳转到七牛短时签名 CDN 地址
|
||||
- 数据库存储口径不变:继续保存系统稳定地址或对象 key,不把短时签名 URL 入库
|
||||
- 若后续回滚,不要只改 `QINIU_PUBLIC_DOMAIN`;应把运行代码版本和对象存储配置作为一组一起回退
|
||||
|
||||
### 4.3 启动基础服务(数据库 / Redis / MinIO)
|
||||
|
||||
在 `furniture-3d-system` 目录下执行:
|
||||
@ -245,6 +259,12 @@ npm run dev
|
||||
4. **系统健康页面**
|
||||
- 访问 `/system-health`,检查是否能正常渲染,且不会抛出前端错误
|
||||
|
||||
5. **对象存储直连验收**
|
||||
- 登录态页面、分享页、审批页中的图片、贴图、模型和缩略图都能正常显示
|
||||
- 浏览器网络面板里,大部分最终素材请求应落到正式 CDN 域名,而不是持续由 `cp.jiansun.vip` 回流响应体
|
||||
- 如正式环境已启用 `QINIU_PUBLIC_DOMAIN=https://<cdn-domain>`,应确认 `307 -> signed CDN URL` 工作正常
|
||||
- 2026-06-06 已在正式环境 `https://cp.jiansun.vip/asset-center/material-library` 做过一次真实业务侧抽验:页面纹理稳定入口指向 `/api/storage/object?...`,服务端响应为 `307`,最终 `Location` 落到 `https://qnycdn.jiansun.vip/...`
|
||||
|
||||
---
|
||||
|
||||
## 7. 与备份/运维的衔接
|
||||
|
||||
124
furniture-3d-system/docs/限价与破价判定规则.md
Normal file
124
furniture-3d-system/docs/限价与破价判定规则.md
Normal file
@ -0,0 +1,124 @@
|
||||
# 限价与破价判定规则(业务确认版)
|
||||
|
||||
> 适用范围:限价管理 · 破价订单审核(`/supply-chain/management/custom-table` → 破价订单审核)
|
||||
> 本文件为业务口径的唯一来源;规则变更请改本文件并注明生效时间。
|
||||
|
||||
## 一、限价与判定口径
|
||||
|
||||
1. **判定基准**:以「商家实收」为准;`商家实收 < 限价 × 数量` 即判定破价。
|
||||
2. **实付款与平台承担**:实付款 + 平台承担(红包 / 消费券 / 平台券)= 商家实收;凭证必须同时包含「实付款」和「数量」。
|
||||
3. **出资方字典**
|
||||
- 商家承担:店铺优惠(官方立减、单品直降、店铺券)、政府补贴
|
||||
- 平台承担:红包、消费券、平台券、平台优惠、新品礼金
|
||||
- 买家承担:淘金币
|
||||
- 待定:首单礼金(等与运营确认后补录)
|
||||
4. **限价维度**:按**货号**限价——同一货号下所有规格/条码共用同一套限价。
|
||||
5. **限价时间版本**:按订单付款时点生效的限价判定(限价调整前后分别取当时版本),不做"用最新限价回算历史单"。
|
||||
6. **成本变动规则(限价维护侧)**:成本价上涨 5% 以内不动限价;成本价下调不变限价。
|
||||
7. **非负责人限价**:2026-10-07 00:00 前的 ID 价格不判定破价(此后按规则判定)。
|
||||
8. **老链接分界**:上架时间早于 2026-10-07 00:00 的链接(老链接)破价申诉通过;之后的按限价流程结束日期判定(上架时间需早于流程结束日期),并核对店铺款式编码 / 商品编码。
|
||||
9. **负责人触发规则**:非运营发起的流程,前两个月全公司运营同价(不归属个人);两个月后按销量触发负责人(销量高者为负责人)。
|
||||
10. **数据快照**:破价单在生成时保存金额快照;退款/售后状态单独记录,退款不等于免责(仍需判断是否已调价)。
|
||||
11. **离职人员**:订单负责人已离职 → 自动标记「人员已离职」,不判定、不计乐捐。
|
||||
12. **是否有检测报告**:取素材平台「检测报告」接口(`/api/external/certificate-reports`,按 **SPU** 精确匹配,只认有报告文件的记录),进程内缓存 10 分钟;
|
||||
接口未配置或读取失败时该列显示为空、筛选候选值为空。配置项:`MATERIAL_LIBRARY_CERTIFICATE_REPORT_API_BASE_URL/API_KEY`,
|
||||
不填则复用素材平台 `MATERIAL_LIBRARY_WHITE_BACKGROUND_API_*`。
|
||||
13. **手工换货单**:聚水潭订单明细出现「被替换行(`item_status=Replaced`、数量归 0)」+「换入行(`item_ext_data` 含 `item_source=手工换货/手工添加`)」时按换货单处理:
|
||||
- 判定按**换出行原编码**的限价执行(客户在平台上购买的那个编码);
|
||||
- 成交数量与商家实收取**换入行**(换货不改变订单成交金额);换入行金额抓不到时回退订单主表商家实收 → 订单主表买家实付,都取不到才按 0(由运营申诉说明);
|
||||
- 换货行配对成功后按原编码生成一条,不再按换入编码单独生成,避免同一订单重复生成两条破价单;
|
||||
- 配对优先按 `sku_uuid` 精确匹配,剩余行仅在一对一时顺序配对;复杂结构(多对多)暂不强行配对,相关行按各自编码现状处理。
|
||||
|
||||
## 二、申诉与审核
|
||||
|
||||
1. **申诉类型**(4 组)
|
||||
- 链接时间:老链接,上架时间早于限价流程(凭证须含链接 ID + 创建时间)/ 京喜换主体 · 老店转新店
|
||||
- 优惠 / 活动:平台优惠 · 红包 · 券(非商家承担)/ 参与秒杀 · 百补 · 大促等平台活动(凭证须含活动开始与截止时间,且活动价未破价)/ 拼多多多件折扣(99折)
|
||||
- 流程申请:已走新品限价及限价调整(必填流程号,系统到「新品限价及限价调整」流程校验,查不到不允许提交)
|
||||
- 其他原因(须在备注中详细说明)
|
||||
2. **申诉范围**:链接时间 / 流程申请类支持**链接级一次性申诉**(默认勾选,覆盖该链接下全部破价订单,后续新单自动继承);优惠 / 活动类按订单申诉。
|
||||
3. **审核要点仅审核人可见**;申诉人只看到凭证要求与示例图。
|
||||
4. **凭证校验**:必填校验 + 识别提示(识别结果仅提示,人工一键打回);后续增加图片查重(hash / 相似度)与关键字段(订单号、链接 ID)一致性校验。
|
||||
5. **乐捐**:驳回计 ¥10/单(离职不计);配套审计链(数据快照 + 申诉材料 + 审核记录),扣款执行交财务确认,保留异议通道。
|
||||
6. **举证期限**:待与运营确认后设置(避免事后无法追溯凭证)。
|
||||
|
||||
## 三、权限(由管理员统一管理)
|
||||
|
||||
路径:用户管理后台 → 权限配置 → 经营管理。限价管理相关的查看 / 导出 / 申诉都由管理员在这里开通。
|
||||
|
||||
| 能力 | 权限节点 | 动作 |
|
||||
| --- | --- | --- |
|
||||
| 页面与数据查看(限价明细、破价订单审核) | 限价管理 | 可见 |
|
||||
| 导出(导出明细 / 导出明细(含图片)/ 破价订单导出 / 乐捐明细) | 限价管理 | 下载 |
|
||||
| 申诉 · 审核 · 生成 · 导入 | 限价管理 | 操作 |
|
||||
| 规则配置(页面可见 + 保存参数) | 限价管理 · 规则配置 | 可见 / 操作 |
|
||||
|
||||
- 没有「下载」权限时页面不展示导出入口,服务端导出接口同样鉴权(下载中心任务按「限价管理 · 下载」校验)。
|
||||
- 没有「操作」权限时:不显示申诉/审核按钮,服务端也会拒绝申诉、审核、生成、导入。
|
||||
- 申诉保留一条兜底:破价单的负责人本人,即使没有「操作」权限,也可以对本人负责的订单发起申诉(审核仍需「操作」)。
|
||||
- 没有「规则配置」权限时,规则配置页签不显示、接口不可调用。
|
||||
|
||||
## 四、生成与运行
|
||||
|
||||
1. **按天增量**:限价逻辑上线后,破价订单**按天增量生成**(每天只生成最新一天的破价单并入库),**历史数据不再重复回算**;破价订单审核页每天展示最新破价单。
|
||||
2. **数据来源**:聚水潭报表库(`jst_Orders` + `jst_OrderItem` / 天猫奇门 `jst_qmcust_orderlist` + `_item`),取 `seller_income_amount`、`buyer_paid_amount`、`qty`、`shop_i_id`、`shop_sku_id`、`pay_date`、退款状态,以及换货识别字段 `item_status`、`item_ext_data`(`item_source`、`sku_uuid`)。报表库查询较慢,逐日增量任务建议按天分片拉取。
|
||||
3. **限价来源**:钉钉流程(仅取**已完结**),共 4 个模板,同步脚本 `scripts/sync-limit-workflows.mjs`:
|
||||
- 「新品限价及限价调整」→ 货号取「货号(非商品编码)」,限价取「期望日常限价」;
|
||||
其中的「调整限价明细表」子表(申请类型=调整原货号限价)只填「调整后限价」,该列同样作为限价取值
|
||||
(2026-10-16 修复,此前这类调价会被漏掉);
|
||||
- 「新品限价流程」→ 明细取系统产品限价页面(按流程链接 SPU 关联)。页面数据来自正式库
|
||||
`external.product_catalog_wide_v1`(限价字段:`expected_daily_limit_price` / `non_spu_owner_limit_price`),
|
||||
本地库该视图为空时退回本地 `IntakeNonIntegratedProduct(LimitRow)`;
|
||||
- 「各中心特殊限价需求申请」→ 货号取「货号」,限价取「最低售价」;
|
||||
- 「GB特殊限价需求申请」→ 货号取「货号」,限价取「期望日常限价」(2026-10-16 纳入)。
|
||||
- 「产品限价页面」已确认(CONFIRMED)的限价全量纳入(2026-10-16):来源正式库
|
||||
`external.product_catalog_wide_v1`,给没有限价流程的货号兜底(如 CY012801=268.6);
|
||||
同货号与流程限价冲突时**以流程为准**,页面价只在流程没覆盖时生效。
|
||||
- 「限价期初数据」(业务整理的存量限价 Excel,2026-10-16 纳入):放在期初数据目录
|
||||
(`LIMIT_SYNC_INITIAL_DIR`,默认 `<app>/期初数据`;目录为空时用仓库自带
|
||||
`scripts/data/limit-sync/期初数据/限价期初数据*.xlsx`),取目录里最新一份,
|
||||
按「货号 + 负责人限价 + 更新日期」写入,作为**最低优先级**兜底(如 MKHDNZ0409260600468=279)。
|
||||
|
||||
优先级:**限价流程 > 产品限价页面(已确认)> 限价期初数据**;低优先级只在货号没有更高优先级限价时生效。
|
||||
**例外(2026-10-16 确认)**:期初数据的「更新日期」**晚于**流程完成时间时,以期初数据为准(谁新用谁);
|
||||
期初比流程旧则维持上面的优先级。
|
||||
**非负责人限价一律按最新口径计算**(2026-10-16 确认):负责人限价 × 1.05,且不低于 +10 元;
|
||||
期初数据表 / 产品限价页面里的历史「非负责人限价」(倍数 1%~15% 不统一)不采用,
|
||||
限价明细页、导出文件、破价判定三处都用同一公式。
|
||||
写入规则:同货号多流程取**审核通过时间最新**的一条;限价为 0 或空的明细剔除;「申请与SPU负责人同价 / 活动限价」不覆盖负责人限价,只作为价格授权;流程「货号」列若填的是商品编码,用商品编码匹配产品档案换算为货号;无货号时同样用商品编码匹配。页面限价的时间按**北京时间墙钟**存(与流程完成时间一致)。
|
||||
|
||||
4. **覆盖率**:暂不做未匹配清单与告警(后续优化)。
|
||||
|
||||
5. **正式站同步(服务器端)**:`scripts/sync-limit-workflows.mjs` 已改成服务器可运行,连接信息全部取
|
||||
服务器 `shared/.env` 里的既有变量(`DATABASE_URL` / `JST_REPORT_DB_*` / `PROD_ARCHIVE_DB_*`),
|
||||
不再依赖 Mac 本地路径:
|
||||
|
||||
```bash
|
||||
# 手动跑一次(正式站目录下,root 或能读 shared/.env 的账号)
|
||||
cd /opt/furniture-3d-system-prod/current/furniture-3d-system
|
||||
sudo -E bash -lc 'set -a; . /opt/furniture-3d-system-prod/shared/.env; set +a; \
|
||||
LIMIT_SYNC_OUT_DIR=/opt/furniture-3d-system-prod/shared/limit-workflow-sync/导出结果 \
|
||||
LIMIT_SYNC_INITIAL_DIR=/opt/furniture-3d-system-prod/shared/limit-workflow-sync/期初数据 \
|
||||
node scripts/run-limit-workflow-sync-production.cjs'
|
||||
|
||||
# 安装每日定时器(systemd,与营销日销同一套样板):每天北京时间 07:30
|
||||
sudo bash scripts/install-limit-workflow-sync-timer.sh
|
||||
systemctl list-timers --all 'furniture-limit-workflow-sync*' --no-pager
|
||||
journalctl -u furniture-limit-workflow-sync.service -n 200 --no-pager
|
||||
```
|
||||
|
||||
- 写库是**全量重建 + 单事务**:成功才提交;失败自动回滚,正式库保留上一版限价与授权。
|
||||
- 输出目录 `LIMIT_SYNC_OUT_DIR`(默认 `<app>/导出结果`)里的 `限价流程_完整明细表.xlsx` 每日覆盖,
|
||||
旧版本进 `历史版本/`,默认保留最近 20 份(`LIMIT_SYNC_HISTORY_KEEP`)。
|
||||
- `--skip-excel` / `LIMIT_SYNC_SKIP_EXCEL=1` 可只写库不导 Excel。
|
||||
- 期初数据更新:把新的 `限价期初数据*.xlsx` 放进 `shared/limit-workflow-sync/期初数据/` 即可,下次同步自动取最新。
|
||||
- `SupplyChainBreachRuleConfig` 不需要灌数:接口读取时用代码内置默认值,首次在「规则配置」保存时落库。
|
||||
|
||||
## 五、待办
|
||||
|
||||
- [ ] 限价流程数据接入 + 筛选逻辑(生效时间版本、货号维度)
|
||||
- [ ] 非负责人限价 10-07 规则、负责人触发规则(前两个月同价 / 两个月后按销量)落到判定引擎
|
||||
- [ ] 按天增量任务(cron / worker)与失败告警
|
||||
- [ ] 凭证查重(hash / 相似度)、关键字段一致性校验
|
||||
- [ ] 举证期限、乐捐财务对接流程
|
||||
- [ ] 首单礼金 / 新品礼金出资方确认后补录出资方字典
|
||||
@ -1,17 +1,86 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
function trimToNull(value) {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? normalized : null
|
||||
}
|
||||
|
||||
function buildUrlOrigin(rawUrl) {
|
||||
const normalized = trimToNull(rawUrl)
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const withProtocol = /^https?:\/\//i.test(normalized)
|
||||
? normalized
|
||||
: `https://${normalized}`
|
||||
|
||||
try {
|
||||
const parsed = new URL(withProtocol)
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return null
|
||||
}
|
||||
return parsed.origin
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function collectStorageConnectSources() {
|
||||
const origins = new Set()
|
||||
for (const candidate of [
|
||||
process.env.S3_PUBLIC_DOMAIN,
|
||||
process.env.S3_PUBLIC_ENDPOINT,
|
||||
process.env.S3_PUBLIC_URL,
|
||||
process.env.S3_ENDPOINT,
|
||||
process.env.QINIU_PUBLIC_DOMAIN,
|
||||
process.env.QINIU_PUBLIC_ENDPOINT,
|
||||
process.env.QINIU_PUBLIC_URL,
|
||||
]) {
|
||||
const origin = buildUrlOrigin(candidate)
|
||||
if (origin) {
|
||||
origins.add(origin)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(origins)
|
||||
}
|
||||
|
||||
const connectSources = [
|
||||
"'self'",
|
||||
'https://login.dingtalk.com',
|
||||
'https://api.dingtalk.com',
|
||||
'https://oapi.dingtalk.com',
|
||||
...collectStorageConnectSources(),
|
||||
'ws://127.0.0.1:8000',
|
||||
'ws://127.0.0.1:18000',
|
||||
'ws://localhost:8000',
|
||||
'ws://localhost:18000',
|
||||
'wss://localhost.lodop.net:8443',
|
||||
...(!isProduction ? ['ws:', 'http://localhost:*', 'http://127.0.0.1:*'] : []),
|
||||
]
|
||||
|
||||
const lodopScriptSources = [
|
||||
'http://127.0.0.1:8000',
|
||||
'http://127.0.0.1:18000',
|
||||
'http://localhost:8000',
|
||||
'http://localhost:18000',
|
||||
'https://localhost.lodop.net:8443',
|
||||
]
|
||||
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
`frame-src 'self' ${lodopScriptSources.join(' ')}`,
|
||||
"form-action 'self'",
|
||||
"img-src 'self' data: blob: http: https:",
|
||||
"font-src 'self' data:",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
`script-src 'self' 'unsafe-inline'${isProduction ? '' : " 'unsafe-eval'"}`,
|
||||
`connect-src 'self' https://login.dingtalk.com https://api.dingtalk.com https://oapi.dingtalk.com${isProduction ? '' : ' ws: http://localhost:* http://127.0.0.1:*'}`,
|
||||
`script-src 'self' 'unsafe-inline' ${lodopScriptSources.join(' ')}${isProduction ? '' : " 'unsafe-eval'"}`,
|
||||
`connect-src ${connectSources.join(' ')}`,
|
||||
"worker-src 'self' blob:",
|
||||
"media-src 'self' blob:",
|
||||
...(isProduction ? ["upgrade-insecure-requests"] : []),
|
||||
@ -56,11 +125,6 @@ const baseSecurityHeaders = [
|
||||
: []),
|
||||
]
|
||||
|
||||
function trimToNull(value) {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? normalized : null
|
||||
}
|
||||
|
||||
function buildRemotePatternFromUrl(rawUrl) {
|
||||
const normalized = trimToNull(rawUrl)
|
||||
if (!normalized) {
|
||||
@ -68,7 +132,10 @@ function buildRemotePatternFromUrl(rawUrl) {
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(normalized)
|
||||
const withProtocol = /^https?:\/\//i.test(normalized)
|
||||
? normalized
|
||||
: `https://${normalized}`
|
||||
const parsed = new URL(withProtocol)
|
||||
const pathname = parsed.pathname.replace(/\/+$/, '')
|
||||
return {
|
||||
protocol: parsed.protocol.replace(':', ''),
|
||||
@ -106,6 +173,9 @@ for (const candidate of [
|
||||
process.env.S3_PUBLIC_ENDPOINT,
|
||||
process.env.S3_PUBLIC_URL,
|
||||
process.env.S3_ENDPOINT,
|
||||
process.env.QINIU_PUBLIC_DOMAIN,
|
||||
process.env.QINIU_PUBLIC_ENDPOINT,
|
||||
process.env.QINIU_PUBLIC_URL,
|
||||
]) {
|
||||
const pattern = buildRemotePatternFromUrl(candidate)
|
||||
if (!pattern) {
|
||||
@ -125,7 +195,42 @@ for (const candidate of [
|
||||
}
|
||||
}
|
||||
|
||||
function buildClientStorageEnv() {
|
||||
return {
|
||||
STORAGE_PROVIDER:
|
||||
trimToNull(process.env.STORAGE_PROVIDER) ||
|
||||
trimToNull(process.env.OBJECT_STORAGE_PROVIDER) ||
|
||||
'',
|
||||
OBJECT_STORAGE_PROVIDER:
|
||||
trimToNull(process.env.OBJECT_STORAGE_PROVIDER) ||
|
||||
trimToNull(process.env.STORAGE_PROVIDER) ||
|
||||
'',
|
||||
STORAGE_FILE_PATH:
|
||||
trimToNull(process.env.STORAGE_FILE_PATH) ||
|
||||
trimToNull(process.env.OBJECT_STORAGE_FILE_PATH) ||
|
||||
'',
|
||||
OBJECT_STORAGE_FILE_PATH:
|
||||
trimToNull(process.env.OBJECT_STORAGE_FILE_PATH) ||
|
||||
trimToNull(process.env.STORAGE_FILE_PATH) ||
|
||||
'',
|
||||
S3_FILE_PATH: trimToNull(process.env.S3_FILE_PATH) || '',
|
||||
QINIU_FILE_PATH: trimToNull(process.env.QINIU_FILE_PATH) || '',
|
||||
S3_PUBLIC_DOMAIN: trimToNull(process.env.S3_PUBLIC_DOMAIN) || '',
|
||||
S3_PUBLIC_ENDPOINT: trimToNull(process.env.S3_PUBLIC_ENDPOINT) || '',
|
||||
S3_PUBLIC_URL: trimToNull(process.env.S3_PUBLIC_URL) || '',
|
||||
S3_ENDPOINT: trimToNull(process.env.S3_ENDPOINT) || '',
|
||||
S3_BUCKET: trimToNull(process.env.S3_BUCKET) || '',
|
||||
QINIU_PUBLIC_DOMAIN: trimToNull(process.env.QINIU_PUBLIC_DOMAIN) || '',
|
||||
QINIU_PUBLIC_ENDPOINT: trimToNull(process.env.QINIU_PUBLIC_ENDPOINT) || '',
|
||||
QINIU_PUBLIC_URL: trimToNull(process.env.QINIU_PUBLIC_URL) || '',
|
||||
QINIU_USE_HTTPS: trimToNull(process.env.QINIU_USE_HTTPS) || '',
|
||||
}
|
||||
}
|
||||
|
||||
const clientStorageEnv = buildClientStorageEnv()
|
||||
|
||||
const nextConfig = {
|
||||
distDir: process.env.NEXT_DIST_DIR || '.next',
|
||||
// 启用standalone输出,用于Docker部署
|
||||
output: 'standalone',
|
||||
|
||||
@ -135,13 +240,14 @@ const nextConfig = {
|
||||
},
|
||||
|
||||
// 服务器外部包配置(Next.js 16新配置)
|
||||
serverExternalPackages: ['sharp', 'three', 'gsap'],
|
||||
serverExternalPackages: ['sharp', 'three', 'gsap', '@huggingface/transformers', 'onnxruntime-node', 'mssql'],
|
||||
|
||||
// Turbopack配置(Next.js 16默认启用)
|
||||
turbopack: {},
|
||||
|
||||
// Keep single request bodies bounded. Large non-integrated files use the chunked upload route.
|
||||
experimental: {
|
||||
cpus: 2,
|
||||
proxyClientMaxBodySize: 500 * 1024 * 1024,
|
||||
},
|
||||
|
||||
@ -238,6 +344,7 @@ const nextConfig = {
|
||||
// 环境变量配置
|
||||
env: {
|
||||
CUSTOM_KEY: process.env.CUSTOM_KEY,
|
||||
...clientStorageEnv,
|
||||
},
|
||||
|
||||
// 重定向配置
|
||||
|
||||
664
furniture-3d-system/package-lock.json
generated
664
furniture-3d-system/package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@huggingface/transformers": "3.8.1",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
@ -20,15 +21,18 @@
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.9",
|
||||
"exceljs": "^4.4.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"gsap": "^3.12.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"ioredis": "^5.9.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
@ -39,18 +43,22 @@
|
||||
"next": "^16.1.6",
|
||||
"obj2gltf": "^3.2.0",
|
||||
"pg": "^8.17.2",
|
||||
"pinyin-pro": "^3.28.1",
|
||||
"pinyin-pro": "^3.29.5",
|
||||
"postcss": "^8.5.6",
|
||||
"print-js": "^1.6.0",
|
||||
"puppeteer": "^24.40.0",
|
||||
"qiniu": "^7.15.2",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.182.0",
|
||||
"uuid": "^11.1.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.2"
|
||||
@ -61,6 +69,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/dockerode": "^4.0.1",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^30.0.0",
|
||||
@ -2961,6 +2970,27 @@
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@huggingface/jinja": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz",
|
||||
"integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@huggingface/transformers": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz",
|
||||
"integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@huggingface/jinja": "^0.5.3",
|
||||
"onnxruntime-node": "1.21.0",
|
||||
"onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4",
|
||||
"sharp": "^0.34.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -3502,6 +3532,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@ -5790,6 +5832,66 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
@ -6047,6 +6149,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/docker-modem": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/docker-modem/-/docker-modem-3.0.6.tgz",
|
||||
@ -6242,6 +6351,15 @@
|
||||
"pg-types": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qrcode": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
@ -7785,6 +7903,15 @@
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@ -7937,6 +8064,13 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
||||
@ -8186,7 +8320,6 @@
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -8751,6 +8884,22 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz",
|
||||
@ -8896,6 +9045,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
@ -8989,7 +9147,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
@ -9019,7 +9176,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
@ -9128,6 +9284,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
@ -9154,6 +9316,12 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/docker-modem": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/docker-modem/-/docker-modem-5.0.6.tgz",
|
||||
@ -9187,6 +9355,20 @@
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@ -9646,6 +9828,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.1.tgz",
|
||||
@ -9707,7 +9895,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -10591,6 +10778,12 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flatbuffers": {
|
||||
"version": "25.9.23",
|
||||
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
|
||||
"integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
|
||||
@ -11052,6 +11245,35 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
|
||||
@ -11069,7 +11291,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
@ -11131,6 +11352,12 @@
|
||||
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||
},
|
||||
"node_modules/guid-typescript": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||
@ -11158,7 +11385,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
@ -11276,6 +11502,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
@ -13244,6 +13483,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
|
||||
@ -14073,6 +14318,18 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -14224,12 +14481,23 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
@ -14677,7 +14945,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -14805,6 +15072,49 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-common": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
|
||||
"integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/onnxruntime-node": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz",
|
||||
"integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"win32",
|
||||
"darwin",
|
||||
"linux"
|
||||
],
|
||||
"dependencies": {
|
||||
"global-agent": "^3.0.0",
|
||||
"onnxruntime-common": "1.21.0",
|
||||
"tar": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-web": {
|
||||
"version": "1.22.0-dev.20250409-89f8206ba4",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz",
|
||||
"integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatbuffers": "^25.1.24",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"long": "^5.2.3",
|
||||
"onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4",
|
||||
"platform": "^1.3.6",
|
||||
"protobufjs": "^7.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
|
||||
"version": "1.22.0-dev.20250409-89f8206ba4",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz",
|
||||
"integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz",
|
||||
@ -14926,7 +15236,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -15024,7 +15333,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -15229,9 +15537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pinyin-pro": {
|
||||
"version": "3.28.1",
|
||||
"resolved": "https://registry.npmmirror.com/pinyin-pro/-/pinyin-pro-3.28.1.tgz",
|
||||
"integrity": "sha512-oqz8ulwRgtUXRi0vbqEfGNly19zpyCxYrjhkk5TibGcgSW6eNwS5woajCXRwqURi8Ehc2yOFTiB4uNoZ+NJOnA==",
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/pinyin-pro/-/pinyin-pro-3.29.5.tgz",
|
||||
"integrity": "sha512-Fk5YoRmtdHqJWqAuzYVG/NzvzeCbaG+ZEwbY6Nbps3QAIrvQQEtcm49frcyCM8i2b5najnzPDtYqrHRYTTiuwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pirates": {
|
||||
@ -15325,6 +15633,12 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/platform": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
||||
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.59.1.tgz",
|
||||
@ -15540,6 +15854,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/print-js": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/print-js/-/print-js-1.6.0.tgz",
|
||||
"integrity": "sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-7.3.0.tgz",
|
||||
@ -15826,6 +16146,182 @@
|
||||
"@types/offscreencanvas": "^2019.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
@ -16119,6 +16615,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
||||
@ -16237,6 +16739,29 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
@ -16390,11 +16915,56 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/seq-queue": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-error/node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/server-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@ -17305,6 +17875,22 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
@ -17333,6 +17919,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tarn": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz",
|
||||
@ -17468,6 +18072,15 @@
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/thenify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
|
||||
@ -18247,17 +18860,26 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
|
||||
"integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
@ -18459,6 +19081,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"worker:ai-image": "node scripts/run-ai-image-worker.mjs",
|
||||
"predev": "npm run prisma:check-client-sync",
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --webpack",
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
@ -16,16 +17,53 @@
|
||||
"prisma:check-client-sync": "node scripts/check-prisma-client-sync.js",
|
||||
"prisma:push": "node scripts/prisma-db-push-local.js",
|
||||
"prisma:studio": "prisma studio",
|
||||
"sync:supply-chain-product-archive": "tsx scripts/sync-supply-chain-product-archive.ts",
|
||||
"sync:supply-chain-product-archive-daily": "tsx scripts/sync-supply-chain-product-archive-sales-performance-daily.ts",
|
||||
"sync:supply-chain-product-archive-enrichment": "tsx scripts/sync-supply-chain-product-archive-enrichment.ts",
|
||||
"backfill:product-archive-designer": "tsx scripts/backfill-product-archive-designer-from-jst-sku-info-manage.ts",
|
||||
"backfill:product-archive-designer:apply": "tsx scripts/backfill-product-archive-designer-from-jst-sku-info-manage.ts --apply",
|
||||
"backfill:product-archive-opshead": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts",
|
||||
"backfill:product-archive-opshead:apply": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --apply",
|
||||
"backfill:product-archive-opshead:regular": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular",
|
||||
"backfill:product-archive-opshead:regular:apply": "tsx scripts/backfill-product-archive-opshead-from-jst-sku-info-manage.ts --mode=regular --apply",
|
||||
"backfill:product-archive-production-type": "tsx scripts/backfill-product-archive-production-type.ts",
|
||||
"backfill:product-archive-production-type:apply": "tsx scripts/backfill-product-archive-production-type.ts --apply",
|
||||
"sync:supply-chain-product-archive-sales-performance": "tsx scripts/sync-supply-chain-product-archive-sales-performance.ts",
|
||||
"run:product-sabc": "tsx scripts/run-product-sabc.ts",
|
||||
"compare:product-sabc-xlsx": "tsx scripts/compare-product-sabc-xlsx.ts",
|
||||
"seed:local-product-sabc-xlsx": "tsx scripts/seed-local-product-sabc-xlsx.ts",
|
||||
"sync:jst-sku-product-archive": "tsx scripts/sync-jst-sku-product-archive.ts",
|
||||
"sync:bom-cost-reference": "tsx scripts/sync-bom-cost-reference.ts",
|
||||
"backtest:bom-cost-reference": "tsx scripts/backtest-bom-cost-reference.ts",
|
||||
"report:bom-cost-shadow-divergence": "tsx scripts/report-bom-cost-shadow-divergence.ts",
|
||||
"report:bom-agent-baseline": "tsx scripts/report-bom-agent-baseline.ts",
|
||||
"trial:bom-costing-agent": "tsx scripts/run-bom-costing-agent-trial.ts",
|
||||
"import:yunshan-created-at": "tsx scripts/import-yunshan-product-created-at.ts",
|
||||
"import:product-archive-drawing-media": "tsx scripts/import-product-archive-drawing-media.ts",
|
||||
"db:verify-master-data": "node scripts/verify-master-data-foundation.js",
|
||||
"db:verify-master-data:audit": "node scripts/verify-master-data-foundation.js --output .master-data-audits/foundation-hard-ready-gate-current.json --summary-output .master-data-audits/foundation-hard-ready-gate-current.summary.json --summary-stdout",
|
||||
"db:verify-master-data:strict": "node scripts/verify-master-data-foundation.js --require-applied",
|
||||
"db:verify-master-data:migration-status": "node scripts/verify-master-data-foundation.js --read-only-migration-status",
|
||||
"db:verify-master-data:production-readonly": "node scripts/verify-master-data-foundation.js --read-only-quality --summary-output .master-data-audits/foundation-production-readonly-current.summary.json --summary-stdout",
|
||||
"db:cleanup-master-data": "node scripts/apply-master-data-cleanup-plan.js",
|
||||
"db:cleanup-master-data:validate": "node scripts/apply-master-data-cleanup-plan.js --validate-plan",
|
||||
"db:cleanup-operation-logs": "tsx scripts/cleanup-operation-logs.ts",
|
||||
"db:cleanup-operation-logs:apply": "tsx scripts/cleanup-operation-logs.ts --apply --confirm=cleanup-operation-logs",
|
||||
"db:export-product-category-review": "node scripts/export-non-integrated-product-category-review.js",
|
||||
"db:build-product-category-cleanup-plan": "node scripts/build-non-integrated-product-category-cleanup-plan.js",
|
||||
"db:match-jst-product-category-cleanup-plan": "node scripts/match-jst-product-category-cleanup-plan.js",
|
||||
"db:archive-jst-unresolved-products": "node scripts/archive-jst-unresolved-non-integrated-products.js",
|
||||
"prisma:seed": "tsx prisma/seed.ts",
|
||||
"open-platform:seed": "tsx scripts/seed-open-platform-demo.ts",
|
||||
"type-check": "node scripts/run-type-check.js",
|
||||
"storage:verify-config": "tsx scripts/verify-object-storage-config.ts",
|
||||
"storage:migrate-historical-assets": "tsx scripts/migrate-historical-assets-to-qiniu.ts",
|
||||
"storage:migrate-historical-assets:gate": "tsx scripts/migrate-historical-assets-to-qiniu.ts --fail-on-missing-sources",
|
||||
"storage:migrate-historical-assets:apply": "tsx scripts/migrate-historical-assets-to-qiniu.ts --apply",
|
||||
"storage:gc:audit": "tsx scripts/storage-gc.ts",
|
||||
"storage:gc:apply": "tsx scripts/storage-gc.ts --apply",
|
||||
"storage:inventory:filesystem": "tsx scripts/migrate-historical-assets-to-qiniu.ts --skip-database-scan --include-filesystem-inventory",
|
||||
"storage:inventory:filesystem:apply": "tsx scripts/migrate-historical-assets-to-qiniu.ts --skip-database-scan --include-filesystem-inventory --apply",
|
||||
"demo:create": "node scripts/create-demo-data.js",
|
||||
"demo:setup": "npm run prisma:push && npm run demo:create",
|
||||
"demo:update": "node scripts/update-demo-models.js",
|
||||
@ -33,11 +71,31 @@
|
||||
"thumbnails:backfill": "tsx scripts/backfill-asset-variant-thumbnails.ts",
|
||||
"thumbnails:backfill:apply": "tsx scripts/backfill-asset-variant-thumbnails.ts --apply",
|
||||
"materials:sync-textures": "tsx scripts/sync-material-textures.ts",
|
||||
"materials:sync-textures:apply": "tsx scripts/sync-material-textures.ts --apply",
|
||||
"materials:sync-standard": "tsx scripts/sync-standard-finishes.ts",
|
||||
"materials:sync-standard:apply": "tsx scripts/sync-standard-finishes.ts --apply",
|
||||
"sync:jst-sku-stock": "tsx scripts/sync-jst-sku-stock-products.ts",
|
||||
"sync:product-inventory-snapshot": "tsx scripts/sync-product-inventory-snapshot.ts",
|
||||
"image-search:model:prepare": "tsx scripts/prepare-product-archive-image-search-model.ts",
|
||||
"image-search:index": "tsx scripts/index-product-archive-images.ts",
|
||||
"repair:barcode-print:historical": "tsx scripts/repair-barcode-print-source-rows.ts",
|
||||
"sync:oa-board": "tsx scripts/sync-oa-board.ts",
|
||||
"sync:oa-board:apply": "tsx scripts/sync-oa-board.ts --apply",
|
||||
"sync:oa-brand-planning": "tsx scripts/sync-oa-brand-planning-board.ts",
|
||||
"sync:oa-brand-planning:apply": "tsx scripts/sync-oa-brand-planning-board.ts --apply",
|
||||
"sync:oa-video-flow": "tsx scripts/sync-oa-video-flow-board.ts",
|
||||
"sync:oa-video-flow:apply": "tsx scripts/sync-oa-video-flow-board.ts --apply",
|
||||
"sync:oa-art-flow-raw": "tsx scripts/sync-oa-art-flow-raw.ts",
|
||||
"sync:oa-art-flow-raw:apply": "tsx scripts/sync-oa-art-flow-raw.ts --apply",
|
||||
"cache:oa-board:local": "tsx scripts/cache-oa-board-local-preview.ts",
|
||||
"import:oa-board-activity-mappings": "tsx scripts/import-oa-board-activity-mappings.ts",
|
||||
"import:oa-board-node-rules": "tsx scripts/import-oa-board-node-rules.ts",
|
||||
"report:oa-board-activities": "tsx scripts/report-oa-board-activities.ts",
|
||||
"sync:employee-directory": "tsx scripts/sync-employee-directory.ts",
|
||||
"sync:employee-directory:apply": "tsx scripts/sync-employee-directory.ts --apply",
|
||||
"sync:dingtalk-directory:apply": "tsx scripts/sync-dingtalk-directory.ts --apply",
|
||||
"sync:external-jsreport": "tsx scripts/sync-external-readonly-to-jsreport.ts",
|
||||
"sync:external-jsreport:apply": "tsx scripts/sync-external-readonly-to-jsreport.ts --apply",
|
||||
"db:verify-master-data:truth": "node scripts/verify-master-data-truth-sources.js",
|
||||
"db:verify-master-data:code": "node scripts/verify-master-data-code-boundaries.js",
|
||||
"db:verify-master-data:all": "npm run db:verify-master-data:truth && npm run db:verify-master-data:code && npm run db:verify-master-data:migration-status && npm run db:verify-master-data",
|
||||
@ -63,7 +121,9 @@
|
||||
"db:foundation:apply-pre-sku-formalization-repair": "node scripts/apply-pre-sku-formalization-repair.js",
|
||||
"db:foundation:audit": "node scripts/run-master-data-foundation-audit.js",
|
||||
"db:foundation:audit:cleanup-template": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template",
|
||||
"db:foundation:audit:full": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template --use-docker-shadow --include-structure-repair-rehearsal --output-dir .master-data-audits/foundation-audit-current-goal",
|
||||
"db:foundation:audit:production-readonly": "node scripts/run-master-data-foundation-audit.js --production-read-only --output-dir .master-data-audits/foundation-production-readonly-current",
|
||||
"db:foundation:verify-production-readonly": "node scripts/verify-production-readonly-foundation-manifest.js",
|
||||
"db:foundation:audit:full": "node scripts/run-master-data-foundation-audit.js --include-cleanup-template --include-storage-migration-gate --use-docker-shadow --include-structure-repair-rehearsal --output-dir .master-data-audits/foundation-audit-current-goal",
|
||||
"db:foundation:completion-checklist": "node scripts/print-master-data-foundation-checklist.js",
|
||||
"db:foundation:decision-package": "node scripts/print-master-data-foundation-decision-package.js"
|
||||
},
|
||||
@ -73,6 +133,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@huggingface/transformers": "3.8.1",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
@ -80,15 +141,18 @@
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dockerode": "^4.0.9",
|
||||
"exceljs": "^4.4.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"gsap": "^3.12.5",
|
||||
"html2canvas": "^1.4.1",
|
||||
"ioredis": "^5.9.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
@ -99,18 +163,22 @@
|
||||
"next": "^16.1.6",
|
||||
"obj2gltf": "^3.2.0",
|
||||
"pg": "^8.17.2",
|
||||
"pinyin-pro": "^3.28.1",
|
||||
"pinyin-pro": "^3.29.5",
|
||||
"postcss": "^8.5.6",
|
||||
"print-js": "^1.6.0",
|
||||
"puppeteer": "^24.40.0",
|
||||
"qiniu": "^7.15.2",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.182.0",
|
||||
"uuid": "^11.1.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.2"
|
||||
@ -144,6 +212,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/dockerode": "^4.0.1",
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/jest": "^30.0.0",
|
||||
@ -166,5 +235,20 @@
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@prisma/engines",
|
||||
"cpu-features",
|
||||
"esbuild",
|
||||
"onnxruntime-node",
|
||||
"prisma",
|
||||
"protobufjs",
|
||||
"puppeteer",
|
||||
"sharp",
|
||||
"ssh2",
|
||||
"unrs-resolver"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +83,14 @@ BEGIN
|
||||
OR cardinality("missingRequiredFields") > 0
|
||||
)
|
||||
)
|
||||
OR
|
||||
(
|
||||
"dataQualityStatus" = 'CONFLICT'
|
||||
AND (
|
||||
NULLIF(btrim("skuCode"), '') IS NOT NULL
|
||||
OR NULLIF(btrim("barcode"), '') IS NOT NULL
|
||||
)
|
||||
)
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@ -234,7 +234,8 @@ BEGIN
|
||||
|
||||
SELECT 'IntakeNonIntegratedSkuItem.skuCode' AS field_name, upper(btrim("skuCode")) AS normalized_value
|
||||
FROM "IntakeNonIntegratedSkuItem"
|
||||
WHERE NULLIF(btrim("skuCode"), '') IS NOT NULL
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("skuCode"), '') IS NOT NULL
|
||||
GROUP BY upper(btrim("skuCode"))
|
||||
HAVING COUNT(*) > 1
|
||||
|
||||
@ -242,7 +243,8 @@ BEGIN
|
||||
|
||||
SELECT 'IntakeNonIntegratedSkuItem.barcode' AS field_name, upper(btrim("barcode")) AS normalized_value
|
||||
FROM "IntakeNonIntegratedSkuItem"
|
||||
WHERE NULLIF(btrim("barcode"), '') IS NOT NULL
|
||||
WHERE "dataQualityStatus" = 'READY'
|
||||
AND NULLIF(btrim("barcode"), '') IS NOT NULL
|
||||
GROUP BY upper(btrim("barcode"))
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
-- Repair environments where the non-integrated SKU quality enum was created
|
||||
-- before CONFLICT was added to the Prisma schema and governance migrations.
|
||||
-- Target relation: "IntakeNonIntegratedSkuItem"."dataQualityStatus".
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_type t
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.typname = 'IntakeNonIntegratedSkuDataQualityStatus'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_enum e
|
||||
JOIN pg_type t ON t.oid = e.enumtypid
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND t.typname = 'IntakeNonIntegratedSkuDataQualityStatus'
|
||||
AND e.enumlabel = 'CONFLICT'
|
||||
) THEN
|
||||
ALTER TYPE "IntakeNonIntegratedSkuDataQualityStatus" ADD VALUE 'CONFLICT';
|
||||
END IF;
|
||||
END $$;
|
||||
@ -0,0 +1,232 @@
|
||||
-- Structure non-integrated product pricing rows so external price queries do not read payload JSON.
|
||||
-- Each active SKU gets one pricing-state row. The row may have no cost yet, but confirmation status is database-owned.
|
||||
|
||||
CREATE TABLE "IntakeNonIntegratedPricingRow" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"skuItemId" TEXT NOT NULL,
|
||||
"skuCode" TEXT NOT NULL DEFAULT '',
|
||||
"barcode" TEXT NOT NULL DEFAULT '',
|
||||
"costPrice" DECIMAL(10,2),
|
||||
"productTag" TEXT NOT NULL DEFAULT '',
|
||||
"pricingConfirmed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"pricingConfirmedAt" TIMESTAMP(3),
|
||||
"systemEnteredAt" TIMESTAMP(3),
|
||||
"jstVirtualInventoryStatus" TEXT NOT NULL DEFAULT '',
|
||||
"jstVirtualInventoryScheduledAt" TIMESTAMP(3),
|
||||
"jstVirtualInventoryPushedAt" TIMESTAMP(3),
|
||||
"jstVirtualInventoryError" TEXT NOT NULL DEFAULT '',
|
||||
"jstVirtualInventorySoId" TEXT NOT NULL DEFAULT '',
|
||||
"source" TEXT NOT NULL DEFAULT 'PAYLOAD_SYNC',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "IntakeNonIntegratedPricingRow_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_product_sku_item_fkey"
|
||||
FOREIGN KEY ("productId", "skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("productId", "skuItemId")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeNonIntegratedPricingRow_productId_skuItemId_key"
|
||||
ON "IntakeNonIntegratedPricingRow"("productId", "skuItemId");
|
||||
CREATE INDEX "IntakeNonIntegratedPricingRow_productId_idx" ON "IntakeNonIntegratedPricingRow"("productId");
|
||||
CREATE INDEX "IntakeNonIntegratedPricingRow_skuCode_idx" ON "IntakeNonIntegratedPricingRow"("skuCode");
|
||||
CREATE INDEX "IntakeNonIntegratedPricingRow_barcode_idx" ON "IntakeNonIntegratedPricingRow"("barcode");
|
||||
CREATE INDEX "IntakeNonIntegratedPricingRow_pricingConfirmed_idx" ON "IntakeNonIntegratedPricingRow"("pricingConfirmed");
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_identity_valid"
|
||||
CHECK (
|
||||
NULLIF(btrim("skuItemId"), '') IS NOT NULL
|
||||
AND "skuCode" = upper(btrim("skuCode"))
|
||||
AND "barcode" = upper(btrim("barcode"))
|
||||
);
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_cost_state_valid"
|
||||
CHECK (
|
||||
("costPrice" IS NULL OR "costPrice" >= 0)
|
||||
AND "jstVirtualInventoryStatus" IN ('', 'pending', 'pushed', 'failed', 'skipped')
|
||||
);
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_optional_text_not_blank"
|
||||
CHECK (
|
||||
"productTag" = btrim("productTag")
|
||||
AND "jstVirtualInventoryStatus" = btrim("jstVirtualInventoryStatus")
|
||||
AND "jstVirtualInventoryError" = btrim("jstVirtualInventoryError")
|
||||
AND "jstVirtualInventorySoId" = btrim("jstVirtualInventorySoId")
|
||||
AND "source" = btrim("source")
|
||||
AND NULLIF("source", '') IS NOT NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION "__parse_intake_non_integrated_pricing_timestamp"(value TEXT)
|
||||
RETURNS TIMESTAMP(3) AS $$
|
||||
BEGIN
|
||||
IF NULLIF(btrim(COALESCE(value, '')), '') IS NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
RETURN btrim(value)::TIMESTAMP(3);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
WITH source_sku_items AS (
|
||||
SELECT
|
||||
p."id" AS product_id,
|
||||
item.sku_item,
|
||||
item.ordinality::integer AS sort_order,
|
||||
COALESCE(NULLIF(btrim(item.sku_item ->> 'id'), ''), 'sku-' || item.ordinality::text) AS raw_sku_item_id
|
||||
FROM "IntakeNonIntegratedProduct" p
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
|
||||
THEN p."payload"::jsonb -> 'skuItems'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS item(sku_item, ordinality)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
),
|
||||
deduped_sku_items AS (
|
||||
SELECT
|
||||
source_sku_items.*,
|
||||
CASE
|
||||
WHEN COUNT(*) OVER (PARTITION BY product_id, raw_sku_item_id) = 1
|
||||
THEN raw_sku_item_id
|
||||
ELSE raw_sku_item_id || '-' || ROW_NUMBER() OVER (
|
||||
PARTITION BY product_id, raw_sku_item_id
|
||||
ORDER BY sort_order
|
||||
)::text
|
||||
END AS sku_item_id
|
||||
FROM source_sku_items
|
||||
),
|
||||
structured_sku_items AS (
|
||||
SELECT
|
||||
source.product_id,
|
||||
source.raw_sku_item_id,
|
||||
sku."skuItemId" AS sku_item_id,
|
||||
upper(btrim(COALESCE(sku."skuCode", ''))) AS sku_code,
|
||||
upper(btrim(COALESCE(sku."barcode", ''))) AS barcode
|
||||
FROM deduped_sku_items source
|
||||
JOIN "IntakeNonIntegratedSkuItem" sku
|
||||
ON sku."productId" = source.product_id
|
||||
AND sku."skuItemId" = source.sku_item_id
|
||||
),
|
||||
pricing_payload AS (
|
||||
SELECT
|
||||
p."id" AS product_id,
|
||||
pricing_row.pricing_row,
|
||||
btrim(COALESCE(pricing_row.pricing_row ->> 'skuId', '')) AS raw_sku_item_id
|
||||
FROM "IntakeNonIntegratedProduct" p
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(p."payload"::jsonb -> 'pricingRows') = 'array'
|
||||
THEN p."payload"::jsonb -> 'pricingRows'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS pricing_row(pricing_row)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
),
|
||||
source_rows AS (
|
||||
SELECT
|
||||
product."id" AS product_id,
|
||||
sku.sku_item_id,
|
||||
sku.sku_code,
|
||||
sku.barcode,
|
||||
pricing.pricing_row,
|
||||
COALESCE(
|
||||
product."payload"::jsonb #>> ARRAY['documentWorkspaceConfirmations', '产品核价'],
|
||||
'false'
|
||||
) = 'true' AS pricing_confirmed,
|
||||
product."payload"::jsonb #>> ARRAY['documentWorkspaceApprovedAt', '产品核价'] AS pricing_confirmed_at
|
||||
FROM structured_sku_items sku
|
||||
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku.product_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT payload.pricing_row
|
||||
FROM pricing_payload payload
|
||||
WHERE payload.product_id = sku.product_id
|
||||
AND (
|
||||
payload.raw_sku_item_id = sku.raw_sku_item_id
|
||||
OR payload.raw_sku_item_id = sku.sku_item_id
|
||||
)
|
||||
LIMIT 1
|
||||
) pricing ON TRUE
|
||||
)
|
||||
INSERT INTO "IntakeNonIntegratedPricingRow" (
|
||||
"id",
|
||||
"productId",
|
||||
"skuItemId",
|
||||
"skuCode",
|
||||
"barcode",
|
||||
"costPrice",
|
||||
"productTag",
|
||||
"pricingConfirmed",
|
||||
"pricingConfirmedAt",
|
||||
"systemEnteredAt",
|
||||
"jstVirtualInventoryStatus",
|
||||
"jstVirtualInventoryScheduledAt",
|
||||
"jstVirtualInventoryPushedAt",
|
||||
"jstVirtualInventoryError",
|
||||
"jstVirtualInventorySoId",
|
||||
"source",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
product_id || ':' || sku_item_id AS id,
|
||||
product_id,
|
||||
sku_item_id,
|
||||
sku_code,
|
||||
barcode,
|
||||
CASE
|
||||
WHEN btrim(COALESCE(pricing_row ->> 'costPrice', '')) ~ '^[0-9]+(\.[0-9]+)?$'
|
||||
AND round((pricing_row ->> 'costPrice')::numeric, 2) <= 99999999.99
|
||||
THEN round((pricing_row ->> 'costPrice')::numeric, 2)::DECIMAL(10,2)
|
||||
ELSE NULL
|
||||
END AS cost_price,
|
||||
btrim(COALESCE(pricing_row ->> 'productTag', '')) AS product_tag,
|
||||
pricing_confirmed,
|
||||
"__parse_intake_non_integrated_pricing_timestamp"(pricing_confirmed_at),
|
||||
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'systemEnteredAt'),
|
||||
CASE
|
||||
WHEN pricing_row ->> 'jstVirtualInventoryStatus' IN ('pending', 'pushed', 'failed', 'skipped')
|
||||
THEN pricing_row ->> 'jstVirtualInventoryStatus'
|
||||
ELSE ''
|
||||
END AS jst_virtual_inventory_status,
|
||||
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'jstVirtualInventoryScheduledAt'),
|
||||
"__parse_intake_non_integrated_pricing_timestamp"(pricing_row ->> 'jstVirtualInventoryPushedAt'),
|
||||
btrim(COALESCE(pricing_row ->> 'jstVirtualInventoryError', '')) AS jst_virtual_inventory_error,
|
||||
btrim(COALESCE(pricing_row ->> 'jstVirtualInventorySoId', '')) AS jst_virtual_inventory_so_id,
|
||||
'PAYLOAD_SYNC',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM source_rows
|
||||
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
|
||||
|
||||
DROP FUNCTION "__parse_intake_non_integrated_pricing_timestamp"(TEXT);
|
||||
|
||||
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_pricing_rows_on_soft_delete"()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
DELETE FROM "IntakeNonIntegratedPricingRow"
|
||||
WHERE "productId" = NEW."id";
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS "IntakeNonIntegratedProduct_soft_delete_cleanup_pricing_rows"
|
||||
ON "IntakeNonIntegratedProduct";
|
||||
|
||||
CREATE TRIGGER "IntakeNonIntegratedProduct_soft_delete_cleanup_pricing_rows"
|
||||
AFTER UPDATE OF "deletedAt" ON "IntakeNonIntegratedProduct"
|
||||
FOR EACH ROW
|
||||
WHEN (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL)
|
||||
EXECUTE FUNCTION "cleanup_intake_non_integrated_pricing_rows_on_soft_delete"();
|
||||
@ -0,0 +1,102 @@
|
||||
CREATE OR REPLACE FUNCTION "normalize_internal_asset_storage_url"(input_url TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
raw_value TEXT;
|
||||
object_path TEXT;
|
||||
BEGIN
|
||||
IF input_url IS NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
raw_value := btrim(input_url);
|
||||
IF raw_value = '' THEN
|
||||
RETURN input_url;
|
||||
END IF;
|
||||
|
||||
IF raw_value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/api/storage/object\?key=' THEN
|
||||
RETURN regexp_replace(raw_value, '^https?://[^/]+', '', 'i');
|
||||
END IF;
|
||||
|
||||
IF raw_value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/furniture-3d/' THEN
|
||||
object_path := regexp_replace(raw_value, '^https?://[^/]+/furniture-3d/', '', 'i');
|
||||
object_path := split_part(object_path, '?', 1);
|
||||
IF object_path <> '' THEN
|
||||
RETURN '/api/storage/object?key=' || object_path;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF raw_value ~* '^https?://[^/]*hd-bkt\.clouddn\.com/' THEN
|
||||
object_path := regexp_replace(raw_value, '^https?://[^/]+/', '', 'i');
|
||||
object_path := split_part(object_path, '?', 1);
|
||||
IF object_path <> '' THEN
|
||||
RETURN '/api/storage/object?key=' || object_path;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN input_url;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
UPDATE "Product"
|
||||
SET
|
||||
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
|
||||
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
|
||||
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
|
||||
|
||||
UPDATE "ProductSKU"
|
||||
SET
|
||||
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
|
||||
"images" = ARRAY(
|
||||
SELECT "normalize_internal_asset_storage_url"(image_url)
|
||||
FROM unnest("images") AS image_url
|
||||
)
|
||||
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM unnest("images") AS image_url
|
||||
WHERE "normalize_internal_asset_storage_url"(image_url) IS DISTINCT FROM image_url
|
||||
);
|
||||
|
||||
UPDATE "ProductSkuAssetProfile"
|
||||
SET "imageUrl" = "normalize_internal_asset_storage_url"("imageUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("imageUrl") IS DISTINCT FROM "imageUrl";
|
||||
|
||||
UPDATE "AssetVariant"
|
||||
SET
|
||||
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
|
||||
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
|
||||
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
|
||||
|
||||
UPDATE "Material"
|
||||
SET
|
||||
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl"),
|
||||
"textureUrl" = "normalize_internal_asset_storage_url"("textureUrl"),
|
||||
"normalMapUrl" = "normalize_internal_asset_storage_url"("normalMapUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl"
|
||||
OR "normalize_internal_asset_storage_url"("textureUrl") IS DISTINCT FROM "textureUrl"
|
||||
OR "normalize_internal_asset_storage_url"("normalMapUrl") IS DISTINCT FROM "normalMapUrl";
|
||||
|
||||
UPDATE "Hardware"
|
||||
SET
|
||||
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
|
||||
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
|
||||
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
|
||||
|
||||
UPDATE "CommonComponent"
|
||||
SET
|
||||
"modelUrl" = "normalize_internal_asset_storage_url"("modelUrl"),
|
||||
"thumbnailUrl" = "normalize_internal_asset_storage_url"("thumbnailUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("modelUrl") IS DISTINCT FROM "modelUrl"
|
||||
OR "normalize_internal_asset_storage_url"("thumbnailUrl") IS DISTINCT FROM "thumbnailUrl";
|
||||
|
||||
UPDATE "IntakeNonIntegratedSkuItem"
|
||||
SET
|
||||
"image" = "normalize_internal_asset_storage_url"("image"),
|
||||
"model3dDataUrl" = "normalize_internal_asset_storage_url"("model3dDataUrl")
|
||||
WHERE "normalize_internal_asset_storage_url"("image") IS DISTINCT FROM "image"
|
||||
OR "normalize_internal_asset_storage_url"("model3dDataUrl") IS DISTINCT FROM "model3dDataUrl";
|
||||
|
||||
DROP FUNCTION "normalize_internal_asset_storage_url"(TEXT);
|
||||
@ -0,0 +1,90 @@
|
||||
-- Add a goods-code grouping layer between AssetFamily and AssetVariant.
|
||||
-- Existing active variants are backfilled by goods code so multiple models can share one goods-code group.
|
||||
|
||||
CREATE TABLE "AssetSkuGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"assetFamilyId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AssetSkuGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
ADD COLUMN "skuGroupId" TEXT;
|
||||
|
||||
WITH sku_group_sources AS (
|
||||
SELECT
|
||||
"assetFamilyId",
|
||||
"code",
|
||||
(ARRAY_AGG(COALESCE(NULLIF(TRIM("name"), ''), "code") ORDER BY "createdAt" ASC, "id" ASC))[1] AS "name",
|
||||
MIN("createdAt") AS "createdAt",
|
||||
MAX("updatedAt") AS "updatedAt",
|
||||
JSONB_AGG("id" ORDER BY "createdAt" ASC, "id" ASC) AS "sourceAssetVariantIds"
|
||||
FROM "AssetVariant"
|
||||
WHERE "deletedAt" IS NULL
|
||||
GROUP BY "assetFamilyId", "code"
|
||||
),
|
||||
ranked_sku_groups AS (
|
||||
SELECT
|
||||
"assetFamilyId",
|
||||
"code",
|
||||
"name",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"sourceAssetVariantIds",
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "assetFamilyId"
|
||||
ORDER BY "createdAt" ASC, "code" ASC
|
||||
) AS row_no
|
||||
FROM sku_group_sources
|
||||
)
|
||||
INSERT INTO "AssetSkuGroup" (
|
||||
"id",
|
||||
"assetFamilyId",
|
||||
"code",
|
||||
"name",
|
||||
"description",
|
||||
"sortOrder",
|
||||
"metadata",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
CONCAT('asg_', MD5("assetFamilyId" || ':' || "code")),
|
||||
"assetFamilyId",
|
||||
"code",
|
||||
"name",
|
||||
NULL,
|
||||
row_no,
|
||||
jsonb_build_object('source', 'variant-backfill', 'sourceAssetVariantIds', "sourceAssetVariantIds"),
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
FROM ranked_sku_groups;
|
||||
|
||||
UPDATE "AssetVariant" AS variant
|
||||
SET "skuGroupId" = CONCAT('asg_', MD5(variant."assetFamilyId" || ':' || variant."code"))
|
||||
WHERE variant."skuGroupId" IS NULL
|
||||
AND variant."deletedAt" IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX "AssetSkuGroup_assetFamilyId_code_key"
|
||||
ON "AssetSkuGroup"("assetFamilyId", "code");
|
||||
CREATE INDEX "AssetSkuGroup_assetFamilyId_idx" ON "AssetSkuGroup"("assetFamilyId");
|
||||
CREATE INDEX "AssetSkuGroup_code_idx" ON "AssetSkuGroup"("code");
|
||||
CREATE INDEX "AssetSkuGroup_sortOrder_idx" ON "AssetSkuGroup"("sortOrder");
|
||||
CREATE INDEX "AssetVariant_skuGroupId_idx" ON "AssetVariant"("skuGroupId");
|
||||
|
||||
ALTER TABLE "AssetSkuGroup"
|
||||
ADD CONSTRAINT "AssetSkuGroup_assetFamilyId_fkey"
|
||||
FOREIGN KEY ("assetFamilyId") REFERENCES "AssetFamily"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
ADD CONSTRAINT "AssetVariant_skuGroupId_fkey"
|
||||
FOREIGN KEY ("skuGroupId") REFERENCES "AssetSkuGroup"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@ -0,0 +1,80 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('public."AssetSkuGroup"') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'AssetVariant'
|
||||
AND column_name = 'skuGroupId'
|
||||
)
|
||||
THEN
|
||||
EXECUTE $SQL$
|
||||
UPDATE "AssetVariant" AS variant
|
||||
SET "businessData" = jsonb_set(
|
||||
CASE
|
||||
WHEN variant."businessData" IS NULL OR jsonb_typeof(variant."businessData") <> 'object'
|
||||
THEN '{}'::jsonb
|
||||
ELSE variant."businessData"
|
||||
END,
|
||||
'{legacySkuGroup}',
|
||||
jsonb_build_object(
|
||||
'id', sku_group.id,
|
||||
'code', sku_group.code,
|
||||
'name', sku_group.name,
|
||||
'description', sku_group.description,
|
||||
'sortOrder', sku_group."sortOrder",
|
||||
'metadata', sku_group.metadata,
|
||||
'assetFamilyId', sku_group."assetFamilyId",
|
||||
'source', 'AssetSkuGroup.retired'
|
||||
),
|
||||
true
|
||||
)
|
||||
FROM "AssetSkuGroup" AS sku_group
|
||||
WHERE variant."skuGroupId" = sku_group.id
|
||||
$SQL$;
|
||||
|
||||
EXECUTE $SQL$
|
||||
UPDATE "AssetFamily" AS family
|
||||
SET metadata = jsonb_set(
|
||||
CASE
|
||||
WHEN family.metadata IS NULL OR jsonb_typeof(family.metadata) <> 'object'
|
||||
THEN '{}'::jsonb
|
||||
ELSE family.metadata
|
||||
END,
|
||||
'{legacySkuGroups}',
|
||||
grouped.groups,
|
||||
true
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
"assetFamilyId",
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', id,
|
||||
'code', code,
|
||||
'name', name,
|
||||
'description', description,
|
||||
'sortOrder', "sortOrder",
|
||||
'metadata', metadata,
|
||||
'source', 'AssetSkuGroup.retired'
|
||||
)
|
||||
ORDER BY "sortOrder", code
|
||||
) AS groups
|
||||
FROM "AssetSkuGroup"
|
||||
GROUP BY "assetFamilyId"
|
||||
) AS grouped
|
||||
WHERE family.id = grouped."assetFamilyId"
|
||||
$SQL$;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
DROP CONSTRAINT IF EXISTS "AssetVariant_skuGroupId_fkey";
|
||||
|
||||
DROP INDEX IF EXISTS "AssetVariant_skuGroupId_idx";
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
DROP COLUMN IF EXISTS "skuGroupId";
|
||||
|
||||
DROP TABLE IF EXISTS "AssetSkuGroup";
|
||||
@ -0,0 +1,96 @@
|
||||
-- Add a single wide external product-catalog view for read-only consumers.
|
||||
-- This view only aggregates existing external contract views; it does not
|
||||
-- expose public business tables or internal JSON snapshots directly.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS external;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
)
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
|
||||
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
|
||||
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
|
||||
asset_summary.first_model_asset_url,
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
sku.created_at,
|
||||
sku.updated_at
|
||||
FROM external.product_skus_v1 sku
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
|
||||
GRANT USAGE ON SCHEMA external TO external_reader;
|
||||
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@ -0,0 +1,303 @@
|
||||
-- Add external warehouse and SKU quality views for read-only consumers.
|
||||
-- These views expose a stable whitelist for product data, warehouse stock,
|
||||
-- packaged items, and warehouse quality inspection records.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS external;
|
||||
|
||||
CREATE OR REPLACE VIEW external.warehouse_locations_v1 AS
|
||||
SELECT
|
||||
wh."id" AS warehouse_location_id,
|
||||
wh."code" AS warehouse_location_code,
|
||||
wh."name" AS warehouse_location_name,
|
||||
wh."zone" AS zone,
|
||||
wh."aisle" AS aisle,
|
||||
wh."shelf" AS shelf,
|
||||
wh."bin" AS bin,
|
||||
wh."isActive" AS is_active,
|
||||
CASE WHEN wh."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
|
||||
wh."createdAt" AS created_at,
|
||||
wh."updatedAt" AS updated_at
|
||||
FROM "WarehouseLocation" wh;
|
||||
|
||||
CREATE OR REPLACE VIEW external.sku_warehouse_stock_v1 AS
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.barcode,
|
||||
sku.stock AS stock_quantity,
|
||||
sku.is_active AS sku_is_active,
|
||||
sku.record_status AS sku_record_status,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
wh.zone AS warehouse_zone,
|
||||
wh.aisle AS warehouse_aisle,
|
||||
wh.shelf AS warehouse_shelf,
|
||||
wh.bin AS warehouse_bin,
|
||||
wh.is_active AS warehouse_location_is_active,
|
||||
wh.record_status AS warehouse_location_status,
|
||||
sku.created_at,
|
||||
sku.updated_at
|
||||
FROM external.product_skus_v1 sku
|
||||
LEFT JOIN external.warehouse_locations_v1 wh
|
||||
ON wh.warehouse_location_id = sku.warehouse_location_id;
|
||||
|
||||
CREATE OR REPLACE VIEW external.packaged_items_v1 AS
|
||||
SELECT
|
||||
item."id" AS packaged_item_id,
|
||||
item."code" AS packaged_item_code,
|
||||
item."name" AS packaged_item_name,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
product."id" AS product_id,
|
||||
product."modelNumber" AS product_model_number,
|
||||
product."name" AS product_name,
|
||||
sku."id" AS sku_id,
|
||||
sku."skuCode" AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(sku."name", ''),
|
||||
NULLIF(profile."specName", ''),
|
||||
sku."skuCode"
|
||||
) AS sku_name,
|
||||
profile."barcode" AS barcode,
|
||||
item."quantityPerPack" AS quantity_per_pack,
|
||||
item."stockQuantity" AS stock_quantity,
|
||||
item."length" AS length,
|
||||
item."width" AS width,
|
||||
item."height" AS height,
|
||||
item."weight" AS weight,
|
||||
item."status" AS status,
|
||||
item."warehouseLocationId" AS warehouse_location_id,
|
||||
wh."code" AS warehouse_location_code,
|
||||
wh."name" AS warehouse_location_name,
|
||||
wh."zone" AS warehouse_zone,
|
||||
wh."aisle" AS warehouse_aisle,
|
||||
wh."shelf" AS warehouse_shelf,
|
||||
wh."bin" AS warehouse_bin,
|
||||
wh."isActive" AS warehouse_location_is_active,
|
||||
item."createdAt" AS created_at,
|
||||
item."updatedAt" AS updated_at
|
||||
FROM "PackagedItem" item
|
||||
LEFT JOIN "ProductSKU" sku
|
||||
ON sku."id" = item."skuId"
|
||||
AND sku."deletedAt" IS NULL
|
||||
LEFT JOIN "Product" product
|
||||
ON product."id" = sku."productId"
|
||||
AND product."deletedAt" IS NULL
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile
|
||||
ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "WarehouseLocation" wh
|
||||
ON wh."id" = item."warehouseLocationId"
|
||||
WHERE sku."id" IS NOT NULL
|
||||
AND product."id" IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.sku_quality_inspections_v1 AS
|
||||
SELECT
|
||||
inspection."id" AS quality_inspection_id,
|
||||
inspection."recordKey" AS record_key,
|
||||
inspection."sourceType" AS source_type,
|
||||
CASE
|
||||
WHEN inspection."sourceType" = 'FIRST_BATCH' THEN '首批质检'
|
||||
ELSE '仓库质检'
|
||||
END AS source_label,
|
||||
inspection."sourceProductId" AS source_product_id,
|
||||
inspection."sourceProductSpu" AS source_product_spu,
|
||||
inspection."sourceSkuItemId" AS source_sku_item_id,
|
||||
inspection."batchNo" AS batch_no,
|
||||
inspection."inspectedAt" AS inspected_at,
|
||||
inspection."inspectorName" AS inspector_name,
|
||||
inspection."conclusion" AS conclusion,
|
||||
inspection."detail" AS detail,
|
||||
inspection."inspectedQuantity" AS inspected_quantity,
|
||||
inspection."qualifiedQuantity" AS qualified_quantity,
|
||||
inspection."qualifiedRate" AS qualified_rate,
|
||||
inspection."status" AS status,
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.barcode,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
inspection."createdAt" AS created_at,
|
||||
inspection."updatedAt" AS updated_at
|
||||
FROM "ProductSkuQualityInspection" inspection
|
||||
JOIN external.product_skus_v1 sku
|
||||
ON sku.sku_id = inspection."skuId";
|
||||
|
||||
DROP VIEW IF EXISTS external.product_catalog_wide_v1;
|
||||
|
||||
CREATE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
sku_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(sku_business_key, '') IS NOT NULL
|
||||
GROUP BY sku_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
sku_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(sku_business_key, '') IS NOT NULL
|
||||
GROUP BY sku_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (sku_business_key)
|
||||
sku_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(sku_business_key, '') IS NOT NULL
|
||||
ORDER BY sku_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
)
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
|
||||
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
|
||||
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
|
||||
asset_summary.first_model_asset_url,
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at
|
||||
FROM external.product_skus_v1 sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.sku_business_key = sku.sku_business_key;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
|
||||
GRANT USAGE ON SCHEMA external TO external_reader;
|
||||
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
|
||||
GRANT SELECT ON external.warehouse_locations_v1 TO external_reader;
|
||||
GRANT SELECT ON external.sku_warehouse_stock_v1 TO external_reader;
|
||||
GRANT SELECT ON external.packaged_items_v1 TO external_reader;
|
||||
GRANT SELECT ON external.sku_quality_inspections_v1 TO external_reader;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@ -0,0 +1,901 @@
|
||||
-- Structure non-integrated product limit rows before exposing them through
|
||||
-- the external read-only contract. Stock-product detail rows are marked as
|
||||
-- limit-price not applicable in the external views.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS external;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "IntakeNonIntegratedProductLimitRow" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"skuItemId" TEXT NOT NULL,
|
||||
"skuCode" TEXT NOT NULL DEFAULT '',
|
||||
"barcode" TEXT NOT NULL DEFAULT '',
|
||||
"expectedDailyLimitPrice" TEXT NOT NULL DEFAULT '',
|
||||
"expectedLimitGrossMarginRate" TEXT NOT NULL DEFAULT '',
|
||||
"nonSpuOwnerLimitPrice" TEXT NOT NULL DEFAULT '',
|
||||
"limitConfirmed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"limitConfirmedAt" TIMESTAMP(3),
|
||||
"source" TEXT NOT NULL DEFAULT 'PAYLOAD_SYNC',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IntakeNonIntegratedProductLimitRow_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_productId_skuItemId_key"
|
||||
ON "IntakeNonIntegratedProductLimitRow"("productId", "skuItemId");
|
||||
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_productId_idx"
|
||||
ON "IntakeNonIntegratedProductLimitRow"("productId");
|
||||
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_skuCode_idx"
|
||||
ON "IntakeNonIntegratedProductLimitRow"("skuCode");
|
||||
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_barcode_idx"
|
||||
ON "IntakeNonIntegratedProductLimitRow"("barcode");
|
||||
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProductLimitRow_limitConfirmed_idx"
|
||||
ON "IntakeNonIntegratedProductLimitRow"("limitConfirmed");
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedProductLimitRow"
|
||||
DROP CONSTRAINT IF EXISTS "IntakeNonIntegratedProductLimitRow_productId_fkey",
|
||||
ADD CONSTRAINT "IntakeNonIntegratedProductLimitRow_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedProductLimitRow"
|
||||
DROP CONSTRAINT IF EXISTS "IntakeNonIntegratedProductLimitRow_product_sku_item_fkey",
|
||||
ADD CONSTRAINT "IntakeNonIntegratedProductLimitRow_product_sku_item_fkey"
|
||||
FOREIGN KEY ("productId", "skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("productId", "skuItemId")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
WITH limit_payload AS (
|
||||
SELECT
|
||||
product."id" AS product_id,
|
||||
product."payload"::jsonb AS payload_json,
|
||||
CASE
|
||||
WHEN product."payload"::jsonb -> 'documentWorkspaceConfirmations' ->> '产品限价' = 'true'
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS limit_confirmed,
|
||||
CASE
|
||||
WHEN COALESCE(product."payload"::jsonb -> 'documentWorkspaceApprovedAt' ->> '产品限价', '') ~ '^\d{4}-\d{2}-\d{2}T'
|
||||
THEN (product."payload"::jsonb -> 'documentWorkspaceApprovedAt' ->> '产品限价')::timestamp(3)
|
||||
ELSE NULL
|
||||
END AS limit_confirmed_at
|
||||
FROM "IntakeNonIntegratedProduct" product
|
||||
),
|
||||
limit_rows AS (
|
||||
SELECT
|
||||
sku."productId" AS product_id,
|
||||
sku."skuItemId" AS sku_item_id,
|
||||
sku."skuCode" AS sku_code,
|
||||
sku."barcode" AS barcode,
|
||||
limit_payload.limit_confirmed,
|
||||
limit_payload.limit_confirmed_at,
|
||||
limit_item.item_json,
|
||||
sku."createdAt" AS created_at,
|
||||
sku."updatedAt" AS updated_at
|
||||
FROM "IntakeNonIntegratedSkuItem" sku
|
||||
JOIN limit_payload
|
||||
ON limit_payload.product_id = sku."productId"
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item AS item_json
|
||||
FROM jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(limit_payload.payload_json -> 'productLimitRows') = 'array'
|
||||
THEN limit_payload.payload_json -> 'productLimitRows'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS item
|
||||
WHERE item ->> 'skuId' = sku."skuItemId"
|
||||
LIMIT 1
|
||||
) limit_item ON true
|
||||
)
|
||||
INSERT INTO "IntakeNonIntegratedProductLimitRow" (
|
||||
"id",
|
||||
"productId",
|
||||
"skuItemId",
|
||||
"skuCode",
|
||||
"barcode",
|
||||
"expectedDailyLimitPrice",
|
||||
"expectedLimitGrossMarginRate",
|
||||
"nonSpuOwnerLimitPrice",
|
||||
"limitConfirmed",
|
||||
"limitConfirmedAt",
|
||||
"source",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
product_id || ':' || sku_item_id,
|
||||
product_id,
|
||||
sku_item_id,
|
||||
COALESCE(sku_code, ''),
|
||||
COALESCE(barcode, ''),
|
||||
COALESCE(NULLIF(btrim(item_json ->> 'expectedDailyLimitPrice'), ''), ''),
|
||||
COALESCE(NULLIF(btrim(item_json ->> 'expectedLimitGrossMarginRate'), ''), ''),
|
||||
COALESCE(NULLIF(btrim(item_json ->> 'nonSpuOwnerLimitPrice'), ''), ''),
|
||||
limit_confirmed,
|
||||
limit_confirmed_at,
|
||||
'PAYLOAD_SYNC',
|
||||
created_at,
|
||||
updated_at
|
||||
FROM limit_rows
|
||||
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
|
||||
|
||||
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_product_limit_rows_on_soft_delete"()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
DELETE FROM "IntakeNonIntegratedProductLimitRow"
|
||||
WHERE "productId" = NEW."id";
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS "IntakeNonIntegratedProduct_soft_delete_cleanup_product_limit_rows"
|
||||
ON "IntakeNonIntegratedProduct";
|
||||
|
||||
CREATE TRIGGER "IntakeNonIntegratedProduct_soft_delete_cleanup_product_limit_rows"
|
||||
AFTER UPDATE OF "deletedAt" ON "IntakeNonIntegratedProduct"
|
||||
FOR EACH ROW
|
||||
WHEN (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL)
|
||||
EXECUTE FUNCTION "cleanup_intake_non_integrated_product_limit_rows_on_soft_delete"();
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_product_limits_v1 AS
|
||||
WITH limit_source AS (
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."brand" AS brand,
|
||||
profile."productDevelopmentKind" AS product_development_kind,
|
||||
profile."processTemplate" AS process_template,
|
||||
profile."productStatus" AS product_status,
|
||||
sku."skuItemId" AS intake_sku_item_id,
|
||||
sku."skuSequence" AS sku_sequence,
|
||||
sku."batchNo" AS batch_no,
|
||||
sku."skuCode" AS sku_code,
|
||||
sku."barcode" AS barcode,
|
||||
limits."expectedDailyLimitPrice" AS expected_daily_limit_price_raw,
|
||||
limits."expectedLimitGrossMarginRate" AS expected_limit_gross_margin_rate_raw,
|
||||
limits."nonSpuOwnerLimitPrice" AS non_spu_owner_limit_price_raw,
|
||||
limits."limitConfirmed" AS limit_confirmed_raw,
|
||||
limits."limitConfirmedAt" AS limit_confirmed_at_raw,
|
||||
limits."createdAt" AS created_at,
|
||||
limits."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN btrim(COALESCE(profile."processTemplate", '')) IN ('存量商品', '存量商品明细')
|
||||
OR btrim(COALESCE(profile."productStatus", '')) IN ('存量商品', '存量商品明细')
|
||||
THEN false
|
||||
WHEN btrim(COALESCE(profile."productDevelopmentKind", '')) IN ('product', 'series')
|
||||
OR btrim(COALESCE(profile."processTemplate", '')) IN ('产品开发明细', '模块化产品明细')
|
||||
OR btrim(COALESCE(profile."productStatus", '')) IN ('产品开发明细', '模块化产品明细')
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS limit_price_applicable
|
||||
FROM "IntakeNonIntegratedProductLimitRow" limits
|
||||
JOIN "IntakeNonIntegratedSkuItem" sku
|
||||
ON sku."productId" = limits."productId"
|
||||
AND sku."skuItemId" = limits."skuItemId"
|
||||
JOIN "IntakeNonIntegratedProduct" product
|
||||
ON product."id" = limits."productId"
|
||||
LEFT JOIN "IntakeNonIntegratedProductProfile" profile
|
||||
ON profile."productId" = product."id"
|
||||
WHERE product."deletedAt" IS NULL
|
||||
)
|
||||
SELECT
|
||||
intake_product_id,
|
||||
spu,
|
||||
brand,
|
||||
product_development_kind,
|
||||
process_template,
|
||||
product_status,
|
||||
intake_sku_item_id,
|
||||
sku_sequence,
|
||||
batch_no,
|
||||
sku_code,
|
||||
barcode,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(brand, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(spu, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(brand)) || ':' || upper(btrim(spu))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(brand, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(spu, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(brand)) || ':' || upper(btrim(spu)) || ':' || upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
limit_price_applicable,
|
||||
CASE
|
||||
WHEN limit_price_applicable
|
||||
AND limit_confirmed_raw
|
||||
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS limit_price_confirmed,
|
||||
CASE
|
||||
WHEN NOT limit_price_applicable THEN 'NOT_APPLICABLE'
|
||||
WHEN limit_confirmed_raw
|
||||
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
|
||||
THEN 'CONFIRMED'
|
||||
ELSE 'PENDING'
|
||||
END AS limit_price_status,
|
||||
CASE WHEN limit_price_applicable THEN expected_daily_limit_price_raw ELSE NULL END AS expected_daily_limit_price,
|
||||
CASE WHEN limit_price_applicable THEN expected_limit_gross_margin_rate_raw ELSE NULL END AS expected_limit_gross_margin_rate,
|
||||
CASE WHEN limit_price_applicable THEN non_spu_owner_limit_price_raw ELSE NULL END AS non_spu_owner_limit_price,
|
||||
CASE
|
||||
WHEN limit_price_applicable
|
||||
AND limit_confirmed_raw
|
||||
AND NULLIF(btrim(COALESCE(expected_daily_limit_price_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(expected_limit_gross_margin_rate_raw, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(non_spu_owner_limit_price_raw, '')), '') IS NOT NULL
|
||||
THEN limit_confirmed_at_raw
|
||||
ELSE NULL
|
||||
END AS limit_confirmed_at,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM limit_source;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_skus_v1 AS
|
||||
SELECT
|
||||
p."id" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
p."name" AS product_name,
|
||||
p."description" AS product_description,
|
||||
pc."id" AS product_category_id,
|
||||
pc."name" AS product_category_name,
|
||||
p."thumbnailUrl" AS product_thumbnail_url,
|
||||
p."modelUrl" AS product_model_url,
|
||||
sku."id" AS sku_id,
|
||||
sku."skuCode" AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(sku."name", ''),
|
||||
NULLIF(profile."specName", ''),
|
||||
sku."skuCode"
|
||||
) AS sku_name,
|
||||
sku."price" AS price,
|
||||
sku."costPrice" AS cost_price,
|
||||
sku."stock" AS stock,
|
||||
sku."isActive" AS is_active,
|
||||
CASE WHEN sku."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
|
||||
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url,
|
||||
COALESCE(
|
||||
NULLIF(profile."imageUrl", ''),
|
||||
sku."images"[1],
|
||||
NULLIF(p."thumbnailUrl", '')
|
||||
) AS image_url,
|
||||
sku."images" AS image_urls,
|
||||
sku."sourcePreSkuId" AS source_pre_sku_id,
|
||||
sku."sourceAssetVariantId" AS source_asset_variant_id,
|
||||
sku."sourceAssetVariantIds" AS source_asset_variant_ids,
|
||||
profile."model" AS asset_family_code,
|
||||
profile."modelName" AS asset_family_name,
|
||||
profile."specCode" AS spec_code,
|
||||
profile."specName" AS spec_name,
|
||||
profile."barcode" AS barcode,
|
||||
profile."size" AS size_json,
|
||||
profile."color" AS color,
|
||||
profile."material" AS material,
|
||||
profile."sourceStatus"::text AS source_status,
|
||||
profile."syncedAt"::text AS synced_at,
|
||||
wh."id" AS warehouse_location_id,
|
||||
wh."code" AS warehouse_location_code,
|
||||
wh."name" AS warehouse_location_name,
|
||||
sku."createdAt" AS created_at,
|
||||
sku."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(p."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(p."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(profile."barcode"))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "ProductCategory" pc ON pc."id" = p."categoryId"
|
||||
LEFT JOIN "WarehouseLocation" wh ON wh."id" = sku."warehouseLocationId"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_assets_v1 AS
|
||||
WITH image_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
image_item.image_url,
|
||||
image_item.sort_order
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
CROSS JOIN LATERAL unnest(sku."images") WITH ORDINALITY AS image_item(image_url, sort_order)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
),
|
||||
model_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
),
|
||||
thumbnail_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
COALESCE(
|
||||
NULLIF(profile."imageUrl", ''),
|
||||
NULLIF(p."thumbnailUrl", '')
|
||||
) AS thumbnail_url
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
)
|
||||
SELECT
|
||||
'SKU_IMAGE' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
image_url AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
sort_order::integer AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM image_assets
|
||||
WHERE NULLIF(image_url, '') IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
'SKU_MODEL' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
model_url AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
0 AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM model_assets
|
||||
WHERE NULLIF(model_url, '') IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
'SKU_THUMBNAIL' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
thumbnail_url AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
0 AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM thumbnail_assets
|
||||
WHERE NULLIF(thumbnail_url, '') IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_bom_v1 AS
|
||||
SELECT
|
||||
'PANEL' AS item_type,
|
||||
p."productId" AS product_id,
|
||||
p."skuId" AS sku_id,
|
||||
p."id" AS item_id,
|
||||
p."code" AS item_code,
|
||||
p."name" AS item_name,
|
||||
1::numeric AS quantity,
|
||||
'piece' AS unit,
|
||||
m."id" AS material_id,
|
||||
m."code" AS material_code,
|
||||
m."name" AS material_name,
|
||||
jsonb_build_object(
|
||||
'length', p."length",
|
||||
'width', p."width",
|
||||
'thickness', p."thickness",
|
||||
'unit', 'mm'
|
||||
) AS dimensions,
|
||||
NULL::numeric AS unit_cost,
|
||||
NULL::numeric AS total_cost,
|
||||
p."createdAt" AS created_at,
|
||||
p."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(profile."barcode"))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM "Panel" p
|
||||
JOIN "Product" product ON product."id" = p."productId"
|
||||
LEFT JOIN "ProductSKU" sku ON sku."id" = p."skuId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "PanelMaterial" pm ON pm."panelId" = p."id"
|
||||
LEFT JOIN "Material" m ON m."id" = pm."materialId"
|
||||
WHERE product."deletedAt" IS NULL
|
||||
AND (sku."id" IS NULL OR sku."deletedAt" IS NULL)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'HARDWARE' AS item_type,
|
||||
ph."productId" AS product_id,
|
||||
ph."skuId" AS sku_id,
|
||||
ph."id" AS item_id,
|
||||
NULLIF(btrim(COALESCE(h."code", ph."sourceBlockName", '')), '') AS item_code,
|
||||
COALESCE(h."name", ph."sourceBlockName", '未匹配五金') AS item_name,
|
||||
1::numeric AS quantity,
|
||||
'piece' AS unit,
|
||||
NULL::text AS material_id,
|
||||
NULL::text AS material_code,
|
||||
NULL::text AS material_name,
|
||||
h."dimensions" AS dimensions,
|
||||
NULL::numeric AS unit_cost,
|
||||
NULL::numeric AS total_cost,
|
||||
ph."createdAt" AS created_at,
|
||||
ph."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(profile."barcode"))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM "ProductHardware" ph
|
||||
JOIN "Product" product ON product."id" = ph."productId"
|
||||
LEFT JOIN "ProductSKU" sku ON sku."id" = ph."skuId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "Hardware" h ON h."id" = ph."hardwareId"
|
||||
WHERE product."deletedAt" IS NULL
|
||||
AND (sku."id" IS NULL OR sku."deletedAt" IS NULL);
|
||||
|
||||
CREATE OR REPLACE VIEW external.sku_warehouse_stock_v1 AS
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.barcode,
|
||||
sku.stock AS stock_quantity,
|
||||
sku.is_active AS sku_is_active,
|
||||
sku.record_status AS sku_record_status,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
wh.zone AS warehouse_zone,
|
||||
wh.aisle AS warehouse_aisle,
|
||||
wh.shelf AS warehouse_shelf,
|
||||
wh.bin AS warehouse_bin,
|
||||
wh.is_active AS warehouse_location_is_active,
|
||||
wh.record_status AS warehouse_location_status,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.barcode_business_key
|
||||
FROM external.product_skus_v1 sku
|
||||
LEFT JOIN external.warehouse_locations_v1 wh
|
||||
ON wh.warehouse_location_id = sku.warehouse_location_id;
|
||||
|
||||
CREATE OR REPLACE VIEW external.packaged_items_v1 AS
|
||||
SELECT
|
||||
item."id" AS packaged_item_id,
|
||||
item."code" AS packaged_item_code,
|
||||
item."name" AS packaged_item_name,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
product."id" AS product_id,
|
||||
product."modelNumber" AS product_model_number,
|
||||
product."name" AS product_name,
|
||||
sku."id" AS sku_id,
|
||||
sku."skuCode" AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(sku."name", ''),
|
||||
NULLIF(profile."specName", ''),
|
||||
sku."skuCode"
|
||||
) AS sku_name,
|
||||
profile."barcode" AS barcode,
|
||||
item."quantityPerPack" AS quantity_per_pack,
|
||||
item."stockQuantity" AS stock_quantity,
|
||||
item."length" AS length,
|
||||
item."width" AS width,
|
||||
item."height" AS height,
|
||||
item."weight" AS weight,
|
||||
item."status" AS status,
|
||||
item."warehouseLocationId" AS warehouse_location_id,
|
||||
wh."code" AS warehouse_location_code,
|
||||
wh."name" AS warehouse_location_name,
|
||||
wh."zone" AS warehouse_zone,
|
||||
wh."aisle" AS warehouse_aisle,
|
||||
wh."shelf" AS warehouse_shelf,
|
||||
wh."bin" AS warehouse_bin,
|
||||
wh."isActive" AS warehouse_location_is_active,
|
||||
item."createdAt" AS created_at,
|
||||
item."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL THEN upper(btrim(profile."barcode"))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM "PackagedItem" item
|
||||
LEFT JOIN "ProductSKU" sku
|
||||
ON sku."id" = item."skuId"
|
||||
AND sku."deletedAt" IS NULL
|
||||
LEFT JOIN "Product" product
|
||||
ON product."id" = sku."productId"
|
||||
AND product."deletedAt" IS NULL
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile
|
||||
ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "WarehouseLocation" wh
|
||||
ON wh."id" = item."warehouseLocationId"
|
||||
WHERE sku."id" IS NOT NULL
|
||||
AND product."id" IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.sku_quality_inspections_v1 AS
|
||||
SELECT
|
||||
inspection."id" AS quality_inspection_id,
|
||||
inspection."recordKey" AS record_key,
|
||||
inspection."sourceType" AS source_type,
|
||||
CASE
|
||||
WHEN inspection."sourceType" = 'FIRST_BATCH' THEN '首批质检'
|
||||
ELSE '仓库质检'
|
||||
END AS source_label,
|
||||
inspection."sourceProductId" AS source_product_id,
|
||||
inspection."sourceProductSpu" AS source_product_spu,
|
||||
inspection."sourceSkuItemId" AS source_sku_item_id,
|
||||
inspection."batchNo" AS batch_no,
|
||||
inspection."inspectedAt" AS inspected_at,
|
||||
inspection."inspectorName" AS inspector_name,
|
||||
inspection."conclusion" AS conclusion,
|
||||
inspection."detail" AS detail,
|
||||
inspection."inspectedQuantity" AS inspected_quantity,
|
||||
inspection."qualifiedQuantity" AS qualified_quantity,
|
||||
inspection."qualifiedRate" AS qualified_rate,
|
||||
inspection."status" AS status,
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.barcode,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
inspection."createdAt" AS created_at,
|
||||
inspection."updatedAt" AS updated_at,
|
||||
sku.barcode_business_key
|
||||
FROM "ProductSkuQualityInspection" inspection
|
||||
JOIN external.product_skus_v1 sku
|
||||
ON sku.sku_id = inspection."skuId";
|
||||
|
||||
DROP VIEW IF EXISTS external.product_catalog_wide_v1;
|
||||
|
||||
CREATE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH sku_source AS (
|
||||
SELECT
|
||||
sku.*
|
||||
FROM external.product_skus_v1 sku
|
||||
),
|
||||
asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (barcode_business_key)
|
||||
barcode_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
),
|
||||
limit_by_barcode AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(barcode)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
limit_by_sku AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(sku_code)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
)
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.barcode_business_key,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
|
||||
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
|
||||
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
|
||||
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
|
||||
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
|
||||
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
|
||||
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
|
||||
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
|
||||
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
|
||||
COALESCE(asset_summary.image_asset_count, 0) AS image_asset_count,
|
||||
COALESCE(asset_summary.model_asset_count, 0) AS model_asset_count,
|
||||
COALESCE(asset_summary.thumbnail_asset_count, 0) AS thumbnail_asset_count,
|
||||
asset_summary.first_model_asset_url,
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at
|
||||
FROM sku_source sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN limit_by_sku
|
||||
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
|
||||
LEFT JOIN limit_by_barcode
|
||||
ON limit_by_barcode.identity_value = sku.barcode_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.barcode_business_key = sku.barcode_business_key;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'external_reader') THEN
|
||||
GRANT USAGE ON SCHEMA external TO external_reader;
|
||||
GRANT SELECT ON external.product_catalog_wide_v1 TO external_reader;
|
||||
GRANT SELECT ON external.intake_product_limits_v1 TO external_reader;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'EXTERNAL_INTAKE';
|
||||
@ -0,0 +1,421 @@
|
||||
-- Expand the external product catalog wide view so external systems can
|
||||
-- consume ready non-integrated SKU rows before formal Product / ProductSKU
|
||||
-- records are materialized.
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH formal_sku_source AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.product_business_key,
|
||||
sku.sku_business_key,
|
||||
sku.barcode_business_key,
|
||||
'FORMAL'::text AS catalog_source
|
||||
FROM external.product_skus_v1 sku
|
||||
),
|
||||
formal_identity_map AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS normalized_sku_code
|
||||
FROM formal_sku_source
|
||||
),
|
||||
intake_ready_sku_source AS (
|
||||
SELECT
|
||||
product.intake_product_id AS product_id,
|
||||
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
|
||||
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
|
||||
NULL::text AS product_description,
|
||||
NULL::text AS product_category_id,
|
||||
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) AS product_thumbnail_url,
|
||||
NULL::text AS product_model_url,
|
||||
sku.intake_sku_item_id AS sku_id,
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
|
||||
) AS sku_name,
|
||||
NULL::numeric(10,2) AS price,
|
||||
NULL::numeric(10,2) AS cost_price,
|
||||
0::integer AS stock,
|
||||
true AS is_active,
|
||||
'ACTIVE'::text AS record_status,
|
||||
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) AS image_url,
|
||||
CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN ARRAY[
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
)
|
||||
]
|
||||
ELSE ARRAY[]::text[]
|
||||
END AS image_urls,
|
||||
NULL::text AS source_pre_sku_id,
|
||||
NULL::text AS source_asset_variant_id,
|
||||
ARRAY[]::text[] AS source_asset_variant_ids,
|
||||
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
|
||||
NULL::text AS asset_family_name,
|
||||
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
|
||||
THEN jsonb_build_object('label', btrim(sku.size))
|
||||
ELSE NULL::jsonb
|
||||
END AS size_json,
|
||||
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
|
||||
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
|
||||
NULL::text AS source_status,
|
||||
NULL::text AS synced_at,
|
||||
NULL::text AS warehouse_location_id,
|
||||
NULL::text AS warehouse_location_code,
|
||||
NULL::text AS warehouse_location_name,
|
||||
product.created_at AS created_at,
|
||||
product.updated_at AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product.spu))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key,
|
||||
'INTAKE_READY'::text AS catalog_source
|
||||
FROM external.intake_sku_items_ready_v1 sku
|
||||
JOIN external.intake_products_ready_v1 product
|
||||
ON product.intake_product_id = sku.intake_product_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM formal_identity_map formal
|
||||
WHERE (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
AND formal.barcode_business_key = upper(btrim(sku.barcode))
|
||||
)
|
||||
OR (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
|
||||
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
|
||||
)
|
||||
)
|
||||
),
|
||||
sku_source AS (
|
||||
SELECT * FROM formal_sku_source
|
||||
UNION ALL
|
||||
SELECT * FROM intake_ready_sku_source
|
||||
),
|
||||
asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (barcode_business_key)
|
||||
barcode_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
),
|
||||
limit_by_barcode AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(barcode)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
limit_by_sku AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(sku_code)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
)
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.barcode_business_key,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
|
||||
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
|
||||
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
|
||||
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
|
||||
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
|
||||
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
|
||||
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
|
||||
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
|
||||
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.image_asset_count, 0)
|
||||
END AS image_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.model_asset_count, 0)
|
||||
END AS model_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
|
||||
END AS thumbnail_asset_count,
|
||||
COALESCE(
|
||||
asset_summary.first_model_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_model_asset_url,
|
||||
COALESCE(
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at
|
||||
FROM sku_source sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN limit_by_sku
|
||||
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
|
||||
LEFT JOIN limit_by_barcode
|
||||
ON limit_by_barcode.identity_value = sku.barcode_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.barcode_business_key = sku.barcode_business_key;
|
||||
@ -0,0 +1,6 @@
|
||||
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_key";
|
||||
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_brand_key";
|
||||
DROP INDEX IF EXISTS "IntakeNonIntegratedProduct_spu_brand_normalized_key";
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IntakeNonIntegratedProduct_spu_idx"
|
||||
ON "IntakeNonIntegratedProduct"("spu");
|
||||
@ -0,0 +1,2 @@
|
||||
-- Keep IntakeReferenceLibraryItem SPU_RULE rows as the only SPU truth source.
|
||||
DROP TABLE IF EXISTS "SpuNumberCounter";
|
||||
@ -0,0 +1,175 @@
|
||||
-- Restore the AssetSkuGroup schema expected by the current asset-center code.
|
||||
-- The previous retirement migration preserved group data in JSONB legacy fields;
|
||||
-- this migration restores the relational table/column and backfills from that data.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "AssetSkuGroup" (
|
||||
"id" TEXT NOT NULL,
|
||||
"assetFamilyId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AssetSkuGroup_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
ADD COLUMN IF NOT EXISTS "skuGroupId" TEXT;
|
||||
|
||||
WITH legacy_family_groups AS (
|
||||
SELECT
|
||||
family.id AS "assetFamilyId",
|
||||
NULLIF(BTRIM(group_item ->> 'code'), '') AS code,
|
||||
NULLIF(BTRIM(group_item ->> 'name'), '') AS name,
|
||||
NULLIF(BTRIM(group_item ->> 'description'), '') AS description,
|
||||
CASE
|
||||
WHEN NULLIF(BTRIM(group_item ->> 'sortOrder'), '') ~ '^-?[0-9]+$'
|
||||
THEN NULLIF(BTRIM(group_item ->> 'sortOrder'), '')::INTEGER
|
||||
ELSE 0
|
||||
END AS "sortOrder",
|
||||
CASE
|
||||
WHEN jsonb_typeof(group_item -> 'metadata') = 'object' THEN group_item -> 'metadata'
|
||||
ELSE NULL
|
||||
END AS metadata,
|
||||
0 AS source_rank
|
||||
FROM "AssetFamily" AS family
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(family.metadata -> 'legacySkuGroups') = 'array'
|
||||
THEN family.metadata -> 'legacySkuGroups'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS group_item
|
||||
),
|
||||
legacy_variant_groups AS (
|
||||
SELECT
|
||||
variant."assetFamilyId",
|
||||
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'code'), '') AS code,
|
||||
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'name'), '') AS name,
|
||||
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'description'), '') AS description,
|
||||
CASE
|
||||
WHEN NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'sortOrder'), '') ~ '^-?[0-9]+$'
|
||||
THEN NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'sortOrder'), '')::INTEGER
|
||||
ELSE 0
|
||||
END AS "sortOrder",
|
||||
CASE
|
||||
WHEN jsonb_typeof(variant."businessData" -> 'legacySkuGroup' -> 'metadata') = 'object'
|
||||
THEN variant."businessData" -> 'legacySkuGroup' -> 'metadata'
|
||||
ELSE NULL
|
||||
END AS metadata,
|
||||
1 AS source_rank
|
||||
FROM "AssetVariant" AS variant
|
||||
WHERE jsonb_typeof(variant."businessData" -> 'legacySkuGroup') = 'object'
|
||||
),
|
||||
variant_code_groups AS (
|
||||
SELECT
|
||||
variant."assetFamilyId",
|
||||
NULLIF(BTRIM(variant.code), '') AS code,
|
||||
(ARRAY_AGG(COALESCE(NULLIF(BTRIM(variant.name), ''), variant.code) ORDER BY variant."createdAt" ASC, variant.id ASC))[1] AS name,
|
||||
NULL::TEXT AS description,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY variant."assetFamilyId"
|
||||
ORDER BY MIN(variant."createdAt") ASC, NULLIF(BTRIM(variant.code), '') ASC
|
||||
)::INTEGER AS "sortOrder",
|
||||
jsonb_build_object(
|
||||
'source',
|
||||
'variant-backfill-after-sku-group-restore',
|
||||
'sourceAssetVariantIds',
|
||||
jsonb_agg(variant.id ORDER BY variant."createdAt" ASC, variant.id ASC)
|
||||
) AS metadata,
|
||||
2 AS source_rank
|
||||
FROM "AssetVariant" AS variant
|
||||
WHERE NULLIF(BTRIM(variant.code), '') IS NOT NULL
|
||||
GROUP BY variant."assetFamilyId", NULLIF(BTRIM(variant.code), '')
|
||||
),
|
||||
candidate_groups AS (
|
||||
SELECT * FROM legacy_family_groups WHERE code IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT * FROM legacy_variant_groups WHERE code IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT * FROM variant_code_groups WHERE code IS NOT NULL
|
||||
),
|
||||
deduped_groups AS (
|
||||
SELECT DISTINCT ON ("assetFamilyId", code)
|
||||
CONCAT('asg_', MD5("assetFamilyId" || ':' || code)) AS id,
|
||||
"assetFamilyId",
|
||||
code,
|
||||
COALESCE(NULLIF(name, ''), code) AS name,
|
||||
description,
|
||||
"sortOrder",
|
||||
metadata
|
||||
FROM candidate_groups
|
||||
ORDER BY "assetFamilyId", code, source_rank ASC, "sortOrder" ASC, name ASC
|
||||
)
|
||||
INSERT INTO "AssetSkuGroup" (
|
||||
"id",
|
||||
"assetFamilyId",
|
||||
"code",
|
||||
"name",
|
||||
"description",
|
||||
"sortOrder",
|
||||
"metadata",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
"assetFamilyId",
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
"sortOrder",
|
||||
metadata,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM deduped_groups
|
||||
ON CONFLICT ("id") DO UPDATE
|
||||
SET
|
||||
"assetFamilyId" = EXCLUDED."assetFamilyId",
|
||||
code = EXCLUDED.code,
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description,
|
||||
"sortOrder" = EXCLUDED."sortOrder",
|
||||
metadata = EXCLUDED.metadata,
|
||||
"updatedAt" = CURRENT_TIMESTAMP;
|
||||
|
||||
UPDATE "AssetVariant" AS variant
|
||||
SET "skuGroupId" = sku_group.id
|
||||
FROM "AssetSkuGroup" AS sku_group
|
||||
WHERE variant."skuGroupId" IS NULL
|
||||
AND sku_group."assetFamilyId" = variant."assetFamilyId"
|
||||
AND sku_group.code = COALESCE(
|
||||
NULLIF(BTRIM(variant."businessData" -> 'legacySkuGroup' ->> 'code'), ''),
|
||||
NULLIF(BTRIM(variant.code), '')
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AssetSkuGroup_assetFamilyId_code_key"
|
||||
ON "AssetSkuGroup"("assetFamilyId", "code");
|
||||
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_assetFamilyId_idx" ON "AssetSkuGroup"("assetFamilyId");
|
||||
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_code_idx" ON "AssetSkuGroup"("code");
|
||||
CREATE INDEX IF NOT EXISTS "AssetSkuGroup_sortOrder_idx" ON "AssetSkuGroup"("sortOrder");
|
||||
CREATE INDEX IF NOT EXISTS "AssetVariant_skuGroupId_idx" ON "AssetVariant"("skuGroupId");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'AssetSkuGroup_assetFamilyId_fkey'
|
||||
) THEN
|
||||
ALTER TABLE "AssetSkuGroup"
|
||||
ADD CONSTRAINT "AssetSkuGroup_assetFamilyId_fkey"
|
||||
FOREIGN KEY ("assetFamilyId") REFERENCES "AssetFamily"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'AssetVariant_skuGroupId_fkey'
|
||||
) THEN
|
||||
ALTER TABLE "AssetVariant"
|
||||
ADD CONSTRAINT "AssetVariant_skuGroupId_fkey"
|
||||
FOREIGN KEY ("skuGroupId") REFERENCES "AssetSkuGroup"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "User"
|
||||
ADD COLUMN "supplierCode" TEXT,
|
||||
ADD COLUMN "supplierName" TEXT;
|
||||
|
||||
CREATE INDEX "User_supplierCode_idx" ON "User"("supplierCode");
|
||||
@ -0,0 +1,37 @@
|
||||
CREATE TABLE IF NOT EXISTS "BarcodePrintImportRecord" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"skuCode" TEXT NOT NULL,
|
||||
"normalizedSku" TEXT NOT NULL,
|
||||
"supplierCode" TEXT NOT NULL DEFAULT '',
|
||||
"supplierName" TEXT NOT NULL DEFAULT '',
|
||||
"sourceFileName" TEXT NOT NULL DEFAULT '',
|
||||
"rowNumber" INTEGER NOT NULL DEFAULT 0,
|
||||
"importedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updates" JSONB NOT NULL,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "BarcodePrintImportRecord_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "BarcodePrintImportRecord_kind_normalizedSku_supplierCode_key"
|
||||
ON "BarcodePrintImportRecord"("kind", "normalizedSku", "supplierCode");
|
||||
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_kind_idx" ON "BarcodePrintImportRecord"("kind");
|
||||
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_normalizedSku_idx" ON "BarcodePrintImportRecord"("normalizedSku");
|
||||
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_supplierCode_idx" ON "BarcodePrintImportRecord"("supplierCode");
|
||||
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_createdBy_idx" ON "BarcodePrintImportRecord"("createdBy");
|
||||
CREATE INDEX IF NOT EXISTS "BarcodePrintImportRecord_createdAt_idx" ON "BarcodePrintImportRecord"("createdAt");
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'BarcodePrintImportRecord_createdBy_fkey'
|
||||
) THEN
|
||||
ALTER TABLE "BarcodePrintImportRecord"
|
||||
ADD CONSTRAINT "BarcodePrintImportRecord_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
@ -0,0 +1,67 @@
|
||||
CREATE TABLE "AiRenderWorkbenchSpu" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL DEFAULT '',
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AiRenderWorkbenchSpu_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "AiRenderWorkbenchSku" (
|
||||
"id" TEXT NOT NULL,
|
||||
"spuId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"modelUrl" TEXT NOT NULL DEFAULT '',
|
||||
"thumbnailUrl" TEXT,
|
||||
"note" TEXT,
|
||||
"materialColor" TEXT NOT NULL DEFAULT '#d8c6ad',
|
||||
"materialTextureUrl" TEXT,
|
||||
"nodeMaterials" JSONB,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AiRenderWorkbenchSku_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD COLUMN "workbenchSpuId" TEXT,
|
||||
ADD COLUMN "workbenchSkuId" TEXT;
|
||||
|
||||
CREATE INDEX "AiRenderWorkbenchSpu_createdBy_idx" ON "AiRenderWorkbenchSpu"("createdBy");
|
||||
CREATE INDEX "AiRenderWorkbenchSpu_sortOrder_createdAt_idx" ON "AiRenderWorkbenchSpu"("sortOrder", "createdAt");
|
||||
CREATE INDEX "AiRenderWorkbenchSku_spuId_sortOrder_createdAt_idx" ON "AiRenderWorkbenchSku"("spuId", "sortOrder", "createdAt");
|
||||
CREATE INDEX "AiRenderWorkbenchSku_createdBy_idx" ON "AiRenderWorkbenchSku"("createdBy");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_workbenchSpuId_createdAt_idx" ON "AssistedAssemblyAiRender"("workbenchSpuId", "createdAt");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_workbenchSkuId_createdAt_idx" ON "AssistedAssemblyAiRender"("workbenchSkuId", "createdAt");
|
||||
|
||||
ALTER TABLE "AiRenderWorkbenchSpu"
|
||||
ADD CONSTRAINT "AiRenderWorkbenchSpu_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AiRenderWorkbenchSku"
|
||||
ADD CONSTRAINT "AiRenderWorkbenchSku_spuId_fkey"
|
||||
FOREIGN KEY ("spuId") REFERENCES "AiRenderWorkbenchSpu"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AiRenderWorkbenchSku"
|
||||
ADD CONSTRAINT "AiRenderWorkbenchSku_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD CONSTRAINT "AssistedAssemblyAiRender_workbenchSpuId_fkey"
|
||||
FOREIGN KEY ("workbenchSpuId") REFERENCES "AiRenderWorkbenchSpu"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD CONSTRAINT "AssistedAssemblyAiRender_workbenchSkuId_fkey"
|
||||
FOREIGN KEY ("workbenchSkuId") REFERENCES "AiRenderWorkbenchSku"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@ -0,0 +1,84 @@
|
||||
-- 20260613151000 temporarily restored AssetSkuGroup for legacy-data recovery.
|
||||
-- The current schema deliberately keeps this grouping as JSON compatibility data,
|
||||
-- so replaying the migration chain must retire the transient relational shape again.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('public."AssetSkuGroup"') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'AssetVariant'
|
||||
AND column_name = 'skuGroupId'
|
||||
)
|
||||
THEN
|
||||
EXECUTE $SQL$
|
||||
UPDATE "AssetVariant" AS variant
|
||||
SET "businessData" = jsonb_set(
|
||||
CASE
|
||||
WHEN variant."businessData" IS NULL OR jsonb_typeof(variant."businessData") <> 'object'
|
||||
THEN '{}'::jsonb
|
||||
ELSE variant."businessData"
|
||||
END,
|
||||
'{legacySkuGroup}',
|
||||
jsonb_build_object(
|
||||
'id', sku_group.id,
|
||||
'code', sku_group.code,
|
||||
'name', sku_group.name,
|
||||
'description', sku_group.description,
|
||||
'sortOrder', sku_group."sortOrder",
|
||||
'metadata', sku_group.metadata,
|
||||
'assetFamilyId', sku_group."assetFamilyId",
|
||||
'source', 'AssetSkuGroup.replay-aligned'
|
||||
),
|
||||
true
|
||||
)
|
||||
FROM "AssetSkuGroup" AS sku_group
|
||||
WHERE variant."skuGroupId" = sku_group.id
|
||||
$SQL$;
|
||||
|
||||
EXECUTE $SQL$
|
||||
UPDATE "AssetFamily" AS family
|
||||
SET metadata = jsonb_set(
|
||||
CASE
|
||||
WHEN family.metadata IS NULL OR jsonb_typeof(family.metadata) <> 'object'
|
||||
THEN '{}'::jsonb
|
||||
ELSE family.metadata
|
||||
END,
|
||||
'{legacySkuGroups}',
|
||||
grouped.groups,
|
||||
true
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
"assetFamilyId",
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', id,
|
||||
'code', code,
|
||||
'name', name,
|
||||
'description', description,
|
||||
'sortOrder', "sortOrder",
|
||||
'metadata', metadata,
|
||||
'source', 'AssetSkuGroup.replay-aligned'
|
||||
)
|
||||
ORDER BY "sortOrder", code
|
||||
) AS groups
|
||||
FROM "AssetSkuGroup"
|
||||
GROUP BY "assetFamilyId"
|
||||
) AS grouped
|
||||
WHERE family.id = grouped."assetFamilyId"
|
||||
$SQL$;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
DROP CONSTRAINT IF EXISTS "AssetVariant_skuGroupId_fkey";
|
||||
|
||||
DROP INDEX IF EXISTS "AssetVariant_skuGroupId_idx";
|
||||
|
||||
ALTER TABLE "AssetVariant"
|
||||
DROP COLUMN IF EXISTS "skuGroupId";
|
||||
|
||||
DROP TABLE IF EXISTS "AssetSkuGroup";
|
||||
@ -0,0 +1,445 @@
|
||||
-- 把宽表 product_catalog_wide_v1 的口径从"一 SKU 一行"明确保护成"一条码一行"。
|
||||
-- 原视图 FROM sku_source 的行粒度是 SKU,barcode 只是字段;当前 43261 行 barcode 全唯一是数据巧合,
|
||||
-- 一旦出现"一条码多 SKU"(正式 ProductSKU 落库 + intake ready 同条码 / intake 内部同条码多行),
|
||||
-- 宽表就会重复,污染外部系统的 barcode 主同步键。
|
||||
-- 修复:复用项目已有的 DISTINCT ON(barcode_business_key) 模式(latest_quality / limit_by_barcode CTE),
|
||||
-- 把原主查询整体包成 catalog_by_sku CTE,外层加 DISTINCT ON (barcode_business_key)。
|
||||
-- 优先级:FORMAL(正式商品)> INTAKE_READY(开发阶段),同源内 updated_at 最新 > created_at 最新。
|
||||
-- 用 CREATE OR REPLACE VIEW(不用 DROP),保留 external_reader 已有的 GRANT SELECT 权限。
|
||||
|
||||
-- Expand the external product catalog wide view so external systems can
|
||||
-- consume ready non-integrated SKU rows before formal Product / ProductSKU
|
||||
-- records are materialized.
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH formal_sku_source AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.product_business_key,
|
||||
sku.sku_business_key,
|
||||
sku.barcode_business_key,
|
||||
'FORMAL'::text AS catalog_source
|
||||
FROM external.product_skus_v1 sku
|
||||
),
|
||||
formal_identity_map AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS normalized_sku_code
|
||||
FROM formal_sku_source
|
||||
),
|
||||
intake_ready_sku_source AS (
|
||||
SELECT
|
||||
product.intake_product_id AS product_id,
|
||||
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
|
||||
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
|
||||
NULL::text AS product_description,
|
||||
NULL::text AS product_category_id,
|
||||
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) AS product_thumbnail_url,
|
||||
NULL::text AS product_model_url,
|
||||
sku.intake_sku_item_id AS sku_id,
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
|
||||
) AS sku_name,
|
||||
NULL::numeric(10,2) AS price,
|
||||
NULL::numeric(10,2) AS cost_price,
|
||||
0::integer AS stock,
|
||||
true AS is_active,
|
||||
'ACTIVE'::text AS record_status,
|
||||
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) AS image_url,
|
||||
CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN ARRAY[
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
)
|
||||
]
|
||||
ELSE ARRAY[]::text[]
|
||||
END AS image_urls,
|
||||
NULL::text AS source_pre_sku_id,
|
||||
NULL::text AS source_asset_variant_id,
|
||||
ARRAY[]::text[] AS source_asset_variant_ids,
|
||||
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
|
||||
NULL::text AS asset_family_name,
|
||||
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
|
||||
THEN jsonb_build_object('label', btrim(sku.size))
|
||||
ELSE NULL::jsonb
|
||||
END AS size_json,
|
||||
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
|
||||
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
|
||||
NULL::text AS source_status,
|
||||
NULL::text AS synced_at,
|
||||
NULL::text AS warehouse_location_id,
|
||||
NULL::text AS warehouse_location_code,
|
||||
NULL::text AS warehouse_location_name,
|
||||
product.created_at AS created_at,
|
||||
product.updated_at AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product.spu))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key,
|
||||
'INTAKE_READY'::text AS catalog_source
|
||||
FROM external.intake_sku_items_ready_v1 sku
|
||||
JOIN external.intake_products_ready_v1 product
|
||||
ON product.intake_product_id = sku.intake_product_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM formal_identity_map formal
|
||||
WHERE (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
AND formal.barcode_business_key = upper(btrim(sku.barcode))
|
||||
)
|
||||
OR (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
|
||||
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
|
||||
)
|
||||
)
|
||||
),
|
||||
sku_source AS (
|
||||
SELECT * FROM formal_sku_source
|
||||
UNION ALL
|
||||
SELECT * FROM intake_ready_sku_source
|
||||
),
|
||||
asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (barcode_business_key)
|
||||
barcode_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
),
|
||||
limit_by_barcode AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(barcode)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
limit_by_sku AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(sku_code)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
catalog_by_sku AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.barcode_business_key,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
|
||||
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
|
||||
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
|
||||
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
|
||||
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
|
||||
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
|
||||
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
|
||||
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
|
||||
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.image_asset_count, 0)
|
||||
END AS image_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.model_asset_count, 0)
|
||||
END AS model_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
|
||||
END AS thumbnail_asset_count,
|
||||
COALESCE(
|
||||
asset_summary.first_model_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_model_asset_url,
|
||||
COALESCE(
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.catalog_source
|
||||
FROM sku_source sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN limit_by_sku
|
||||
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
|
||||
LEFT JOIN limit_by_barcode
|
||||
ON limit_by_barcode.identity_value = sku.barcode_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.barcode_business_key = sku.barcode_business_key
|
||||
)
|
||||
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
|
||||
catalog_by_sku.*
|
||||
FROM catalog_by_sku
|
||||
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
|
||||
AND btrim(catalog_by_sku.barcode_business_key) <> ''
|
||||
ORDER BY
|
||||
catalog_by_sku.barcode_business_key,
|
||||
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
|
||||
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
|
||||
ELSE 1 END,
|
||||
catalog_by_sku.updated_at DESC NULLS LAST,
|
||||
catalog_by_sku.created_at DESC;
|
||||
@ -0,0 +1,390 @@
|
||||
-- Normalize asset URLs exposed by external read-only views.
|
||||
--
|
||||
-- Business tables still store stable in-app object routes such as
|
||||
-- /api/storage/object?key=... . External database consumers need complete
|
||||
-- URLs, so the external contract layer prefixes relative application routes
|
||||
-- with the current production site origin. The routes remain application
|
||||
-- controlled; this migration does not expose private bucket objects directly.
|
||||
|
||||
CREATE OR REPLACE FUNCTION external.to_external_asset_url(raw_value text)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
WITH normalized AS (
|
||||
SELECT NULLIF(btrim(raw_value), '') AS value
|
||||
),
|
||||
base_url AS (
|
||||
SELECT COALESCE(
|
||||
NULLIF(btrim(current_setting('app.external_asset_base_url', true)), ''),
|
||||
NULLIF(btrim(current_setting('app.public_base_url', true)), ''),
|
||||
'https://cp.jiansun.vip'
|
||||
) AS value
|
||||
)
|
||||
SELECT
|
||||
CASE
|
||||
WHEN normalized.value IS NULL THEN NULL
|
||||
WHEN normalized.value ~* '^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?/' THEN
|
||||
regexp_replace(base_url.value, '/+$', '') ||
|
||||
regexp_replace(
|
||||
normalized.value,
|
||||
'^https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?',
|
||||
'',
|
||||
'i'
|
||||
)
|
||||
WHEN normalized.value LIKE '/%' THEN regexp_replace(base_url.value, '/+$', '') || normalized.value
|
||||
ELSE normalized.value
|
||||
END
|
||||
FROM normalized, base_url;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE VIEW external.materials_v1 AS
|
||||
SELECT
|
||||
m."id" AS material_id,
|
||||
m."code" AS material_code,
|
||||
m."name" AS material_name,
|
||||
c."name" AS category_name,
|
||||
external.to_external_asset_url(m."thumbnailUrl") AS thumbnail_url,
|
||||
external.to_external_asset_url(m."textureUrl") AS texture_url,
|
||||
external.to_external_asset_url(m."normalMapUrl") AS normal_map_url,
|
||||
m."color" AS base_color,
|
||||
m."metalness" AS metalness,
|
||||
m."roughness" AS roughness,
|
||||
m."opacity" AS opacity,
|
||||
CASE WHEN m."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status
|
||||
FROM "Material" m
|
||||
JOIN "MaterialCategory" c ON c."id" = m."categoryId";
|
||||
|
||||
CREATE OR REPLACE VIEW external.asset_variants_v1 AS
|
||||
SELECT
|
||||
v."id" AS asset_variant_id,
|
||||
v."code" AS spec_code,
|
||||
v."name" AS spec_name,
|
||||
v."assetType"::text AS asset_type,
|
||||
v."status"::text AS asset_status,
|
||||
f."id" AS asset_family_id,
|
||||
f."code" AS asset_family_code,
|
||||
f."name" AS asset_family_name,
|
||||
s."id" AS asset_series_id,
|
||||
s."code" AS asset_series_code,
|
||||
s."name" AS asset_series_name,
|
||||
pc."id" AS product_category_id,
|
||||
pc."name" AS product_category_name,
|
||||
v."sizeLabel" AS size_label,
|
||||
v."dimensions" AS dimensions,
|
||||
external.to_external_asset_url(v."modelUrl") AS model_url,
|
||||
external.to_external_asset_url(v."thumbnailUrl") AS thumbnail_url,
|
||||
v."barcode" AS barcode,
|
||||
v."supplierName" AS supplier_name,
|
||||
v."supplierCode" AS supplier_code,
|
||||
v."versionNo" AS version_no,
|
||||
sub."id" AS substrate_id,
|
||||
sub."name" AS substrate_name,
|
||||
finish."id" AS finish_id,
|
||||
finish."code" AS finish_code,
|
||||
finish."name" AS finish_name,
|
||||
CASE
|
||||
WHEN sub."name" IS NOT NULL AND finish."name" IS NOT NULL THEN sub."name" || ' + ' || finish."name"
|
||||
ELSE COALESCE(sub."name", finish."name")
|
||||
END AS material_label,
|
||||
v."weightKg" AS weight_kg,
|
||||
v."costPrice" AS cost_price,
|
||||
v."estimatedCostPrice" AS estimated_cost_price,
|
||||
v."createdAt" AS created_at,
|
||||
v."updatedAt" AS updated_at
|
||||
FROM "AssetVariant" v
|
||||
JOIN "AssetFamily" f ON f."id" = v."assetFamilyId"
|
||||
LEFT JOIN "AssetSeries" s ON s."id" = f."assetSeriesId"
|
||||
LEFT JOIN "ProductCategory" pc ON pc."id" = f."productCategoryId"
|
||||
LEFT JOIN "Substrate" sub ON sub."id" = v."substrateId"
|
||||
LEFT JOIN "Material" finish ON finish."id" = v."finishId"
|
||||
WHERE v."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_skus_v1 AS
|
||||
SELECT
|
||||
p."id" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
p."name" AS product_name,
|
||||
p."description" AS product_description,
|
||||
pc."id" AS product_category_id,
|
||||
pc."name" AS product_category_name,
|
||||
external.to_external_asset_url(p."thumbnailUrl") AS product_thumbnail_url,
|
||||
external.to_external_asset_url(p."modelUrl") AS product_model_url,
|
||||
sku."id" AS sku_id,
|
||||
sku."skuCode" AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(sku."name", ''),
|
||||
NULLIF(profile."specName", ''),
|
||||
sku."skuCode"
|
||||
) AS sku_name,
|
||||
sku."price" AS price,
|
||||
sku."costPrice" AS cost_price,
|
||||
sku."stock" AS stock,
|
||||
sku."isActive" AS is_active,
|
||||
CASE WHEN sku."isActive" THEN 'ACTIVE' ELSE 'INACTIVE' END AS record_status,
|
||||
external.to_external_asset_url(
|
||||
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", ''))
|
||||
) AS model_url,
|
||||
external.to_external_asset_url(
|
||||
COALESCE(
|
||||
NULLIF(profile."imageUrl", ''),
|
||||
sku."images"[1],
|
||||
NULLIF(p."thumbnailUrl", '')
|
||||
)
|
||||
) AS image_url,
|
||||
ARRAY(
|
||||
SELECT external.to_external_asset_url(image_item.image_url)
|
||||
FROM unnest(sku."images") AS image_item(image_url)
|
||||
WHERE NULLIF(btrim(image_item.image_url), '') IS NOT NULL
|
||||
) AS image_urls,
|
||||
sku."sourcePreSkuId" AS source_pre_sku_id,
|
||||
sku."sourceAssetVariantId" AS source_asset_variant_id,
|
||||
sku."sourceAssetVariantIds" AS source_asset_variant_ids,
|
||||
profile."model" AS asset_family_code,
|
||||
profile."modelName" AS asset_family_name,
|
||||
profile."specCode" AS spec_code,
|
||||
profile."specName" AS spec_name,
|
||||
profile."barcode" AS barcode,
|
||||
profile."size" AS size_json,
|
||||
profile."color" AS color,
|
||||
profile."material" AS material,
|
||||
profile."sourceStatus"::text AS source_status,
|
||||
profile."syncedAt"::text AS synced_at,
|
||||
wh."id" AS warehouse_location_id,
|
||||
wh."code" AS warehouse_location_code,
|
||||
wh."name" AS warehouse_location_name,
|
||||
sku."createdAt" AS created_at,
|
||||
sku."updatedAt" AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(p."modelNumber", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(p."modelNumber"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(profile."barcode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(profile."barcode"))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
LEFT JOIN "ProductCategory" pc ON pc."id" = p."categoryId"
|
||||
LEFT JOIN "WarehouseLocation" wh ON wh."id" = sku."warehouseLocationId"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_assets_v1 AS
|
||||
WITH image_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
image_item.image_url,
|
||||
image_item.sort_order
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
CROSS JOIN LATERAL unnest(sku."images") WITH ORDINALITY AS image_item(image_url, sort_order)
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
),
|
||||
model_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
COALESCE(NULLIF(sku."modelUrl", ''), NULLIF(p."modelUrl", '')) AS model_url
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
),
|
||||
thumbnail_assets AS (
|
||||
SELECT
|
||||
sku."id" AS sku_id,
|
||||
sku."productId" AS product_id,
|
||||
p."modelNumber" AS product_model_number,
|
||||
sku."skuCode" AS sku_code,
|
||||
profile."barcode" AS barcode,
|
||||
COALESCE(
|
||||
NULLIF(profile."imageUrl", ''),
|
||||
NULLIF(p."thumbnailUrl", '')
|
||||
) AS thumbnail_url
|
||||
FROM "ProductSKU" sku
|
||||
JOIN "Product" p ON p."id" = sku."productId"
|
||||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = sku."id"
|
||||
WHERE p."deletedAt" IS NULL
|
||||
AND sku."deletedAt" IS NULL
|
||||
)
|
||||
SELECT
|
||||
'SKU_IMAGE' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
external.to_external_asset_url(image_url) AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
sort_order::integer AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM image_assets
|
||||
WHERE NULLIF(image_url, '') IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
'SKU_MODEL' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
external.to_external_asset_url(model_url) AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
0 AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM model_assets
|
||||
WHERE NULLIF(model_url, '') IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
'SKU_THUMBNAIL' AS asset_type,
|
||||
product_id,
|
||||
sku_id,
|
||||
external.to_external_asset_url(thumbnail_url) AS asset_url,
|
||||
NULL::text AS file_name,
|
||||
NULL::text AS mime_type,
|
||||
0 AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product_model_number, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product_model_number))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key
|
||||
FROM thumbnail_assets
|
||||
WHERE NULLIF(thumbnail_url, '') IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_products_v1 AS
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."spuBase" AS spu_base,
|
||||
product."brand" AS brand,
|
||||
profile."productName" AS product_name,
|
||||
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
|
||||
profile."productCategory" AS product_category,
|
||||
profile."salesCategory" AS sales_category,
|
||||
profile."productYear" AS product_year,
|
||||
profile."productStatus" AS product_status,
|
||||
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
|
||||
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
|
||||
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
|
||||
profile."productDevelopmentKind" AS product_development_kind,
|
||||
profile."goodsType" AS goods_type,
|
||||
profile."materialType" AS material_type,
|
||||
profile."productionType" AS production_type,
|
||||
product."createdAt" AS created_at,
|
||||
product."updatedAt" AS updated_at,
|
||||
profile."productPlanning" AS product_planning,
|
||||
profile."visualPlanning" AS visual_planning,
|
||||
profile."structureDesigner" AS structure_designer,
|
||||
profile."purchaseOwner" AS purchase_owner,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
|
||||
ELSE NULL
|
||||
END AS product_business_key
|
||||
FROM "IntakeNonIntegratedProduct" product
|
||||
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
|
||||
WHERE product."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_sku_items_v1 AS
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."brand" AS brand,
|
||||
sku."skuItemId" AS intake_sku_item_id,
|
||||
sku."skuSequence" AS sku_sequence,
|
||||
sku."batchNo" AS batch_no,
|
||||
external.to_external_asset_url(sku."image") AS image_url,
|
||||
sku."imageName" AS image_name,
|
||||
sku."model" AS model,
|
||||
sku."skuCode" AS sku_code,
|
||||
sku."specName" AS spec_name,
|
||||
sku."barcode" AS barcode,
|
||||
sku."size" AS size,
|
||||
sku."color" AS color,
|
||||
sku."material" AS material,
|
||||
sku."versionNo" AS version_no,
|
||||
sku."attributeCode" AS attribute_code,
|
||||
sku."drawingId" AS drawing_id,
|
||||
sku."installMethod" AS install_method,
|
||||
sku."executeStandard" AS execute_standard,
|
||||
sku."manufacturer" AS manufacturer,
|
||||
sku."manufacturerCode" AS manufacturer_code,
|
||||
sku."salesStatus" AS sales_status,
|
||||
external.to_external_asset_url(sku."model3dDataUrl") AS model_3d_url,
|
||||
sku."source" AS source,
|
||||
sku."sourceTable" AS source_table,
|
||||
sku."sourceSkuId" AS source_sku_id,
|
||||
sku."missingRequiredFields" AS missing_required_fields,
|
||||
sku."dataQualityStatus"::text AS data_quality_status,
|
||||
sku."contentLocked" AS content_locked,
|
||||
sku."sortOrder" AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu")) || ':' || upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key
|
||||
FROM "IntakeNonIntegratedSkuItem" sku
|
||||
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku."productId"
|
||||
WHERE product."deletedAt" IS NULL;
|
||||
@ -0,0 +1,510 @@
|
||||
-- Add explicit development-owner columns to the external catalog wide view.
|
||||
--
|
||||
-- The external main entry remains external.product_catalog_wide_v1. The new
|
||||
-- columns mirror the product-development list people fields and are appended to
|
||||
-- the existing wide view so current consumers keep their column order.
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_products_v1 AS
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."spuBase" AS spu_base,
|
||||
product."brand" AS brand,
|
||||
profile."productName" AS product_name,
|
||||
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
|
||||
profile."productCategory" AS product_category,
|
||||
profile."salesCategory" AS sales_category,
|
||||
profile."productYear" AS product_year,
|
||||
profile."productStatus" AS product_status,
|
||||
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
|
||||
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
|
||||
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
|
||||
profile."productDevelopmentKind" AS product_development_kind,
|
||||
profile."goodsType" AS goods_type,
|
||||
profile."materialType" AS material_type,
|
||||
profile."productionType" AS production_type,
|
||||
product."createdAt" AS created_at,
|
||||
product."updatedAt" AS updated_at,
|
||||
profile."productPlanning" AS product_planning,
|
||||
profile."visualPlanning" AS visual_planning,
|
||||
profile."structureDesigner" AS structure_designer,
|
||||
profile."purchaseOwner" AS purchase_owner,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
NULLIF(btrim(COALESCE(product."designer", '')), '') AS appearance_designer,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(profile."spuOwner", '')), ''),
|
||||
NULLIF(btrim(COALESCE(product."operator", '')), ''),
|
||||
NULLIF(btrim(COALESCE(profile."operationsOwner", '')), '')
|
||||
) AS spu_owner,
|
||||
NULLIF(btrim(COALESCE(profile."productPlanning", '')), '') AS planning_owner,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(profile."visualPlanning", '')), ''),
|
||||
NULLIF(btrim(COALESCE(profile."visualDirector", '')), '')
|
||||
) AS space_designer
|
||||
FROM "IntakeNonIntegratedProduct" product
|
||||
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
|
||||
WHERE product."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_products_ready_v1 AS
|
||||
SELECT *
|
||||
FROM external.intake_products_v1
|
||||
WHERE NULLIF(btrim(COALESCE(product_name, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_category, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_development_kind, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_business_key, '')), '') IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH formal_sku_source AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.product_business_key,
|
||||
sku.sku_business_key,
|
||||
sku.barcode_business_key,
|
||||
'FORMAL'::text AS catalog_source,
|
||||
NULL::text AS appearance_designer,
|
||||
NULL::text AS structure_designer,
|
||||
NULL::text AS spu_owner,
|
||||
NULL::text AS planning_owner,
|
||||
NULL::text AS space_designer,
|
||||
NULL::text AS purchase_owner
|
||||
FROM external.product_skus_v1 sku
|
||||
),
|
||||
formal_identity_map AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS normalized_sku_code
|
||||
FROM formal_sku_source
|
||||
),
|
||||
intake_ready_sku_source AS (
|
||||
SELECT
|
||||
product.intake_product_id AS product_id,
|
||||
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
|
||||
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
|
||||
NULL::text AS product_description,
|
||||
NULL::text AS product_category_id,
|
||||
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) AS product_thumbnail_url,
|
||||
NULL::text AS product_model_url,
|
||||
sku.intake_sku_item_id AS sku_id,
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
|
||||
) AS sku_name,
|
||||
NULL::numeric(10,2) AS price,
|
||||
NULL::numeric(10,2) AS cost_price,
|
||||
0::integer AS stock,
|
||||
true AS is_active,
|
||||
'ACTIVE'::text AS record_status,
|
||||
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) AS image_url,
|
||||
CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN ARRAY[
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
)
|
||||
]
|
||||
ELSE ARRAY[]::text[]
|
||||
END AS image_urls,
|
||||
NULL::text AS source_pre_sku_id,
|
||||
NULL::text AS source_asset_variant_id,
|
||||
ARRAY[]::text[] AS source_asset_variant_ids,
|
||||
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
|
||||
NULL::text AS asset_family_name,
|
||||
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
|
||||
THEN jsonb_build_object('label', btrim(sku.size))
|
||||
ELSE NULL::jsonb
|
||||
END AS size_json,
|
||||
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
|
||||
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
|
||||
NULL::text AS source_status,
|
||||
NULL::text AS synced_at,
|
||||
NULL::text AS warehouse_location_id,
|
||||
NULL::text AS warehouse_location_code,
|
||||
NULL::text AS warehouse_location_name,
|
||||
product.created_at AS created_at,
|
||||
product.updated_at AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product.spu))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key,
|
||||
'INTAKE_READY'::text AS catalog_source,
|
||||
product.appearance_designer,
|
||||
product.structure_designer,
|
||||
product.spu_owner,
|
||||
product.planning_owner,
|
||||
product.space_designer,
|
||||
product.purchase_owner
|
||||
FROM external.intake_sku_items_ready_v1 sku
|
||||
JOIN external.intake_products_ready_v1 product
|
||||
ON product.intake_product_id = sku.intake_product_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM formal_identity_map formal
|
||||
WHERE (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
AND formal.barcode_business_key = upper(btrim(sku.barcode))
|
||||
)
|
||||
OR (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
|
||||
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
|
||||
)
|
||||
)
|
||||
),
|
||||
sku_source AS (
|
||||
SELECT * FROM formal_sku_source
|
||||
UNION ALL
|
||||
SELECT * FROM intake_ready_sku_source
|
||||
),
|
||||
asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (barcode_business_key)
|
||||
barcode_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
),
|
||||
limit_by_barcode AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(barcode)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
limit_by_sku AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(sku_code)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
catalog_by_sku AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.barcode_business_key,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
|
||||
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
|
||||
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
|
||||
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
|
||||
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
|
||||
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
|
||||
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
|
||||
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
|
||||
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.image_asset_count, 0)
|
||||
END AS image_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.model_asset_count, 0)
|
||||
END AS model_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
|
||||
END AS thumbnail_asset_count,
|
||||
COALESCE(
|
||||
asset_summary.first_model_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_model_asset_url,
|
||||
COALESCE(
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.catalog_source,
|
||||
sku.appearance_designer,
|
||||
sku.structure_designer,
|
||||
sku.spu_owner,
|
||||
sku.planning_owner,
|
||||
sku.space_designer,
|
||||
sku.purchase_owner
|
||||
FROM sku_source sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN limit_by_sku
|
||||
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
|
||||
LEFT JOIN limit_by_barcode
|
||||
ON limit_by_barcode.identity_value = sku.barcode_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.barcode_business_key = sku.barcode_business_key
|
||||
)
|
||||
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
|
||||
catalog_by_sku.*
|
||||
FROM catalog_by_sku
|
||||
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
|
||||
AND btrim(catalog_by_sku.barcode_business_key) <> ''
|
||||
ORDER BY
|
||||
catalog_by_sku.barcode_business_key,
|
||||
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
|
||||
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
|
||||
ELSE 1 END,
|
||||
catalog_by_sku.updated_at DESC NULLS LAST,
|
||||
catalog_by_sku.created_at DESC;
|
||||
@ -0,0 +1,16 @@
|
||||
CREATE TABLE "InstallationDemoConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"drawingId" TEXT NOT NULL,
|
||||
"parts" JSONB NOT NULL,
|
||||
"steps" JSONB NOT NULL,
|
||||
"numbering" JSONB,
|
||||
"uploadedModel" JSONB,
|
||||
"explodeTemplate" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "InstallationDemoConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "InstallationDemoConfig_drawingId_key"
|
||||
ON "InstallationDemoConfig"("drawingId");
|
||||
@ -0,0 +1,26 @@
|
||||
ALTER TABLE "IntakeNonIntegratedSkuItem"
|
||||
ADD COLUMN "demandFlowNo" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "purchaseOwner" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE "IntakeNonIntegratedSkuItem" AS sku
|
||||
SET
|
||||
"demandFlowNo" = COALESCE(
|
||||
NULLIF(BTRIM(product."payload" -> 'skuBatchFlowNos' ->> sku."batchNo"::text), ''),
|
||||
NULLIF(BTRIM(profile."demandFlowNo"), ''),
|
||||
''
|
||||
),
|
||||
"purchaseOwner" = COALESCE(
|
||||
NULLIF(BTRIM(profile."purchaseOwner"), ''),
|
||||
NULLIF(BTRIM(product."purchaser"), ''),
|
||||
''
|
||||
)
|
||||
FROM "IntakeNonIntegratedProduct" AS product
|
||||
LEFT JOIN "IntakeNonIntegratedProductProfile" AS profile
|
||||
ON profile."productId" = product."id"
|
||||
WHERE sku."productId" = product."id";
|
||||
|
||||
CREATE INDEX "IntakeNonIntegratedSkuItem_productId_purchaseOwner_idx"
|
||||
ON "IntakeNonIntegratedSkuItem"("productId", "purchaseOwner");
|
||||
|
||||
CREATE INDEX "IntakeNonIntegratedSkuItem_demandFlowNo_idx"
|
||||
ON "IntakeNonIntegratedSkuItem"("demandFlowNo");
|
||||
@ -0,0 +1,12 @@
|
||||
-- Prisma @updatedAt columns are maintained by application writes, not by a
|
||||
-- database default. Align fresh migration replay with the current schema and
|
||||
-- the existing local database.
|
||||
|
||||
ALTER TABLE "AiRenderWorkbenchSku"
|
||||
ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
|
||||
ALTER TABLE "AiRenderWorkbenchSpu"
|
||||
ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
|
||||
ALTER TABLE "BarcodePrintImportRecord"
|
||||
ALTER COLUMN "updatedAt" DROP DEFAULT;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'OPERATIONS';
|
||||
@ -0,0 +1,6 @@
|
||||
UPDATE "User" AS u
|
||||
SET "businessRoles" = ARRAY['OPERATIONS'::"BusinessAccountType"]
|
||||
FROM "Department" AS d
|
||||
WHERE u."departmentId" = d."id"
|
||||
AND cardinality(u."businessRoles") = 0
|
||||
AND regexp_replace(d."fullPath", '^\s*[^>]+>\s*', '') LIKE '运营中心%';
|
||||
@ -0,0 +1,9 @@
|
||||
UPDATE "User" AS u
|
||||
SET "businessRoles" = CASE
|
||||
WHEN cardinality(u."businessRoles") = 0 THEN ARRAY['OPERATIONS'::"BusinessAccountType"]
|
||||
ELSE array_append(u."businessRoles", 'OPERATIONS'::"BusinessAccountType")
|
||||
END
|
||||
FROM "Department" AS d
|
||||
WHERE u."departmentId" = d."id"
|
||||
AND split_part(d."fullPath", ' > ', 2) LIKE '运营中心%'
|
||||
AND NOT ('OPERATIONS'::"BusinessAccountType" = ANY(u."businessRoles"));
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'GENERAL';
|
||||
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'STRATEGY';
|
||||
@ -0,0 +1,30 @@
|
||||
UPDATE "User" AS u
|
||||
SET "businessRoles" = CASE
|
||||
WHEN cardinality(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType")) = 0 THEN ARRAY['PLANNING'::"BusinessAccountType"]
|
||||
ELSE array_append(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType"), 'PLANNING'::"BusinessAccountType")
|
||||
END
|
||||
FROM "Department" AS d
|
||||
WHERE u."departmentId" = d."id"
|
||||
AND split_part(d."fullPath", ' > ', 2) = '品牌中心'
|
||||
AND (
|
||||
d."fullPath" LIKE '% > 空间视觉部%'
|
||||
OR d."fullPath" LIKE '% > 商品企划部%'
|
||||
)
|
||||
AND NOT ('PLANNING'::"BusinessAccountType" = ANY(u."businessRoles"));
|
||||
|
||||
UPDATE "User" AS u
|
||||
SET "businessRoles" = CASE
|
||||
WHEN cardinality(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType")) = 0 THEN ARRAY['STRATEGY'::"BusinessAccountType"]
|
||||
ELSE array_append(array_remove(u."businessRoles", 'GENERAL'::"BusinessAccountType"), 'STRATEGY'::"BusinessAccountType")
|
||||
END
|
||||
FROM "Department" AS d
|
||||
WHERE u."departmentId" = d."id"
|
||||
AND split_part(d."fullPath", ' > ', 2) = '品牌中心'
|
||||
AND d."fullPath" NOT LIKE '% > 空间视觉部%'
|
||||
AND d."fullPath" NOT LIKE '% > 商品企划部%'
|
||||
AND NOT ('STRATEGY'::"BusinessAccountType" = ANY(u."businessRoles"));
|
||||
|
||||
UPDATE "User"
|
||||
SET "businessRoles" = ARRAY['GENERAL'::"BusinessAccountType"]
|
||||
WHERE cardinality("businessRoles") = 0
|
||||
AND (NULLIF(BTRIM(COALESCE("supplierCode", '')), '') IS NULL);
|
||||
@ -0,0 +1 @@
|
||||
ALTER TYPE "BusinessAccountType" ADD VALUE IF NOT EXISTS 'CUSTOMER_SERVICE';
|
||||
@ -0,0 +1,20 @@
|
||||
WITH customer_service_department AS (
|
||||
SELECT U&'\5BA2\670D\4E2D\5FC3'::text AS name
|
||||
)
|
||||
UPDATE "User" AS u
|
||||
SET "businessRoles" = CASE
|
||||
WHEN cardinality(array_remove(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[]), 'GENERAL'::"BusinessAccountType")) = 0
|
||||
THEN ARRAY['CUSTOMER_SERVICE'::"BusinessAccountType"]
|
||||
ELSE array_append(
|
||||
array_remove(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[]), 'GENERAL'::"BusinessAccountType"),
|
||||
'CUSTOMER_SERVICE'::"BusinessAccountType"
|
||||
)
|
||||
END
|
||||
FROM "Department" AS d, customer_service_department AS cs
|
||||
WHERE u."departmentId" = d."id"
|
||||
AND (
|
||||
d."fullPath" = cs.name
|
||||
OR split_part(d."fullPath", ' > ', 2) = cs.name
|
||||
OR d."fullPath" LIKE ('% > ' || cs.name || '%')
|
||||
)
|
||||
AND NOT ('CUSTOMER_SERVICE'::"BusinessAccountType" = ANY(COALESCE(u."businessRoles", ARRAY[]::"BusinessAccountType"[])));
|
||||
@ -0,0 +1,13 @@
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD COLUMN "jstVirtualInventoryRetryCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "jstVirtualInventoryLastAttemptAt" TIMESTAMP(3);
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedPricingRow"
|
||||
ADD CONSTRAINT "IntakeNonIntegratedPricingRow_virtual_inventory_retry_count_valid"
|
||||
CHECK ("jstVirtualInventoryRetryCount" >= 0);
|
||||
|
||||
CREATE INDEX "IntakeNonIntegratedPricingRow_virtual_inventory_due_idx"
|
||||
ON "IntakeNonIntegratedPricingRow" (
|
||||
"jstVirtualInventoryStatus",
|
||||
"jstVirtualInventoryScheduledAt"
|
||||
);
|
||||
@ -0,0 +1,38 @@
|
||||
CREATE TYPE "OperationLogAction" AS ENUM (
|
||||
'INTAKE_PRODUCT_WRITE',
|
||||
'JUSHUITAN_PRODUCT_PUSH',
|
||||
'JUSHUITAN_VIRTUAL_INVENTORY_PUSH'
|
||||
);
|
||||
|
||||
CREATE TYPE "OperationLogStatus" AS ENUM ('SUCCESS', 'FAILED', 'RETRY');
|
||||
|
||||
CREATE TYPE "OperationLogSource" AS ENUM ('USER', 'SYSTEM');
|
||||
|
||||
CREATE TABLE "OperationLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"action" "OperationLogAction" NOT NULL,
|
||||
"status" "OperationLogStatus" NOT NULL,
|
||||
"source" "OperationLogSource" NOT NULL DEFAULT 'USER',
|
||||
"actorId" TEXT,
|
||||
"actorName" TEXT NOT NULL,
|
||||
"productId" TEXT,
|
||||
"productSpu" TEXT,
|
||||
"skuId" TEXT,
|
||||
"skuCode" TEXT,
|
||||
"workspaceCategory" TEXT,
|
||||
"errorCode" TEXT,
|
||||
"message" TEXT NOT NULL,
|
||||
"requestId" TEXT,
|
||||
"details" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OperationLog_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "OperationLog_actorId_fkey"
|
||||
FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "OperationLog_status_createdAt_idx" ON "OperationLog"("status", "createdAt");
|
||||
CREATE INDEX "OperationLog_action_createdAt_idx" ON "OperationLog"("action", "createdAt");
|
||||
CREATE INDEX "OperationLog_actorId_createdAt_idx" ON "OperationLog"("actorId", "createdAt");
|
||||
CREATE INDEX "OperationLog_productSpu_createdAt_idx" ON "OperationLog"("productSpu", "createdAt");
|
||||
CREATE INDEX "OperationLog_productId_createdAt_idx" ON "OperationLog"("productId", "createdAt");
|
||||
@ -0,0 +1,659 @@
|
||||
-- Move product-factory fields out of the compatibility payload and into the
|
||||
-- structured non-integrated SKU truth. The payload is used only for this
|
||||
-- one-time backfill; runtime reads and the external contract use columns.
|
||||
|
||||
ALTER TABLE "IntakeNonIntegratedSkuItem"
|
||||
ADD COLUMN IF NOT EXISTS "factoryWarehouse" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS "shippingMethod" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS "minimumOrderQuantity" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS "taxIncludedUnitPrice" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS "leadTime" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
WITH factory_rows AS (
|
||||
SELECT DISTINCT ON (product."id", factory_row.item ->> 'skuId')
|
||||
product."id" AS product_id,
|
||||
factory_row.item ->> 'skuId' AS sku_item_id,
|
||||
factory_row.item
|
||||
FROM "IntakeNonIntegratedProduct" product
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
CASE
|
||||
WHEN jsonb_typeof(product."payload" -> 'factoryRows') = 'array'
|
||||
THEN product."payload" -> 'factoryRows'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS factory_row(item, ordinal_position)
|
||||
WHERE NULLIF(btrim(COALESCE(factory_row.item ->> 'skuId', '')), '') IS NOT NULL
|
||||
ORDER BY product."id", factory_row.item ->> 'skuId', factory_row.ordinal_position DESC
|
||||
)
|
||||
UPDATE "IntakeNonIntegratedSkuItem" sku
|
||||
SET
|
||||
"manufacturer" = COALESCE(
|
||||
NULLIF(btrim(sku."manufacturer"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'supplierName', ''))
|
||||
),
|
||||
"manufacturerCode" = COALESCE(
|
||||
NULLIF(btrim(sku."manufacturerCode"), ''),
|
||||
upper(btrim(COALESCE(factory.item ->> 'supplierCode', '')))
|
||||
),
|
||||
"installMethod" = COALESCE(
|
||||
NULLIF(btrim(sku."installMethod"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'installMethod', ''))
|
||||
),
|
||||
"factoryWarehouse" = COALESCE(
|
||||
NULLIF(btrim(sku."factoryWarehouse"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'warehouse', ''))
|
||||
),
|
||||
"shippingMethod" = COALESCE(
|
||||
NULLIF(btrim(sku."shippingMethod"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'shippingMethod', ''))
|
||||
),
|
||||
"minimumOrderQuantity" = COALESCE(
|
||||
NULLIF(btrim(sku."minimumOrderQuantity"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'minimumOrderQuantity', ''))
|
||||
),
|
||||
"taxIncludedUnitPrice" = COALESCE(
|
||||
NULLIF(btrim(sku."taxIncludedUnitPrice"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'taxIncludedUnitPrice', ''))
|
||||
),
|
||||
"leadTime" = COALESCE(
|
||||
NULLIF(btrim(sku."leadTime"), ''),
|
||||
btrim(COALESCE(factory.item ->> 'leadTime', ''))
|
||||
)
|
||||
FROM factory_rows factory
|
||||
WHERE sku."productId" = factory.product_id
|
||||
AND sku."skuItemId" = factory.sku_item_id;
|
||||
|
||||
COMMENT ON COLUMN "IntakeNonIntegratedSkuItem"."factoryWarehouse" IS
|
||||
'Product-factory shipping warehouse; not the inventory warehouse location.';
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_sku_items_v1 AS
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."brand" AS brand,
|
||||
sku."skuItemId" AS intake_sku_item_id,
|
||||
sku."skuSequence" AS sku_sequence,
|
||||
sku."batchNo" AS batch_no,
|
||||
external.to_external_asset_url(sku."image") AS image_url,
|
||||
sku."imageName" AS image_name,
|
||||
sku."model" AS model,
|
||||
sku."skuCode" AS sku_code,
|
||||
sku."specName" AS spec_name,
|
||||
sku."barcode" AS barcode,
|
||||
sku."size" AS size,
|
||||
sku."color" AS color,
|
||||
sku."material" AS material,
|
||||
sku."versionNo" AS version_no,
|
||||
sku."attributeCode" AS attribute_code,
|
||||
sku."drawingId" AS drawing_id,
|
||||
sku."installMethod" AS install_method,
|
||||
sku."executeStandard" AS execute_standard,
|
||||
sku."manufacturer" AS manufacturer,
|
||||
sku."manufacturerCode" AS manufacturer_code,
|
||||
sku."salesStatus" AS sales_status,
|
||||
external.to_external_asset_url(sku."model3dDataUrl") AS model_3d_url,
|
||||
sku."source" AS source,
|
||||
sku."sourceTable" AS source_table,
|
||||
sku."sourceSkuId" AS source_sku_id,
|
||||
sku."missingRequiredFields" AS missing_required_fields,
|
||||
sku."dataQualityStatus"::text AS data_quality_status,
|
||||
sku."contentLocked" AS content_locked,
|
||||
sku."sortOrder" AS sort_order,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(sku."skuCode", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu")) || ':' || upper(btrim(sku."skuCode"))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
sku."factoryWarehouse" AS factory_warehouse,
|
||||
sku."shippingMethod" AS shipping_method,
|
||||
sku."minimumOrderQuantity" AS minimum_order_quantity,
|
||||
sku."taxIncludedUnitPrice" AS tax_included_unit_price,
|
||||
sku."leadTime" AS lead_time
|
||||
FROM "IntakeNonIntegratedSkuItem" sku
|
||||
JOIN "IntakeNonIntegratedProduct" product ON product."id" = sku."productId"
|
||||
WHERE product."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_sku_items_ready_v1 AS
|
||||
SELECT sku.*
|
||||
FROM external.intake_sku_items_v1 sku
|
||||
JOIN external.intake_products_ready_v1 product
|
||||
ON product.intake_product_id = sku.intake_product_id
|
||||
WHERE sku.data_quality_status = 'READY'
|
||||
AND NULLIF(btrim(COALESCE(sku.sku_business_key, '')), '') IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_products_v1 AS
|
||||
SELECT
|
||||
product."id" AS intake_product_id,
|
||||
product."spu" AS spu,
|
||||
product."spuBase" AS spu_base,
|
||||
product."brand" AS brand,
|
||||
profile."productName" AS product_name,
|
||||
external.to_external_asset_url(profile."productImageUrl") AS product_image_url,
|
||||
profile."productCategory" AS product_category,
|
||||
profile."salesCategory" AS sales_category,
|
||||
profile."productYear" AS product_year,
|
||||
profile."productStatus" AS product_status,
|
||||
COALESCE(NULLIF(profile."structureDesigner", ''), NULLIF(product."designer", '')) AS designer,
|
||||
COALESCE(NULLIF(profile."productPlanning", ''), NULLIF(product."operator", '')) AS operator,
|
||||
COALESCE(NULLIF(profile."purchaseOwner", ''), NULLIF(product."purchaser", '')) AS purchaser,
|
||||
profile."productDevelopmentKind" AS product_development_kind,
|
||||
profile."goodsType" AS goods_type,
|
||||
profile."materialType" AS material_type,
|
||||
profile."productionType" AS production_type,
|
||||
product."createdAt" AS created_at,
|
||||
product."updatedAt" AS updated_at,
|
||||
profile."productPlanning" AS product_planning,
|
||||
profile."visualPlanning" AS visual_planning,
|
||||
profile."structureDesigner" AS structure_designer,
|
||||
profile."purchaseOwner" AS purchase_owner,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product."brand", '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product."spu", '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product."brand")) || ':' || upper(btrim(product."spu"))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
NULLIF(btrim(COALESCE(product."designer", '')), '') AS appearance_designer,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(profile."spuOwner", '')), ''),
|
||||
NULLIF(btrim(COALESCE(product."operator", '')), ''),
|
||||
NULLIF(btrim(COALESCE(profile."operationsOwner", '')), '')
|
||||
) AS spu_owner,
|
||||
NULLIF(btrim(COALESCE(profile."productPlanning", '')), '') AS planning_owner,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(profile."visualPlanning", '')), ''),
|
||||
NULLIF(btrim(COALESCE(profile."visualDirector", '')), '')
|
||||
) AS space_designer
|
||||
FROM "IntakeNonIntegratedProduct" product
|
||||
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
|
||||
WHERE product."deletedAt" IS NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.intake_products_ready_v1 AS
|
||||
SELECT *
|
||||
FROM external.intake_products_v1
|
||||
WHERE NULLIF(btrim(COALESCE(product_name, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_category, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_development_kind, '')), '') IS NOT NULL
|
||||
AND NULLIF(btrim(COALESCE(product_business_key, '')), '') IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE VIEW external.product_catalog_wide_v1 AS
|
||||
WITH formal_sku_source AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.product_business_key,
|
||||
sku.sku_business_key,
|
||||
sku.barcode_business_key,
|
||||
'FORMAL'::text AS catalog_source,
|
||||
NULL::text AS appearance_designer,
|
||||
NULL::text AS structure_designer,
|
||||
NULL::text AS spu_owner,
|
||||
NULL::text AS planning_owner,
|
||||
NULL::text AS space_designer,
|
||||
NULL::text AS purchase_owner,
|
||||
NULL::text AS supplier_name,
|
||||
NULL::text AS supplier_code,
|
||||
NULL::text AS factory_warehouse,
|
||||
NULL::text AS shipping_method,
|
||||
NULL::text AS minimum_order_quantity,
|
||||
NULL::text AS tax_included_unit_price,
|
||||
NULL::text AS lead_time,
|
||||
NULL::text AS install_method
|
||||
FROM external.product_skus_v1 sku
|
||||
),
|
||||
formal_identity_map AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku_code))
|
||||
ELSE NULL
|
||||
END AS normalized_sku_code
|
||||
FROM formal_sku_source
|
||||
),
|
||||
intake_ready_sku_source AS (
|
||||
SELECT
|
||||
product.intake_product_id AS product_id,
|
||||
NULLIF(btrim(COALESCE(product.spu, '')), '') AS product_model_number,
|
||||
NULLIF(btrim(COALESCE(product.product_name, '')), '') AS product_name,
|
||||
NULL::text AS product_description,
|
||||
NULL::text AS product_category_id,
|
||||
NULLIF(btrim(COALESCE(product.product_category, '')), '') AS product_category_name,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) AS product_thumbnail_url,
|
||||
NULL::text AS product_model_url,
|
||||
sku.intake_sku_item_id AS sku_id,
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '') AS sku_code,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.sku_code, '')), '')
|
||||
) AS sku_name,
|
||||
NULL::numeric(10,2) AS price,
|
||||
NULL::numeric(10,2) AS cost_price,
|
||||
0::integer AS stock,
|
||||
true AS is_active,
|
||||
'ACTIVE'::text AS record_status,
|
||||
NULLIF(btrim(COALESCE(sku.model_3d_url, '')), '') AS model_url,
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) AS image_url,
|
||||
CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN ARRAY[
|
||||
COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(product.product_image_url, '')), '')
|
||||
)
|
||||
]
|
||||
ELSE ARRAY[]::text[]
|
||||
END AS image_urls,
|
||||
NULL::text AS source_pre_sku_id,
|
||||
NULL::text AS source_asset_variant_id,
|
||||
ARRAY[]::text[] AS source_asset_variant_ids,
|
||||
NULLIF(btrim(COALESCE(sku.model, '')), '') AS asset_family_code,
|
||||
NULL::text AS asset_family_name,
|
||||
NULLIF(btrim(COALESCE(sku.attribute_code, '')), '') AS spec_code,
|
||||
NULLIF(btrim(COALESCE(sku.spec_name, '')), '') AS spec_name,
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') AS barcode,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.size, '')), '') IS NOT NULL
|
||||
THEN jsonb_build_object('label', btrim(sku.size))
|
||||
ELSE NULL::jsonb
|
||||
END AS size_json,
|
||||
NULLIF(btrim(COALESCE(sku.color, '')), '') AS color,
|
||||
NULLIF(btrim(COALESCE(sku.material, '')), '') AS material,
|
||||
NULL::text AS source_status,
|
||||
NULL::text AS synced_at,
|
||||
NULL::text AS warehouse_location_id,
|
||||
NULL::text AS warehouse_location_code,
|
||||
NULL::text AS warehouse_location_name,
|
||||
product.created_at AS created_at,
|
||||
product.updated_at AS updated_at,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(product.spu, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(product.spu))
|
||||
ELSE NULL
|
||||
END AS product_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.sku_code))
|
||||
ELSE NULL
|
||||
END AS sku_business_key,
|
||||
CASE
|
||||
WHEN NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
THEN upper(btrim(sku.barcode))
|
||||
ELSE NULL
|
||||
END AS barcode_business_key,
|
||||
'INTAKE_READY'::text AS catalog_source,
|
||||
product.appearance_designer,
|
||||
product.structure_designer,
|
||||
product.spu_owner,
|
||||
product.planning_owner,
|
||||
product.space_designer,
|
||||
product.purchase_owner,
|
||||
NULLIF(btrim(COALESCE(sku.manufacturer, '')), '') AS supplier_name,
|
||||
NULLIF(btrim(COALESCE(sku.manufacturer_code, '')), '') AS supplier_code,
|
||||
NULLIF(btrim(COALESCE(sku.factory_warehouse, '')), '') AS factory_warehouse,
|
||||
NULLIF(btrim(COALESCE(sku.shipping_method, '')), '') AS shipping_method,
|
||||
NULLIF(btrim(COALESCE(sku.minimum_order_quantity, '')), '') AS minimum_order_quantity,
|
||||
NULLIF(btrim(COALESCE(sku.tax_included_unit_price, '')), '') AS tax_included_unit_price,
|
||||
NULLIF(btrim(COALESCE(sku.lead_time, '')), '') AS lead_time,
|
||||
NULLIF(btrim(COALESCE(sku.install_method, '')), '') AS install_method
|
||||
FROM external.intake_sku_items_ready_v1 sku
|
||||
JOIN external.intake_products_ready_v1 product
|
||||
ON product.intake_product_id = sku.intake_product_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM formal_identity_map formal
|
||||
WHERE (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NOT NULL
|
||||
AND formal.barcode_business_key = upper(btrim(sku.barcode))
|
||||
)
|
||||
OR (
|
||||
NULLIF(btrim(COALESCE(sku.barcode, '')), '') IS NULL
|
||||
AND NULLIF(btrim(COALESCE(sku.sku_code, '')), '') IS NOT NULL
|
||||
AND formal.normalized_sku_code = upper(btrim(sku.sku_code))
|
||||
)
|
||||
)
|
||||
),
|
||||
sku_source AS (
|
||||
SELECT * FROM formal_sku_source
|
||||
UNION ALL
|
||||
SELECT * FROM intake_ready_sku_source
|
||||
),
|
||||
asset_summary AS (
|
||||
SELECT
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_IMAGE')::integer AS image_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_MODEL')::integer AS model_asset_count,
|
||||
COUNT(*) FILTER (WHERE asset_type = 'SKU_THUMBNAIL')::integer AS thumbnail_asset_count,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_MODEL') AS first_model_asset_url,
|
||||
MIN(asset_url) FILTER (WHERE asset_type = 'SKU_THUMBNAIL') AS first_thumbnail_asset_url
|
||||
FROM external.product_assets_v1
|
||||
GROUP BY product_business_key, sku_business_key
|
||||
),
|
||||
packaged_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS packaged_item_count,
|
||||
COALESCE(SUM(stock_quantity), 0)::integer AS packaged_stock_quantity
|
||||
FROM external.packaged_items_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
quality_summary AS (
|
||||
SELECT
|
||||
barcode_business_key,
|
||||
COUNT(*)::integer AS quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'WAREHOUSE')::integer AS warehouse_quality_record_count,
|
||||
COUNT(*) FILTER (WHERE source_type = 'FIRST_BATCH')::integer AS first_batch_quality_record_count
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
GROUP BY barcode_business_key
|
||||
),
|
||||
latest_quality AS (
|
||||
SELECT DISTINCT ON (barcode_business_key)
|
||||
barcode_business_key,
|
||||
record_key AS latest_quality_record_key,
|
||||
source_type AS latest_quality_source_type,
|
||||
batch_no AS latest_quality_batch_no,
|
||||
inspected_at AS latest_quality_inspected_at,
|
||||
conclusion AS latest_quality_conclusion,
|
||||
status AS latest_quality_status
|
||||
FROM external.sku_quality_inspections_v1
|
||||
WHERE NULLIF(barcode_business_key, '') IS NOT NULL
|
||||
ORDER BY barcode_business_key, COALESCE(inspected_at, updated_at, created_at) DESC, created_at DESC
|
||||
),
|
||||
limit_by_barcode AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(barcode)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(barcode, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
limit_by_sku AS (
|
||||
SELECT DISTINCT ON (identity_value)
|
||||
identity_value,
|
||||
product_business_key AS limit_source_product_business_key,
|
||||
sku_business_key AS limit_source_sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at
|
||||
FROM (
|
||||
SELECT
|
||||
upper(btrim(sku_code)) AS identity_value,
|
||||
product_business_key,
|
||||
sku_business_key,
|
||||
limit_price_applicable,
|
||||
limit_price_confirmed,
|
||||
limit_price_status,
|
||||
expected_daily_limit_price,
|
||||
expected_limit_gross_margin_rate,
|
||||
non_spu_owner_limit_price,
|
||||
limit_confirmed_at,
|
||||
updated_at
|
||||
FROM external.intake_product_limits_v1
|
||||
WHERE NULLIF(btrim(COALESCE(sku_code, '')), '') IS NOT NULL
|
||||
) limit_rows
|
||||
WHERE NULLIF(identity_value, '') IS NOT NULL
|
||||
ORDER BY
|
||||
identity_value,
|
||||
limit_price_applicable DESC,
|
||||
limit_price_confirmed DESC,
|
||||
limit_confirmed_at DESC NULLS LAST,
|
||||
updated_at DESC
|
||||
),
|
||||
catalog_by_sku AS (
|
||||
SELECT
|
||||
sku.product_id,
|
||||
sku.product_model_number,
|
||||
sku.product_business_key,
|
||||
sku.product_name,
|
||||
sku.product_description,
|
||||
sku.product_category_id,
|
||||
sku.product_category_name,
|
||||
sku.product_thumbnail_url,
|
||||
sku.product_model_url,
|
||||
sku.sku_id,
|
||||
sku.sku_code,
|
||||
sku.sku_business_key,
|
||||
sku.sku_name,
|
||||
sku.price,
|
||||
sku.cost_price,
|
||||
sku.stock,
|
||||
sku.is_active,
|
||||
sku.record_status,
|
||||
sku.model_url,
|
||||
sku.image_url,
|
||||
sku.image_urls,
|
||||
sku.source_pre_sku_id,
|
||||
sku.source_asset_variant_id,
|
||||
sku.source_asset_variant_ids,
|
||||
sku.asset_family_code,
|
||||
sku.asset_family_name,
|
||||
sku.spec_code,
|
||||
sku.spec_name,
|
||||
sku.barcode,
|
||||
sku.barcode_business_key,
|
||||
sku.size_json,
|
||||
sku.color,
|
||||
sku.material,
|
||||
sku.source_status,
|
||||
sku.synced_at,
|
||||
sku.warehouse_location_id,
|
||||
sku.warehouse_location_code,
|
||||
sku.warehouse_location_name,
|
||||
stock.warehouse_zone,
|
||||
stock.warehouse_aisle,
|
||||
stock.warehouse_shelf,
|
||||
stock.warehouse_bin,
|
||||
stock.warehouse_location_is_active,
|
||||
stock.warehouse_location_status,
|
||||
COALESCE(limit_by_barcode.limit_price_applicable, limit_by_sku.limit_price_applicable, false) AS limit_price_applicable,
|
||||
COALESCE(limit_by_barcode.limit_price_confirmed, limit_by_sku.limit_price_confirmed, false) AS limit_price_confirmed,
|
||||
COALESCE(limit_by_barcode.limit_price_status, limit_by_sku.limit_price_status, 'NOT_APPLICABLE') AS limit_price_status,
|
||||
COALESCE(limit_by_barcode.expected_daily_limit_price, limit_by_sku.expected_daily_limit_price) AS expected_daily_limit_price,
|
||||
COALESCE(limit_by_barcode.expected_limit_gross_margin_rate, limit_by_sku.expected_limit_gross_margin_rate) AS expected_limit_gross_margin_rate,
|
||||
COALESCE(limit_by_barcode.non_spu_owner_limit_price, limit_by_sku.non_spu_owner_limit_price) AS non_spu_owner_limit_price,
|
||||
COALESCE(limit_by_barcode.limit_confirmed_at, limit_by_sku.limit_confirmed_at) AS limit_confirmed_at,
|
||||
COALESCE(limit_by_barcode.limit_source_product_business_key, limit_by_sku.limit_source_product_business_key) AS limit_source_product_business_key,
|
||||
COALESCE(limit_by_barcode.limit_source_sku_business_key, limit_by_sku.limit_source_sku_business_key) AS limit_source_sku_business_key,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.image_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.image_asset_count, 0)
|
||||
END AS image_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE WHEN NULLIF(btrim(COALESCE(sku.model_url, '')), '') IS NOT NULL THEN 1 ELSE 0 END
|
||||
ELSE COALESCE(asset_summary.model_asset_count, 0)
|
||||
END AS model_asset_count,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN CASE
|
||||
WHEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
) IS NOT NULL
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
ELSE COALESCE(asset_summary.thumbnail_asset_count, 0)
|
||||
END AS thumbnail_asset_count,
|
||||
COALESCE(
|
||||
asset_summary.first_model_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN NULLIF(btrim(COALESCE(sku.model_url, '')), '')
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_model_asset_url,
|
||||
COALESCE(
|
||||
asset_summary.first_thumbnail_asset_url,
|
||||
CASE
|
||||
WHEN sku.catalog_source = 'INTAKE_READY'
|
||||
THEN COALESCE(
|
||||
NULLIF(btrim(COALESCE(sku.product_thumbnail_url, '')), ''),
|
||||
NULLIF(btrim(COALESCE(sku.image_url, '')), '')
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) AS first_thumbnail_asset_url,
|
||||
COALESCE(bom_summary.bom_item_count, 0) AS bom_item_count,
|
||||
COALESCE(bom_summary.panel_item_count, 0) AS panel_item_count,
|
||||
COALESCE(bom_summary.hardware_item_count, 0) AS hardware_item_count,
|
||||
COALESCE(bom_summary.material_codes, ARRAY[]::text[]) AS material_codes,
|
||||
COALESCE(bom_summary.material_names, ARRAY[]::text[]) AS material_names,
|
||||
COALESCE(packaged_summary.packaged_item_count, 0) AS packaged_item_count,
|
||||
COALESCE(packaged_summary.packaged_stock_quantity, 0) AS packaged_stock_quantity,
|
||||
COALESCE(quality_summary.quality_record_count, 0) AS quality_record_count,
|
||||
COALESCE(quality_summary.warehouse_quality_record_count, 0) AS warehouse_quality_record_count,
|
||||
COALESCE(quality_summary.first_batch_quality_record_count, 0) AS first_batch_quality_record_count,
|
||||
latest_quality.latest_quality_record_key,
|
||||
latest_quality.latest_quality_source_type,
|
||||
latest_quality.latest_quality_batch_no,
|
||||
latest_quality.latest_quality_inspected_at,
|
||||
latest_quality.latest_quality_conclusion,
|
||||
latest_quality.latest_quality_status,
|
||||
sku.created_at,
|
||||
sku.updated_at,
|
||||
sku.catalog_source,
|
||||
sku.appearance_designer,
|
||||
sku.structure_designer,
|
||||
sku.spu_owner,
|
||||
sku.planning_owner,
|
||||
sku.space_designer,
|
||||
sku.purchase_owner,
|
||||
sku.supplier_name,
|
||||
sku.supplier_code,
|
||||
sku.factory_warehouse,
|
||||
sku.shipping_method,
|
||||
sku.minimum_order_quantity,
|
||||
sku.tax_included_unit_price,
|
||||
sku.lead_time,
|
||||
sku.install_method
|
||||
FROM sku_source sku
|
||||
LEFT JOIN external.sku_warehouse_stock_v1 stock
|
||||
ON stock.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN limit_by_sku
|
||||
ON limit_by_sku.identity_value = upper(btrim(COALESCE(sku.sku_code, '')))
|
||||
LEFT JOIN limit_by_barcode
|
||||
ON limit_by_barcode.identity_value = sku.barcode_business_key
|
||||
LEFT JOIN asset_summary
|
||||
ON asset_summary.product_business_key = sku.product_business_key
|
||||
AND asset_summary.sku_business_key = sku.sku_business_key
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS bom_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'PANEL')::integer AS panel_item_count,
|
||||
COUNT(*) FILTER (WHERE item_type = 'HARDWARE')::integer AS hardware_item_count,
|
||||
array_remove(array_agg(DISTINCT material_code ORDER BY material_code), NULL) AS material_codes,
|
||||
array_remove(array_agg(DISTINCT material_name ORDER BY material_name), NULL) AS material_names
|
||||
FROM external.product_bom_v1 bom
|
||||
WHERE bom.product_business_key = sku.product_business_key
|
||||
AND (
|
||||
bom.sku_business_key = sku.sku_business_key
|
||||
OR bom.sku_business_key IS NULL
|
||||
)
|
||||
) bom_summary ON true
|
||||
LEFT JOIN packaged_summary
|
||||
ON packaged_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN quality_summary
|
||||
ON quality_summary.barcode_business_key = sku.barcode_business_key
|
||||
LEFT JOIN latest_quality
|
||||
ON latest_quality.barcode_business_key = sku.barcode_business_key
|
||||
)
|
||||
SELECT DISTINCT ON (catalog_by_sku.barcode_business_key)
|
||||
catalog_by_sku.*
|
||||
FROM catalog_by_sku
|
||||
WHERE catalog_by_sku.barcode_business_key IS NOT NULL
|
||||
AND btrim(catalog_by_sku.barcode_business_key) <> ''
|
||||
ORDER BY
|
||||
catalog_by_sku.barcode_business_key,
|
||||
CASE WHEN catalog_by_sku.catalog_source IS NULL THEN 1
|
||||
WHEN catalog_by_sku.catalog_source = 'FORMAL' THEN 0
|
||||
ELSE 1 END,
|
||||
catalog_by_sku.updated_at DESC NULLS LAST,
|
||||
catalog_by_sku.created_at DESC;
|
||||
@ -0,0 +1,18 @@
|
||||
ALTER TABLE "InstallationDemoConfig"
|
||||
ADD COLUMN IF NOT EXISTS "toolIds" JSONB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "InstallationDemoTool" (
|
||||
"id" TEXT NOT NULL,
|
||||
"toolCode" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"modelUrl" TEXT NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"mountOffset" JSONB,
|
||||
"mountRotation" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "InstallationDemoTool_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "InstallationDemoTool_toolCode_key"
|
||||
ON "InstallationDemoTool"("toolCode");
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_PRODUCT_CREATE';
|
||||
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_PRODUCT_DELETE';
|
||||
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_FILE_UPLOAD';
|
||||
ALTER TYPE "OperationLogAction" ADD VALUE IF NOT EXISTS 'INTAKE_WORKSPACE_CONFIRMATION';
|
||||
@ -0,0 +1,180 @@
|
||||
CREATE TYPE "BomCostingCaseStatus" AS ENUM (
|
||||
'DRAFT',
|
||||
'QUEUED',
|
||||
'ANALYZING',
|
||||
'READY',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingLineCategory" AS ENUM (
|
||||
'BOARD',
|
||||
'STEEL',
|
||||
'ACCESSORY',
|
||||
'PACKAGING',
|
||||
'PROCESSING',
|
||||
'LABOR'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingMatchStatus" AS ENUM (
|
||||
'UNMATCHED',
|
||||
'MATCHED',
|
||||
'NEEDS_CONFIRMATION'
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingCase" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sourceUrl" TEXT NOT NULL,
|
||||
"sourceHost" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL DEFAULT '',
|
||||
"providerCode" TEXT NOT NULL DEFAULT '',
|
||||
"providerTaskId" TEXT NOT NULL DEFAULT '',
|
||||
"analysisSchemaVersion" TEXT NOT NULL DEFAULT 'competitor-analysis/v1',
|
||||
"status" "BomCostingCaseStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"progress" INTEGER NOT NULL DEFAULT 0,
|
||||
"statusMessage" TEXT NOT NULL DEFAULT '',
|
||||
"targetErrorPercent" DECIMAL(5,2) NOT NULL DEFAULT 10,
|
||||
"currency" TEXT NOT NULL DEFAULT 'CNY',
|
||||
"rawAnalysisSnapshot" JSONB,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "BomCostingCase_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingSku" (
|
||||
"id" TEXT NOT NULL,
|
||||
"caseId" TEXT NOT NULL,
|
||||
"externalSkuId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL DEFAULT '',
|
||||
"structureGroupKey" TEXT NOT NULL DEFAULT '',
|
||||
"imageUrl" TEXT,
|
||||
"attributes" JSONB,
|
||||
"lengthMm" DECIMAL(12,3),
|
||||
"widthMm" DECIMAL(12,3),
|
||||
"heightMm" DECIMAL(12,3),
|
||||
"weightKg" DECIMAL(12,3),
|
||||
"packageLengthMm" DECIMAL(12,3),
|
||||
"packageWidthMm" DECIMAL(12,3),
|
||||
"packageHeightMm" DECIMAL(12,3),
|
||||
"packageWeightKg" DECIMAL(12,3),
|
||||
"estimatedCost" DECIMAL(12,2),
|
||||
"costCalculatedAt" TIMESTAMP(3),
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "BomCostingSku_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingLine" (
|
||||
"id" TEXT NOT NULL,
|
||||
"skuId" TEXT NOT NULL,
|
||||
"externalLineId" TEXT NOT NULL,
|
||||
"category" "BomCostingLineCategory" NOT NULL,
|
||||
"partName" TEXT NOT NULL,
|
||||
"materialCode" TEXT NOT NULL DEFAULT '',
|
||||
"materialName" TEXT NOT NULL DEFAULT '',
|
||||
"materialSpec" TEXT NOT NULL DEFAULT '',
|
||||
"processName" TEXT NOT NULL DEFAULT '',
|
||||
"packagingKind" TEXT NOT NULL DEFAULT '',
|
||||
"lengthMm" DECIMAL(12,3),
|
||||
"widthMm" DECIMAL(12,3),
|
||||
"heightMm" DECIMAL(12,3),
|
||||
"quantity" DECIMAL(12,4) NOT NULL DEFAULT 1,
|
||||
"unit" TEXT NOT NULL DEFAULT '',
|
||||
"weightKg" DECIMAL(12,4),
|
||||
"lossFactor" DECIMAL(8,4) NOT NULL DEFAULT 1,
|
||||
"sourceConfidence" DECIMAL(5,4),
|
||||
"evidence" JSONB,
|
||||
"matchStatus" "BomCostingMatchStatus" NOT NULL DEFAULT 'UNMATCHED',
|
||||
"matchedLibraryType" TEXT NOT NULL DEFAULT '',
|
||||
"matchedLibraryRecordId" TEXT NOT NULL DEFAULT '',
|
||||
"matchedLibraryName" TEXT NOT NULL DEFAULT '',
|
||||
"matchedLibrarySpec" TEXT NOT NULL DEFAULT '',
|
||||
"resolvedUnitPrice" DECIMAL(12,4),
|
||||
"calculatedQuantity" DECIMAL(14,6),
|
||||
"subtotal" DECIMAL(12,2),
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "BomCostingLine_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingPriceSnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"lineId" TEXT NOT NULL,
|
||||
"sourceType" TEXT NOT NULL,
|
||||
"sourceRecordId" TEXT NOT NULL,
|
||||
"sourceName" TEXT NOT NULL,
|
||||
"sourceSpec" TEXT NOT NULL DEFAULT '',
|
||||
"unitPrice" DECIMAL(12,4) NOT NULL,
|
||||
"unit" TEXT NOT NULL,
|
||||
"currency" TEXT NOT NULL DEFAULT 'CNY',
|
||||
"lossFactor" DECIMAL(8,4) NOT NULL DEFAULT 1,
|
||||
"isCurrent" BOOLEAN NOT NULL DEFAULT true,
|
||||
"matchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "BomCostingPriceSnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingTaskEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"caseId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"progress" INTEGER NOT NULL DEFAULT 0,
|
||||
"message" TEXT NOT NULL DEFAULT '',
|
||||
"providerTaskId" TEXT NOT NULL DEFAULT '',
|
||||
"payload" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "BomCostingTaskEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "BomCostingSku_caseId_externalSkuId_key"
|
||||
ON "BomCostingSku"("caseId", "externalSkuId");
|
||||
CREATE UNIQUE INDEX "BomCostingLine_skuId_externalLineId_key"
|
||||
ON "BomCostingLine"("skuId", "externalLineId");
|
||||
CREATE INDEX "BomCostingCase_createdById_deletedAt_createdAt_idx"
|
||||
ON "BomCostingCase"("createdById", "deletedAt", "createdAt");
|
||||
CREATE INDEX "BomCostingCase_status_deletedAt_idx"
|
||||
ON "BomCostingCase"("status", "deletedAt");
|
||||
CREATE INDEX "BomCostingCase_providerTaskId_idx"
|
||||
ON "BomCostingCase"("providerTaskId");
|
||||
CREATE INDEX "BomCostingSku_caseId_sortOrder_idx"
|
||||
ON "BomCostingSku"("caseId", "sortOrder");
|
||||
CREATE INDEX "BomCostingSku_structureGroupKey_idx"
|
||||
ON "BomCostingSku"("structureGroupKey");
|
||||
CREATE INDEX "BomCostingLine_skuId_sortOrder_idx"
|
||||
ON "BomCostingLine"("skuId", "sortOrder");
|
||||
CREATE INDEX "BomCostingLine_category_matchStatus_idx"
|
||||
ON "BomCostingLine"("category", "matchStatus");
|
||||
CREATE INDEX "BomCostingLine_matchedLibraryType_matchedLibraryRecordId_idx"
|
||||
ON "BomCostingLine"("matchedLibraryType", "matchedLibraryRecordId");
|
||||
CREATE INDEX "BomCostingPriceSnapshot_lineId_isCurrent_idx"
|
||||
ON "BomCostingPriceSnapshot"("lineId", "isCurrent");
|
||||
CREATE INDEX "BomCostingPriceSnapshot_sourceType_sourceRecordId_idx"
|
||||
ON "BomCostingPriceSnapshot"("sourceType", "sourceRecordId");
|
||||
CREATE INDEX "BomCostingTaskEvent_caseId_createdAt_idx"
|
||||
ON "BomCostingTaskEvent"("caseId", "createdAt");
|
||||
CREATE INDEX "BomCostingTaskEvent_providerTaskId_idx"
|
||||
ON "BomCostingTaskEvent"("providerTaskId");
|
||||
|
||||
ALTER TABLE "BomCostingCase"
|
||||
ADD CONSTRAINT "BomCostingCase_createdById_fkey"
|
||||
FOREIGN KEY ("createdById") REFERENCES "User"("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ALTER TABLE "BomCostingSku"
|
||||
ADD CONSTRAINT "BomCostingSku_caseId_fkey"
|
||||
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "BomCostingLine"
|
||||
ADD CONSTRAINT "BomCostingLine_skuId_fkey"
|
||||
FOREIGN KEY ("skuId") REFERENCES "BomCostingSku"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "BomCostingPriceSnapshot"
|
||||
ADD CONSTRAINT "BomCostingPriceSnapshot_lineId_fkey"
|
||||
FOREIGN KEY ("lineId") REFERENCES "BomCostingLine"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "BomCostingTaskEvent"
|
||||
ADD CONSTRAINT "BomCostingTaskEvent_caseId_fkey"
|
||||
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,84 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkRecordScoreRule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"primary" TEXT NOT NULL,
|
||||
"secondary" TEXT NOT NULL DEFAULT '',
|
||||
"unitScore" DECIMAL(12,4),
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkRecordScoreRule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkRecord" (
|
||||
"id" TEXT NOT NULL,
|
||||
"workDate" DATE NOT NULL,
|
||||
"designerId" TEXT NOT NULL,
|
||||
"designerName" TEXT NOT NULL,
|
||||
"primary" TEXT NOT NULL,
|
||||
"secondary" TEXT NOT NULL DEFAULT '',
|
||||
"scoreRuleId" TEXT,
|
||||
"approvalOrTask" TEXT NOT NULL DEFAULT '',
|
||||
"spu" TEXT NOT NULL DEFAULT '',
|
||||
"quantity" DECIMAL(12,2) NOT NULL DEFAULT 1,
|
||||
"unitScore" DECIMAL(12,4),
|
||||
"taskTotal" DECIMAL(14,4),
|
||||
"fileStoragePath" TEXT NOT NULL DEFAULT '',
|
||||
"createdById" TEXT NOT NULL,
|
||||
"updatedById" TEXT NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkRecord_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WorkRecordMonthlyArchive" (
|
||||
"id" TEXT NOT NULL,
|
||||
"monthKey" TEXT NOT NULL,
|
||||
"periodStart" DATE NOT NULL,
|
||||
"periodEnd" DATE NOT NULL,
|
||||
"fileName" TEXT NOT NULL,
|
||||
"summary" JSONB NOT NULL,
|
||||
"records" JSONB NOT NULL,
|
||||
"recordCount" INTEGER NOT NULL,
|
||||
"generatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WorkRecordMonthlyArchive_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecordScoreRule_primary_sortOrder_idx" ON "WorkRecordScoreRule"("primary", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecordScoreRule_isActive_sortOrder_idx" ON "WorkRecordScoreRule"("isActive", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecord_workDate_deletedAt_idx" ON "WorkRecord"("workDate", "deletedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecord_designerId_workDate_idx" ON "WorkRecord"("designerId", "workDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecord_primary_workDate_idx" ON "WorkRecord"("primary", "workDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecord_scoreRuleId_idx" ON "WorkRecord"("scoreRuleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WorkRecordMonthlyArchive_monthKey_key" ON "WorkRecordMonthlyArchive"("monthKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WorkRecordMonthlyArchive_periodStart_idx" ON "WorkRecordMonthlyArchive"("periodStart");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkRecord" ADD CONSTRAINT "WorkRecord_designerId_fkey" FOREIGN KEY ("designerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WorkRecord" ADD CONSTRAINT "WorkRecord_scoreRuleId_fkey" FOREIGN KEY ("scoreRuleId") REFERENCES "WorkRecordScoreRule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@ -0,0 +1,7 @@
|
||||
UPDATE "WorkRecordScoreRule"
|
||||
SET
|
||||
"secondary" = '将图纸上传图纸系统并生成对应二维码需存入对应SPU文件夹',
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
"primary" = '上传图纸+制作二维码'
|
||||
AND "secondary" = '将图纸上传图纸系统并生成对应二维码 / 需存入对应SPU文件夹';
|
||||
@ -0,0 +1,36 @@
|
||||
CREATE TYPE "ReleaseNoteStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
||||
|
||||
CREATE TABLE "ReleaseNote" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"summary" TEXT NOT NULL,
|
||||
"content" JSONB NOT NULL,
|
||||
"targetBusinessRoles" "BusinessAccountType"[] NOT NULL DEFAULT ARRAY[]::"BusinessAccountType"[],
|
||||
"status" "ReleaseNoteStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ReleaseNote_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "ReleaseNote_createdById_fkey"
|
||||
FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ReleaseNoteAcknowledgement" (
|
||||
"id" TEXT NOT NULL,
|
||||
"releaseNoteId" TEXT NOT NULL,
|
||||
"readerId" TEXT NOT NULL,
|
||||
"readAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ReleaseNoteAcknowledgement_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "ReleaseNoteAcknowledgement_releaseNoteId_fkey"
|
||||
FOREIGN KEY ("releaseNoteId") REFERENCES "ReleaseNote"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "ReleaseNoteAcknowledgement_readerId_fkey"
|
||||
FOREIGN KEY ("readerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX "ReleaseNote_status_publishedAt_idx" ON "ReleaseNote"("status", "publishedAt");
|
||||
CREATE INDEX "ReleaseNote_createdById_idx" ON "ReleaseNote"("createdById");
|
||||
CREATE UNIQUE INDEX "ReleaseNoteAcknowledgement_releaseNoteId_readerId_key" ON "ReleaseNoteAcknowledgement"("releaseNoteId", "readerId");
|
||||
CREATE INDEX "ReleaseNoteAcknowledgement_readerId_readAt_idx" ON "ReleaseNoteAcknowledgement"("readerId", "readAt");
|
||||
@ -0,0 +1,91 @@
|
||||
CREATE TYPE "BomCostingSourceStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'FETCHING',
|
||||
'ARCHIVING',
|
||||
'READY',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingSourceAssetKind" AS ENUM (
|
||||
'MAIN',
|
||||
'DETAIL',
|
||||
'SKU'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingSourceAssetStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'STORED',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingSourceSnapshot" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sourceKey" TEXT NOT NULL,
|
||||
"sourceUrl" TEXT NOT NULL,
|
||||
"sourceHost" TEXT NOT NULL,
|
||||
"channel" TEXT NOT NULL,
|
||||
"externalItemId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL DEFAULT '',
|
||||
"shopName" TEXT NOT NULL DEFAULT '',
|
||||
"detailUrl" TEXT,
|
||||
"status" "BomCostingSourceStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"skuCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"imageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"storedImageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"failedImageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"rawSnapshot" JSONB,
|
||||
"errorMessage" TEXT NOT NULL DEFAULT '',
|
||||
"fetchedAt" TIMESTAMP(3),
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "BomCostingSourceSnapshot_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "BomCostingSourceAsset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sourceSnapshotId" TEXT NOT NULL,
|
||||
"assetKey" TEXT NOT NULL,
|
||||
"kind" "BomCostingSourceAssetKind" NOT NULL,
|
||||
"status" "BomCostingSourceAssetStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"originalUrl" TEXT NOT NULL,
|
||||
"storedUrl" TEXT NOT NULL DEFAULT '',
|
||||
"storageKey" TEXT NOT NULL DEFAULT '',
|
||||
"contentType" TEXT NOT NULL DEFAULT '',
|
||||
"byteSize" INTEGER,
|
||||
"externalSkuIds" JSONB NOT NULL DEFAULT '[]',
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorMessage" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "BomCostingSourceAsset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "BomCostingCase"
|
||||
ADD COLUMN "sourceSnapshotId" TEXT,
|
||||
ADD COLUMN "sourceCacheHit" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "processingStartedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "processingFinishedAt" TIMESTAMP(3);
|
||||
|
||||
CREATE UNIQUE INDEX "BomCostingSourceSnapshot_sourceKey_key"
|
||||
ON "BomCostingSourceSnapshot"("sourceKey");
|
||||
CREATE INDEX "BomCostingSourceSnapshot_status_updatedAt_idx"
|
||||
ON "BomCostingSourceSnapshot"("status", "updatedAt");
|
||||
CREATE INDEX "BomCostingSourceSnapshot_channel_externalItemId_idx"
|
||||
ON "BomCostingSourceSnapshot"("channel", "externalItemId");
|
||||
CREATE UNIQUE INDEX "BomCostingSourceAsset_sourceSnapshotId_assetKey_key"
|
||||
ON "BomCostingSourceAsset"("sourceSnapshotId", "assetKey");
|
||||
CREATE INDEX "BomCostingSourceAsset_sourceSnapshotId_status_sortOrder_idx"
|
||||
ON "BomCostingSourceAsset"("sourceSnapshotId", "status", "sortOrder");
|
||||
CREATE INDEX "BomCostingCase_sourceSnapshotId_idx"
|
||||
ON "BomCostingCase"("sourceSnapshotId");
|
||||
|
||||
ALTER TABLE "BomCostingCase"
|
||||
ADD CONSTRAINT "BomCostingCase_sourceSnapshotId_fkey"
|
||||
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "BomCostingSourceAsset"
|
||||
ADD CONSTRAINT "BomCostingSourceAsset_sourceSnapshotId_fkey"
|
||||
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,188 @@
|
||||
CREATE TYPE "IntakeAiRenderOutputMode" AS ENUM ('WHITE_BACKGROUND', 'EIGHT_VIEW', 'SCENE');
|
||||
CREATE TYPE "IntakeAiRenderStorageClass" AS ENUM ('TEMPORARY', 'PERMANENT');
|
||||
|
||||
CREATE TABLE "IntakeProductModelAsset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"modelKey" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL DEFAULT '',
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "IntakeProductModelAsset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "IntakeProductModelVersion" (
|
||||
"id" TEXT NOT NULL,
|
||||
"assetId" TEXT NOT NULL,
|
||||
"versionNo" INTEGER NOT NULL,
|
||||
"originalFileName" TEXT NOT NULL,
|
||||
"mimeType" TEXT NOT NULL,
|
||||
"fileSize" INTEGER NOT NULL,
|
||||
"checksumSha256" TEXT NOT NULL DEFAULT '',
|
||||
"sourceStorageKey" TEXT NOT NULL,
|
||||
"sourceUrl" TEXT NOT NULL,
|
||||
"previewStorageKey" TEXT,
|
||||
"previewUrl" TEXT,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "IntakeProductModelVersion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "IntakeSkuAiProfile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"skuItemId" TEXT NOT NULL,
|
||||
"currentModelVersionId" TEXT,
|
||||
"currentMaterialVersionId" TEXT,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"updatedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "IntakeSkuAiProfile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "IntakeSkuAiMaterialVersion" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"versionNo" INTEGER NOT NULL,
|
||||
"materialColor" TEXT NOT NULL DEFAULT '#d8c6ad',
|
||||
"textureStorageKey" TEXT,
|
||||
"textureUrl" TEXT,
|
||||
"nodeMaterials" JSONB,
|
||||
"uvConfig" JSONB,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "IntakeSkuAiMaterialVersion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD COLUMN "intakeSkuItemId" TEXT,
|
||||
ADD COLUMN "modelVersionId" TEXT,
|
||||
ADD COLUMN "materialVersionId" TEXT,
|
||||
ADD COLUMN "outputMode" "IntakeAiRenderOutputMode",
|
||||
ADD COLUMN "batchId" TEXT,
|
||||
ADD COLUMN "viewName" TEXT,
|
||||
ADD COLUMN "viewIndex" INTEGER,
|
||||
ADD COLUMN "storageClass" "IntakeAiRenderStorageClass" NOT NULL DEFAULT 'TEMPORARY';
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeProductModelAsset_productId_modelKey_key"
|
||||
ON "IntakeProductModelAsset"("productId", "modelKey");
|
||||
CREATE INDEX "IntakeProductModelAsset_productId_createdAt_idx"
|
||||
ON "IntakeProductModelAsset"("productId", "createdAt");
|
||||
CREATE INDEX "IntakeProductModelAsset_createdBy_idx"
|
||||
ON "IntakeProductModelAsset"("createdBy");
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeProductModelVersion_assetId_versionNo_key"
|
||||
ON "IntakeProductModelVersion"("assetId", "versionNo");
|
||||
CREATE INDEX "IntakeProductModelVersion_assetId_createdAt_idx"
|
||||
ON "IntakeProductModelVersion"("assetId", "createdAt");
|
||||
CREATE INDEX "IntakeProductModelVersion_checksumSha256_idx"
|
||||
ON "IntakeProductModelVersion"("checksumSha256");
|
||||
CREATE INDEX "IntakeProductModelVersion_createdBy_idx"
|
||||
ON "IntakeProductModelVersion"("createdBy");
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeSkuAiProfile_skuItemId_key"
|
||||
ON "IntakeSkuAiProfile"("skuItemId");
|
||||
CREATE UNIQUE INDEX "IntakeSkuAiProfile_currentMaterialVersionId_key"
|
||||
ON "IntakeSkuAiProfile"("currentMaterialVersionId");
|
||||
CREATE INDEX "IntakeSkuAiProfile_currentModelVersionId_idx"
|
||||
ON "IntakeSkuAiProfile"("currentModelVersionId");
|
||||
CREATE INDEX "IntakeSkuAiProfile_createdBy_idx"
|
||||
ON "IntakeSkuAiProfile"("createdBy");
|
||||
|
||||
CREATE UNIQUE INDEX "IntakeSkuAiMaterialVersion_profileId_versionNo_key"
|
||||
ON "IntakeSkuAiMaterialVersion"("profileId", "versionNo");
|
||||
CREATE INDEX "IntakeSkuAiMaterialVersion_profileId_createdAt_idx"
|
||||
ON "IntakeSkuAiMaterialVersion"("profileId", "createdAt");
|
||||
CREATE INDEX "IntakeSkuAiMaterialVersion_createdBy_idx"
|
||||
ON "IntakeSkuAiMaterialVersion"("createdBy");
|
||||
|
||||
CREATE INDEX "AssistedAssemblyAiRender_intakeSkuItemId_createdAt_idx"
|
||||
ON "AssistedAssemblyAiRender"("intakeSkuItemId", "createdAt");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_intakeSkuItemId_outputMode_createdAt_idx"
|
||||
ON "AssistedAssemblyAiRender"("intakeSkuItemId", "outputMode", "createdAt");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_modelVersionId_idx"
|
||||
ON "AssistedAssemblyAiRender"("modelVersionId");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_materialVersionId_idx"
|
||||
ON "AssistedAssemblyAiRender"("materialVersionId");
|
||||
CREATE INDEX "AssistedAssemblyAiRender_storageClass_createdAt_idx"
|
||||
ON "AssistedAssemblyAiRender"("storageClass", "createdAt");
|
||||
|
||||
ALTER TABLE "IntakeProductModelAsset"
|
||||
ADD CONSTRAINT "IntakeProductModelAsset_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "IntakeProductModelAsset"
|
||||
ADD CONSTRAINT "IntakeProductModelAsset_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeProductModelVersion"
|
||||
ADD CONSTRAINT "IntakeProductModelVersion_assetId_fkey"
|
||||
FOREIGN KEY ("assetId") REFERENCES "IntakeProductModelAsset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "IntakeProductModelVersion"
|
||||
ADD CONSTRAINT "IntakeProductModelVersion_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeSkuAiProfile"
|
||||
ADD CONSTRAINT "IntakeSkuAiProfile_skuItemId_fkey"
|
||||
FOREIGN KEY ("skuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "IntakeSkuAiProfile"
|
||||
ADD CONSTRAINT "IntakeSkuAiProfile_currentModelVersionId_fkey"
|
||||
FOREIGN KEY ("currentModelVersionId") REFERENCES "IntakeProductModelVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "IntakeSkuAiProfile"
|
||||
ADD CONSTRAINT "IntakeSkuAiProfile_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeSkuAiMaterialVersion"
|
||||
ADD CONSTRAINT "IntakeSkuAiMaterialVersion_profileId_fkey"
|
||||
FOREIGN KEY ("profileId") REFERENCES "IntakeSkuAiProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "IntakeSkuAiMaterialVersion"
|
||||
ADD CONSTRAINT "IntakeSkuAiMaterialVersion_createdBy_fkey"
|
||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "IntakeSkuAiProfile"
|
||||
ADD CONSTRAINT "IntakeSkuAiProfile_currentMaterialVersionId_fkey"
|
||||
FOREIGN KEY ("currentMaterialVersionId") REFERENCES "IntakeSkuAiMaterialVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD CONSTRAINT "AssistedAssemblyAiRender_intakeSkuItemId_fkey"
|
||||
FOREIGN KEY ("intakeSkuItemId") REFERENCES "IntakeNonIntegratedSkuItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD CONSTRAINT "AssistedAssemblyAiRender_modelVersionId_fkey"
|
||||
FOREIGN KEY ("modelVersionId") REFERENCES "IntakeProductModelVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "AssistedAssemblyAiRender"
|
||||
ADD CONSTRAINT "AssistedAssemblyAiRender_materialVersionId_fkey"
|
||||
FOREIGN KEY ("materialVersionId") REFERENCES "IntakeSkuAiMaterialVersion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
UPDATE "AssistedAssemblyAiRender"
|
||||
SET
|
||||
"outputMode" = CASE
|
||||
WHEN "params"->>'mode' = 'white-background' THEN 'WHITE_BACKGROUND'::"IntakeAiRenderOutputMode"
|
||||
WHEN "params"->>'mode' = 'eight-view' THEN 'EIGHT_VIEW'::"IntakeAiRenderOutputMode"
|
||||
WHEN "params"->>'mode' = 'scene' THEN 'SCENE'::"IntakeAiRenderOutputMode"
|
||||
ELSE NULL
|
||||
END,
|
||||
"batchId" = NULLIF("params"->>'batchId', ''),
|
||||
"viewName" = NULLIF("params"->>'viewName', ''),
|
||||
"viewIndex" = CASE
|
||||
WHEN ("params"->>'viewIndex') ~ '^[0-9]+$' THEN ("params"->>'viewIndex')::INTEGER
|
||||
ELSE NULL
|
||||
END;
|
||||
|
||||
WITH exact_matches AS (
|
||||
SELECT render."id" AS "renderId", MIN(item."id") AS "skuItemId"
|
||||
FROM "AssistedAssemblyAiRender" render
|
||||
JOIN "AiRenderWorkbenchSku" workbench_sku ON workbench_sku."id" = render."workbenchSkuId"
|
||||
JOIN "AiRenderWorkbenchSpu" workbench_spu ON workbench_spu."id" = workbench_sku."spuId"
|
||||
JOIN "IntakeNonIntegratedSkuItem" item
|
||||
ON UPPER(BTRIM(item."barcode")) = UPPER(BTRIM(workbench_sku."name"))
|
||||
JOIN "IntakeNonIntegratedProduct" product
|
||||
ON product."id" = item."productId"
|
||||
AND UPPER(BTRIM(product."spu")) = UPPER(BTRIM(workbench_spu."name"))
|
||||
WHERE BTRIM(item."barcode") <> ''
|
||||
GROUP BY render."id"
|
||||
HAVING COUNT(*) = 1
|
||||
)
|
||||
UPDATE "AssistedAssemblyAiRender" render
|
||||
SET "intakeSkuItemId" = exact_matches."skuItemId"
|
||||
FROM exact_matches
|
||||
WHERE render."id" = exact_matches."renderId";
|
||||
@ -0,0 +1,49 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BomCostingFactAnalysisStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'PROCESSING',
|
||||
'READY',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BomCostingSourceFactAnalysis" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sourceSnapshotId" TEXT NOT NULL,
|
||||
"schemaVersion" TEXT NOT NULL,
|
||||
"assetFingerprint" TEXT NOT NULL,
|
||||
"status" "BomCostingFactAnalysisStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"model" TEXT NOT NULL DEFAULT '',
|
||||
"factSnapshot" JSONB,
|
||||
"analyzedImageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalImageCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"batchCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"promptTokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"completionTokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"durationMs" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorMessage" TEXT NOT NULL DEFAULT '',
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BomCostingSourceFactAnalysis_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_key"
|
||||
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "schemaVersion");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_status_idx"
|
||||
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "BomCostingSourceFactAnalysis_status_updatedAt_idx"
|
||||
ON "BomCostingSourceFactAnalysis"("status", "updatedAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BomCostingSourceFactAnalysis"
|
||||
ADD CONSTRAINT "BomCostingSourceFactAnalysis_sourceSnapshotId_fkey"
|
||||
FOREIGN KEY ("sourceSnapshotId") REFERENCES "BomCostingSourceSnapshot"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -0,0 +1,27 @@
|
||||
-- Barcode imports are append-only business records. Rows with the same SKU,
|
||||
-- barcode, or supplier must remain independently printable.
|
||||
ALTER TABLE "BarcodePrintImportRecord"
|
||||
ADD COLUMN "normalizedSupplierName" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
WITH supplier_names AS (
|
||||
SELECT
|
||||
"id",
|
||||
btrim(
|
||||
CASE
|
||||
WHEN btrim("supplierName") <> '' THEN "supplierName"
|
||||
ELSE COALESCE("updates" ->> 'supplierName', '')
|
||||
END
|
||||
) AS "supplierName"
|
||||
FROM "BarcodePrintImportRecord"
|
||||
)
|
||||
UPDATE "BarcodePrintImportRecord" AS records
|
||||
SET
|
||||
"supplierName" = supplier_names."supplierName",
|
||||
"normalizedSupplierName" = lower(supplier_names."supplierName")
|
||||
FROM supplier_names
|
||||
WHERE records."id" = supplier_names."id";
|
||||
|
||||
DROP INDEX "BarcodePrintImportRecord_kind_normalizedSku_supplierCode_key";
|
||||
|
||||
CREATE INDEX "BarcodePrintImportRecord_normalizedSupplierName_idx"
|
||||
ON "BarcodePrintImportRecord"("normalizedSupplierName");
|
||||
@ -0,0 +1,140 @@
|
||||
CREATE TYPE "BomCostingFactAnalysisOrigin" AS ENUM (
|
||||
'INITIAL',
|
||||
'SELECTED_REANALYSIS',
|
||||
'FULL_REANALYSIS'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingAnalysisRunMode" AS ENUM (
|
||||
'INITIAL',
|
||||
'REUSE_FACTS',
|
||||
'REANALYZE_SELECTED',
|
||||
'REANALYZE_ALL'
|
||||
);
|
||||
|
||||
CREATE TYPE "BomCostingAnalysisRunStatus" AS ENUM (
|
||||
'PENDING',
|
||||
'PROCESSING',
|
||||
'READY',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
ALTER TABLE "BomCostingSourceFactAnalysis"
|
||||
ADD COLUMN "revision" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "origin" "BomCostingFactAnalysisOrigin" NOT NULL DEFAULT 'INITIAL',
|
||||
ADD COLUMN "baseAnalysisId" TEXT,
|
||||
ADD COLUMN "selectedAssetIds" JSONB NOT NULL DEFAULT '[]';
|
||||
|
||||
DROP INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_key";
|
||||
|
||||
CREATE UNIQUE INDEX "BomCostingSourceFactAnalysis_sourceSnapshotId_schemaVersion_revision_key"
|
||||
ON "BomCostingSourceFactAnalysis"("sourceSnapshotId", "schemaVersion", "revision");
|
||||
|
||||
CREATE INDEX "BomCostingSourceFactAnalysis_baseAnalysisId_idx"
|
||||
ON "BomCostingSourceFactAnalysis"("baseAnalysisId");
|
||||
|
||||
ALTER TABLE "BomCostingSourceFactAnalysis"
|
||||
ADD CONSTRAINT "BomCostingSourceFactAnalysis_baseAnalysisId_fkey"
|
||||
FOREIGN KEY ("baseAnalysisId") REFERENCES "BomCostingSourceFactAnalysis"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE TABLE "BomCostingAnalysisRun" (
|
||||
"id" TEXT NOT NULL,
|
||||
"caseId" TEXT NOT NULL,
|
||||
"revision" INTEGER NOT NULL,
|
||||
"mode" "BomCostingAnalysisRunMode" NOT NULL,
|
||||
"status" "BomCostingAnalysisRunStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"factAnalysisId" TEXT,
|
||||
"selectedAssetIds" JSONB NOT NULL DEFAULT '[]',
|
||||
"instruction" TEXT NOT NULL DEFAULT '',
|
||||
"resultSnapshot" JSONB,
|
||||
"model" TEXT NOT NULL DEFAULT '',
|
||||
"callId" TEXT NOT NULL DEFAULT '',
|
||||
"promptTokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"completionTokens" INTEGER NOT NULL DEFAULT 0,
|
||||
"durationMs" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorMessage" TEXT NOT NULL DEFAULT '',
|
||||
"createdById" TEXT,
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BomCostingAnalysisRun_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "BomCostingAnalysisRun_caseId_revision_key"
|
||||
ON "BomCostingAnalysisRun"("caseId", "revision");
|
||||
|
||||
CREATE INDEX "BomCostingAnalysisRun_caseId_status_idx"
|
||||
ON "BomCostingAnalysisRun"("caseId", "status");
|
||||
|
||||
CREATE INDEX "BomCostingAnalysisRun_factAnalysisId_idx"
|
||||
ON "BomCostingAnalysisRun"("factAnalysisId");
|
||||
|
||||
CREATE INDEX "BomCostingAnalysisRun_status_updatedAt_idx"
|
||||
ON "BomCostingAnalysisRun"("status", "updatedAt");
|
||||
|
||||
CREATE INDEX "BomCostingAnalysisRun_createdById_idx"
|
||||
ON "BomCostingAnalysisRun"("createdById");
|
||||
|
||||
ALTER TABLE "BomCostingAnalysisRun"
|
||||
ADD CONSTRAINT "BomCostingAnalysisRun_caseId_fkey"
|
||||
FOREIGN KEY ("caseId") REFERENCES "BomCostingCase"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "BomCostingAnalysisRun"
|
||||
ADD CONSTRAINT "BomCostingAnalysisRun_factAnalysisId_fkey"
|
||||
FOREIGN KEY ("factAnalysisId") REFERENCES "BomCostingSourceFactAnalysis"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "BomCostingAnalysisRun"
|
||||
ADD CONSTRAINT "BomCostingAnalysisRun_createdById_fkey"
|
||||
FOREIGN KEY ("createdById") REFERENCES "User"("id")
|
||||
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
INSERT INTO "BomCostingAnalysisRun" (
|
||||
"id",
|
||||
"caseId",
|
||||
"revision",
|
||||
"mode",
|
||||
"status",
|
||||
"factAnalysisId",
|
||||
"resultSnapshot",
|
||||
"model",
|
||||
"callId",
|
||||
"errorMessage",
|
||||
"createdById",
|
||||
"startedAt",
|
||||
"completedAt",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
)
|
||||
SELECT
|
||||
'legacy-' || c."id",
|
||||
c."id",
|
||||
1,
|
||||
'INITIAL'::"BomCostingAnalysisRunMode",
|
||||
CASE c."status"
|
||||
WHEN 'ANALYZING' THEN 'PROCESSING'::"BomCostingAnalysisRunStatus"
|
||||
WHEN 'READY' THEN 'READY'::"BomCostingAnalysisRunStatus"
|
||||
WHEN 'FAILED' THEN 'FAILED'::"BomCostingAnalysisRunStatus"
|
||||
ELSE 'PENDING'::"BomCostingAnalysisRunStatus"
|
||||
END,
|
||||
(
|
||||
SELECT f."id"
|
||||
FROM "BomCostingSourceFactAnalysis" f
|
||||
WHERE f."sourceSnapshotId" = c."sourceSnapshotId"
|
||||
AND f."schemaVersion" = 'competitor-facts/v1'
|
||||
ORDER BY f."revision" DESC
|
||||
LIMIT 1
|
||||
),
|
||||
c."rawAnalysisSnapshot",
|
||||
'',
|
||||
c."providerTaskId",
|
||||
CASE WHEN c."status" = 'FAILED' THEN c."statusMessage" ELSE '' END,
|
||||
c."createdById",
|
||||
c."processingStartedAt",
|
||||
c."processingFinishedAt",
|
||||
c."createdAt",
|
||||
c."updatedAt"
|
||||
FROM "BomCostingCase" c;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user